diff options
Diffstat (limited to '')
140 files changed, 49872 insertions, 0 deletions
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<CompositorVsyncDispatcher> aVsyncDispatcher, + RefPtr<CompositorWidgetVsyncObserver> 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<CompositorVsyncDispatcher> aVsyncDispatcher, + RefPtr<CompositorWidgetVsyncObserver> 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<CompositorVsyncDispatcher> mVsyncDispatcher; + RefPtr<CompositorWidgetVsyncObserver> 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<VsyncObserver> 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<VsyncObserver> GetVsyncObserver() const override; + + mozilla::ipc::IPCResult RecvNotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) override; + + private: + RefPtr<VsyncObserver> 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 <gdk/gdk.h> +#include <gdk/gdkx.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <dlfcn.h> + +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<nsDMABufDevice*>(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<nsDMABufDevice*>(data); + if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0 && version > 2) { + auto* dmabuf = WaylandRegistryBind<zwp_linux_dmabuf_v1>( + 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 <fcntl.h> +#include <getopt.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/time.h> +#include <dlfcn.h> +#include <sys/mman.h> +#include <sys/eventfd.h> +#include <poll.h> + +#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<type>(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<int> 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> DMABufSurface::CreateDMABufSurface( + const mozilla::layers::SurfaceDescriptor& aDesc) { + const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf(); + RefPtr<DMABufSurface> 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<uint32_t, DMABUF_BUFFER_PLANES> width; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> height; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> format; + AutoTArray<ipc::FileDescriptor, DMABUF_BUFFER_PLANES> fds; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> strides; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> offsets; + AutoTArray<uintptr_t, DMABUF_BUFFER_PLANES> images; + AutoTArray<ipc::FileDescriptor, 1> fenceFDs; + AutoTArray<ipc::FileDescriptor, 1> 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<EGLint> 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> DMABufSurfaceRGBA::CreateDMABufSurface( + int aWidth, int aHeight, int aDMABufSurfaceFlags) { + RefPtr<DMABufSurfaceRGBA> surf = new DMABufSurfaceRGBA(); + if (!surf->Create(aWidth, aHeight, aDMABufSurfaceFlags)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CreateYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc) { + RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV(); + if (!surf->UpdateYUVData(aDesc)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData, int* aLineSizes) { + RefPtr<DMABufSurfaceYUV> 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<uint32_t, DMABUF_BUFFER_PLANES> width; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> height; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> format; + AutoTArray<ipc::FileDescriptor, DMABUF_BUFFER_PLANES> fds; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> strides; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> offsets; + AutoTArray<ipc::FileDescriptor, 1> fenceFDs; + AutoTArray<ipc::FileDescriptor, 1> 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<EGLint> 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 <stdint.h> +#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<DMABufSurface> 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<mozilla::gl::GLContext> mGL; + + int mGlobalRefCountFd; + uint32_t mUID; +}; + +class DMABufSurfaceRGBA : public DMABufSurface { + public: + static already_AddRefed<DMABufSurfaceRGBA> 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<DMABufSurfaceYUV> CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData = nullptr, + int* aLineSizes = nullptr); + + static already_AddRefed<DMABufSurfaceYUV> 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<T> with various kinds of GObjects + +#include <gdk/gdk.h> +#include <gtk/gtk.h> +#include "mozilla/RefPtr.h" + +namespace mozilla { + +template <typename T> +struct GObjectRefPtrTraits { + static void AddRef(T* aObject) { g_object_ref(aObject); } + static void Release(T* aObject) { g_object_unref(aObject); } +}; + +template <> +struct RefPtrTraits<GtkWidget> : public GObjectRefPtrTraits<GtkWidget> {}; + +template <> +struct RefPtrTraits<GdkDragContext> + : public GObjectRefPtrTraits<GdkDragContext> {}; + +} // 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<gfx::DrawTarget> GtkCompositorWidget::StartRemoteDrawing() { + return nullptr; +} +void GtkCompositorWidget::EndRemoteDrawing() {} + +already_AddRefed<gfx::DrawTarget> +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<uintptr_t>(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<gfx::DrawTarget> 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<gfx::DrawTarget> StartRemoteDrawing() override; + void EndRemoteDrawing() override; + + already_AddRefed<gfx::DrawTarget> 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<gfx::DrawTarget> 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<LayoutDeviceIntSize> 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<TextRangeStyle::LineStyleType>(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 <input type="text">) 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<double>(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<double>(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<size_t>(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<nsWindow*>(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<nsWindow*>(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<GdkEventKey*>(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 + // <input type="password"> 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<IMContextWrapper> kungFuDeathGrip(this); + RefPtr<nsWindow> 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 + // <input type="password" style="ime-mode: enabled;">. + // 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: + // <input type="text" style="ime-mode: disabled;"> + // <input type="password" style="ime-mode: enabled;"> + // + // 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<bool> 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<TextEventDispatcher> 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<nsWindow> 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<GtkIMContext*>(g_object_ref(aContext)); + MOZ_ASSERT(mComposingContext); + + // Keep the last focused window alive + RefPtr<nsWindow> 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<TextEventDispatcher> 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<TextEventDispatcher> 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<TextRangeArray> 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<nsWindow> 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<TextEventDispatcher> 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<nsWindow> 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<TextRangeArray> 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> 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<uint32_t>(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<PangoAttrInt*>( + 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<PangoAttrColor*>( + 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<PangoAttrColor*>( + 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<PangoAttrColor*>( + 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<gint>(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<nsWindow*>(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<nsWindow> 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 <gdk/gdk.h> +#include <gtk/gtk.h> + +#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 <textarea> or <input> have focus, it means offset + // from the first character of them. When a HTML editor has focus, it + // means offset from the first character of the root element of the editor. + uint32_t mCompositionStart; + + // mDispatchedCompositionString is the latest composition string which + // was dispatched by compositionupdate event. + nsString mDispatchedCompositionString; + + // mSelectedStringRemovedByComposition is the selected string which was + // removed by first compositionchange event. + nsString mSelectedStringRemovedByComposition; + + // OnKeyEvent() temporarily sets mProcessingKeyEvent to the given native + // event. + GdkEventKey* mProcessingKeyEvent; + + /** + * GdkEventKeyQueue stores *copy* of GdkEventKey instances. However, this + * must be safe to our usecase since it has |time| and the value should not + * be same as older event. + */ + class GdkEventKeyQueue final { + public: + ~GdkEventKeyQueue() { Clear(); } + + void Clear() { + if (!mEvents.IsEmpty()) { + RemoveEventsAt(0, mEvents.Length()); + } + } + + /** + * PutEvent() puts new event into the queue. + */ + void PutEvent(const GdkEventKey* aEvent) { + GdkEventKey* newEvent = reinterpret_cast<GdkEventKey*>( + gdk_event_copy(reinterpret_cast<const GdkEvent*>(aEvent))); + newEvent->state &= GDK_MODIFIER_MASK; + mEvents.AppendElement(newEvent); + } + + /** + * RemoveEvent() removes oldest same event and its preceding events + * from the queue. + */ + void RemoveEvent(const GdkEventKey* aEvent) { + size_t index = IndexOf(aEvent); + if (NS_WARN_IF(index == GdkEventKeyQueue::NoIndex())) { + return; + } + RemoveEventsAt(0, index + 1); + } + + /** + * FirstEvent() returns oldest event in the queue. + */ + GdkEventKey* GetFirstEvent() const { + if (mEvents.IsEmpty()) { + return nullptr; + } + return mEvents[0]; + } + + bool IsEmpty() const { return mEvents.IsEmpty(); } + + static size_t NoIndex() { return nsTArray<GdkEventKey*>::NoIndex; } + size_t Length() const { return mEvents.Length(); } + size_t IndexOf(const GdkEventKey* aEvent) const { + static_assert(!(GDK_MODIFIER_MASK & (1 << 24)), + "We assumes 25th bit is used by some IM, but used by GDK"); + static_assert(!(GDK_MODIFIER_MASK & (1 << 25)), + "We assumes 26th bit is used by some IM, but used by GDK"); + for (size_t i = 0; i < mEvents.Length(); i++) { + GdkEventKey* event = mEvents[i]; + // It must be enough to compare only type, time, keyval and + // part of state. Note that we cannot compaire two events + // simply since IME may have changed unused bits of state. + if (event->time == aEvent->time) { + if (NS_WARN_IF(event->type != aEvent->type) || + NS_WARN_IF(event->keyval != aEvent->keyval) || + NS_WARN_IF(event->state != (aEvent->state & GDK_MODIFIER_MASK))) { + continue; + } + } + return i; + } + return GdkEventKeyQueue::NoIndex(); + } + + private: + nsTArray<GdkEventKey*> mEvents; + + void RemoveEventsAt(size_t aStart, size_t aCount) { + for (size_t i = aStart; i < aStart + aCount; i++) { + gdk_event_free(reinterpret_cast<GdkEvent*>(mEvents[i])); + } + mEvents.RemoveElementsAt(aStart, aCount); + } + }; + // OnKeyEvent() append mPostingKeyEvents when it believes that a key event + // is posted to other IME process. + GdkEventKeyQueue mPostingKeyEvents; + + static guint16 sWaitingSynthesizedKeyPressHardwareKeyCode; + + struct Range { + uint32_t mOffset; + uint32_t mLength; + + Range() : mOffset(UINT32_MAX), mLength(UINT32_MAX) {} + + bool IsValid() const { return mOffset != UINT32_MAX; } + void Clear() { + mOffset = UINT32_MAX; + mLength = UINT32_MAX; + } + }; + + // current target offset and length of IME composition + Range mCompositionTargetRange; + + // mCompositionState indicates current status of composition. + enum eCompositionState : uint8_t { + eCompositionState_NotComposing, + eCompositionState_CompositionStartDispatched, + eCompositionState_CompositionChangeEventDispatched + }; + eCompositionState mCompositionState; + + bool IsComposing() const { + return (mCompositionState != eCompositionState_NotComposing); + } + + bool IsComposingOn(GtkIMContext* aContext) const { + return IsComposing() && mComposingContext == aContext; + } + + bool IsComposingOnCurrentContext() const { + return IsComposingOn(GetCurrentContext()); + } + + bool EditorHasCompositionString() { + return (mCompositionState == + eCompositionState_CompositionChangeEventDispatched); + } + + /** + * Checks if aContext is valid context for handling composition. + * + * @param aContext An IM context which is specified by native + * composition events. + * @return true if the context is valid context for + * handling composition. Otherwise, false. + */ + bool IsValidContext(GtkIMContext* aContext) const; + + const char* GetCompositionStateName() { + switch (mCompositionState) { + case eCompositionState_NotComposing: + return "NotComposing"; + case eCompositionState_CompositionStartDispatched: + return "CompositionStartDispatched"; + case eCompositionState_CompositionChangeEventDispatched: + return "CompositionChangeEventDispatched"; + default: + return "InvaildState"; + } + } + + // mIMContextID indicates the ID of mContext. This is actually indicates + // IM which user selected. + IMContextID mIMContextID; + + struct Selection final { + nsString mString; + uint32_t mOffset; + WritingMode mWritingMode; + + Selection() : mOffset(UINT32_MAX) {} + + void Clear() { + mString.Truncate(); + mOffset = UINT32_MAX; + mWritingMode = WritingMode(); + } + void CollapseTo(uint32_t aOffset, const WritingMode& aWritingMode) { + mWritingMode = aWritingMode; + mOffset = aOffset; + mString.Truncate(); + } + + void Assign(const IMENotification& aIMENotification); + void Assign(const WidgetQueryContentEvent& aSelectedTextEvent); + + bool IsValid() const { return mOffset != UINT32_MAX; } + bool Collapsed() const { return mString.IsEmpty(); } + uint32_t Length() const { return mString.Length(); } + uint32_t EndOffset() const { + if (NS_WARN_IF(!IsValid())) { + return UINT32_MAX; + } + CheckedInt<uint32_t> endOffset = + CheckedInt<uint32_t>(mOffset) + mString.Length(); + if (NS_WARN_IF(!endOffset.isValid())) { + return UINT32_MAX; + } + return endOffset.value(); + } + } mSelection; + bool EnsureToCacheSelection(nsAString* aSelectedString = nullptr); + + // mIsIMFocused is set to TRUE when we call gtk_im_context_focus_in(). And + // it's set to FALSE when we call gtk_im_context_focus_out(). + bool mIsIMFocused; + // mFallbackToKeyEvent is set to false when this class starts to handle + // a native key event (at that time, mProcessingKeyEvent is set to the + // native event). If active IME just commits composition with a character + // which is produced by the key with current keyboard layout, this is set + // to true. + bool mFallbackToKeyEvent; + // mKeyboardEventWasDispatched is used by OnKeyEvent() and + // MaybeDispatchKeyEventAsProcessedByIME(). + // MaybeDispatchKeyEventAsProcessedByIME() dispatches an eKeyDown or + // eKeyUp event event if the composition is caused by a native + // key press event. If this is true, a keyboard event has been dispatched + // for the native event. If so, MaybeDispatchKeyEventAsProcessedByIME() + // won't dispatch keyboard event anymore. + bool mKeyboardEventWasDispatched; + // Whether the keyboard event which as dispatched at setting + // mKeyboardEventWasDispatched to true was consumed or not. + bool mKeyboardEventWasConsumed; + // mIsDeletingSurrounding is true while OnDeleteSurroundingNative() is + // trying to delete the surrounding text. + bool mIsDeletingSurrounding; + // mLayoutChanged is true after OnLayoutChange() is called. This is reset + // when eCompositionChange is being dispatched. + bool mLayoutChanged; + // mSetCursorPositionOnKeyEvent true when caret rect or position is updated + // with no composition. If true, we update candidate window position + // before key down + bool mSetCursorPositionOnKeyEvent; + // mPendingResettingIMContext becomes true if selection change notification + // is received during composition but the selection change occurred before + // starting the composition. In such case, we cannot notify IME of + // selection change during composition because we don't want to commit + // the composition in such case. However, we should notify IME of the + // selection change after the composition is committed. + bool mPendingResettingIMContext; + // mRetrieveSurroundingSignalReceived is true after "retrieve_surrounding" + // signal is received until selection is changed in Gecko. + bool mRetrieveSurroundingSignalReceived; + // mMaybeInDeadKeySequence is set to true when we detect a dead key press + // and set to false when we're sure dead key sequence has been finished. + // Note that we cannot detect which key event causes ending a dead key + // sequence. For example, when you press dead key grave with ibus Spanish + // keyboard layout, it just consumes the key event when we call + // gtk_im_context_filter_keypress(). Then, pressing "Escape" key cancels + // the dead key sequence but we don't receive any signal and it's consumed + // by gtk_im_context_filter_keypress() normally. On the other hand, when + // pressing "Shift" key causes exactly same behavior but dead key sequence + // isn't finished yet. + bool mMaybeInDeadKeySequence; + // mIsIMInAsyncKeyHandlingMode is set to true if we know that IM handles + // key events asynchronously. I.e., filtered key event may come again + // later. + bool mIsIMInAsyncKeyHandlingMode; + // mIsKeySnooped is set to true if IM uses key snooper to listen key events. + // In such case, we won't receive key events if IME consumes the event. + bool mIsKeySnooped; + + // sLastFocusedContext is a pointer to the last focused instance of this + // class. When a instance is destroyed and sLastFocusedContext refers it, + // this is cleared. So, this refers valid pointer always. + static IMContextWrapper* sLastFocusedContext; + + // sUseSimpleContext indeicates if password editors and editors with + // |ime-mode: disabled;| should use GtkIMContextSimple. + // If true, they use GtkIMContextSimple. Otherwise, not. + static bool sUseSimpleContext; + + // Callback methods for native IME events. These methods should call + // the related instance methods simply. + static gboolean OnRetrieveSurroundingCallback(GtkIMContext* aContext, + IMContextWrapper* aModule); + static gboolean OnDeleteSurroundingCallback(GtkIMContext* aContext, + gint aOffset, gint aNChars, + IMContextWrapper* aModule); + static void OnCommitCompositionCallback(GtkIMContext* aContext, + const gchar* aString, + IMContextWrapper* aModule); + static void OnChangeCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule); + static void OnStartCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule); + static void OnEndCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule); + + // The instance methods for the native IME events. + gboolean OnRetrieveSurroundingNative(GtkIMContext* aContext); + gboolean OnDeleteSurroundingNative(GtkIMContext* aContext, gint aOffset, + gint aNChars); + void OnCommitCompositionNative(GtkIMContext* aContext, const gchar* aString); + void OnChangeCompositionNative(GtkIMContext* aContext); + void OnStartCompositionNative(GtkIMContext* aContext); + void OnEndCompositionNative(GtkIMContext* aContext); + + /** + * GetCurrentContext() returns current IM context which is chosen with the + * enabled state. + * WARNING: + * When this class receives some signals for a composition after focus + * is moved in Gecko, the result of this may be different from given + * context by the signals. + */ + GtkIMContext* GetCurrentContext() const; + + /** + * GetActiveContext() returns a composing context or current context. + */ + GtkIMContext* GetActiveContext() const { + return mComposingContext ? mComposingContext : GetCurrentContext(); + } + + // If the owner window and IM context have been destroyed, returns TRUE. + bool IsDestroyed() { return !mOwnerWindow; } + + // Sets focus to the instance of this class. + void Focus(); + + // Steals focus from the instance of this class. + void Blur(); + + // Initializes the instance. + void Init(); + + /** + * Reset the active context, i.e., if there is mComposingContext, reset it. + * Otherwise, reset current context. Note that all native composition + * events during calling this will be ignored. + */ + void ResetIME(); + + // Gets the current composition string by the native APIs. + void GetCompositionString(GtkIMContext* aContext, + nsAString& aCompositionString); + + /** + * Generates our text range array from current composition string. + * + * @param aContext A GtkIMContext which is being handled. + * @param aCompositionString The data to be dispatched with + * compositionchange event. + */ + already_AddRefed<TextRangeArray> CreateTextRangeArray( + GtkIMContext* aContext, const nsAString& aCompositionString); + + /** + * SetTextRange() initializes aTextRange with aPangoAttrIter. + * + * @param aPangoAttrIter An iter which represents a clause of the + * composition string. + * @param aUTF8CompositionString The whole composition string (UTF-8). + * @param aUTF16CaretOffset The caret offset in the composition + * string encoded as UTF-16. + * @param aTextRange The result. + * @return true if this initializes aTextRange. + * Otherwise, false. + */ + bool SetTextRange(PangoAttrIterator* aPangoAttrIter, + const gchar* aUTF8CompositionString, + uint32_t aUTF16CaretOffset, TextRange& aTextRange) const; + + /** + * ToNscolor() converts the PangoColor in aPangoAttrColor to nscolor. + */ + static nscolor ToNscolor(PangoAttrColor* aPangoAttrColor); + + /** + * Move the candidate window with "fake" cursor position. + * + * @param aContext A GtkIMContext which is being handled. + */ + void SetCursorPosition(GtkIMContext* aContext); + + // Queries the current selection offset of the window. + uint32_t GetSelectionOffset(nsWindow* aWindow); + + // Get current paragraph text content and cursor position + nsresult GetCurrentParagraph(nsAString& aText, uint32_t& aCursorPos); + + /** + * Delete text portion + * + * @param aContext A GtkIMContext which is being handled. + * @param aOffset Start offset of the range to delete. + * @param aNChars Count of characters to delete. It depends + * on |g_utf8_strlen()| what is one character. + */ + nsresult DeleteText(GtkIMContext* aContext, int32_t aOffset, + uint32_t aNChars); + + // Initializes the GUI event. + void InitEvent(WidgetGUIEvent& aEvent); + + // Called before destroying the context to work around some platform bugs. + void PrepareToDestroyContext(GtkIMContext* aContext); + + /** + * WARNING: + * Following methods dispatch gecko events. Then, the focused widget + * can be destroyed, and also it can be stolen focus. If they returns + * FALSE, callers cannot continue the composition. + * - MaybeDispatchKeyEventAsProcessedByIME + * - DispatchCompositionStart + * - DispatchCompositionChangeEvent + * - DispatchCompositionCommitEvent + */ + + /** + * Dispatch an eKeyDown or eKeyUp event whose mKeyCode value is + * NS_VK_PROCESSKEY and mKeyNameIndex is KEY_NAME_INDEX_Process if + * we're not in a dead key sequence, mProcessingKeyEvent is nullptr + * but mPostingKeyEvents is not empty or mProcessingKeyEvent is not + * nullptr and mKeyboardEventWasDispatched is still false. If this + * dispatches a keyboard event, this sets mKeyboardEventWasDispatched + * to true. + * + * @param aFollowingEvent The following event message. + * @return If the caller can continue to handle + * composition, returns true. Otherwise, + * false. For example, if focus is moved + * by dispatched keyboard event, returns + * false. + */ + bool MaybeDispatchKeyEventAsProcessedByIME(EventMessage aFollowingEvent); + + /** + * Dispatches a composition start event. + * + * @param aContext A GtkIMContext which is being handled. + * @return true if the focused widget is neither + * destroyed nor changed. Otherwise, false. + */ + bool DispatchCompositionStart(GtkIMContext* aContext); + + /** + * Dispatches a compositionchange event. + * + * @param aContext A GtkIMContext which is being handled. + * @param aCompositionString New composition string. + * @return true if the focused widget is neither + * destroyed nor changed. Otherwise, false. + */ + bool DispatchCompositionChangeEvent(GtkIMContext* aContext, + const nsAString& aCompositionString); + + /** + * Dispatches a compositioncommit event or compositioncommitasis event. + * + * @param aContext A GtkIMContext which is being handled. + * @param aCommitString If this is nullptr, the composition will + * be committed with last dispatched data. + * Otherwise, the composition will be + * committed with this value. + * @return true if the focused widget is neither + * destroyed nor changed. Otherwise, false. + */ + bool DispatchCompositionCommitEvent(GtkIMContext* aContext, + const nsAString* aCommitString = nullptr); +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef IMContextWrapper_h_ diff --git a/widget/gtk/InProcessGtkCompositorWidget.cpp b/widget/gtk/InProcessGtkCompositorWidget.cpp new file mode 100644 index 0000000000..41581184fb --- /dev/null +++ b/widget/gtk/InProcessGtkCompositorWidget.cpp @@ -0,0 +1,45 @@ +/* -*- 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 "HeadlessCompositorWidget.h" +#include "HeadlessWidget.h" +#include "mozilla/widget/PlatformWidgetTypes.h" + +#include "InProcessGtkCompositorWidget.h" +#include "nsWindow.h" + +namespace mozilla { +namespace widget { + +/* static */ +RefPtr<CompositorWidget> CompositorWidget::CreateLocal( + const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, nsIWidget* aWidget) { + if (aInitData.type() == + CompositorWidgetInitData::THeadlessCompositorWidgetInitData) { + return new HeadlessCompositorWidget( + aInitData.get_HeadlessCompositorWidgetInitData(), aOptions, + static_cast<HeadlessWidget*>(aWidget)); + } else { + return new InProcessGtkCompositorWidget( + aInitData.get_GtkCompositorWidgetInitData(), aOptions, + static_cast<nsWindow*>(aWidget)); + } +} + +InProcessGtkCompositorWidget::InProcessGtkCompositorWidget( + const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, nsWindow* aWindow) + : GtkCompositorWidget(aInitData, aOptions, aWindow) {} + +void InProcessGtkCompositorWidget::ObserveVsync(VsyncObserver* aObserver) { + if (RefPtr<CompositorVsyncDispatcher> cvd = + mWidget->GetCompositorVsyncDispatcher()) { + cvd->SetCompositorVsyncObserver(aObserver); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/InProcessGtkCompositorWidget.h b/widget/gtk/InProcessGtkCompositorWidget.h new file mode 100644 index 0000000000..bd7e68d6fb --- /dev/null +++ b/widget/gtk/InProcessGtkCompositorWidget.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_gtk_InProcessGtkCompositorWidget_h +#define widget_gtk_InProcessGtkCompositorWidget_h + +#include "GtkCompositorWidget.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +class InProcessGtkCompositorWidget final : public GtkCompositorWidget { + public: + InProcessGtkCompositorWidget(const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, + nsWindow* aWindow); + + // CompositorWidgetDelegate + + void ObserveVsync(VsyncObserver* aObserver) override; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_InProcessGtkCompositorWidget_h diff --git a/widget/gtk/MPRISInterfaceDescription.h b/widget/gtk/MPRISInterfaceDescription.h new file mode 100644 index 0000000000..28e719e651 --- /dev/null +++ b/widget/gtk/MPRISInterfaceDescription.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_ +#define WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_ + +#include <gio/gio.h> + +extern const gchar introspection_xml[] = + // adopted from https://github.com/freedesktop/mpris-spec/blob/master/spec/org.mpris.MediaPlayer2.xml + // everything starting with tp can be removed, as it is used for HTML Spec Documentation Generation + "<node>" + "<interface name=\"org.mpris.MediaPlayer2\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "<method name=\"Raise\"/>" + "<method name=\"Quit\"/>" + "<property name=\"CanQuit\" type=\"b\" access=\"read\"/>" + "<property name=\"CanRaise\" type=\"b\" access=\"read\"/>" + "<property name=\"HasTrackList\" type=\"b\" access=\"read\"/>" + "<property name=\"Identity\" type=\"s\" access=\"read\"/>" + "<property name=\"DesktopEntry\" type=\"s\" access=\"read\"/>" + "<property name=\"SupportedUriSchemes\" type=\"as\" access=\"read\"/>" + "<property name=\"SupportedMimeTypes\" type=\"as\" access=\"read\"/>" + "</interface>" + // Note that every property emits a changed signal (which is default) apart from Position. + "<interface name=\"org.mpris.MediaPlayer2.Player\">" + "<method name=\"Next\"/>" + "<method name=\"Previous\"/>" + "<method name=\"Pause\"/>" + "<method name=\"PlayPause\"/>" + "<method name=\"Stop\"/>" + "<method name=\"Play\"/>" + "<method name=\"Seek\">" + "<arg direction=\"in\" type=\"x\" name=\"Offset\"/>" + "</method>" + "<method name=\"SetPosition\">" + "<arg direction=\"in\" type=\"o\" name=\"TrackId\"/>" + "<arg direction=\"in\" type=\"x\" name=\"Position\"/>" + "</method>" + "<method name=\"OpenUri\">" + "<arg direction=\"in\" type=\"s\" name=\"Uri\"/>" + "</method>" + "<property name=\"PlaybackStatus\" type=\"s\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"Rate\" type=\"d\" access=\"readwrite\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"Metadata\" type=\"a{sv}\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"Volume\" type=\"d\" access=\"readwrite\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"Position\" type=\"x\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"false\"/>" + "</property>" + "<property name=\"MinimumRate\" type=\"d\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"MaximumRate\" type=\"d\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"CanGoNext\" type=\"b\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"CanGoPrevious\" type=\"b\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"CanPlay\" type=\"b\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"CanPause\" type=\"b\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"CanSeek\" type=\"b\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>" + "</property>" + "<property name=\"CanControl\" type=\"b\" access=\"read\">" + "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"false\"/>" + "</property>" + "<signal name=\"Seeked\">" + "<arg name=\"Position\" type=\"x\"/>" + "</signal>" + "</interface>" + "</node>"; + +#endif // WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_ diff --git a/widget/gtk/MPRISServiceHandler.cpp b/widget/gtk/MPRISServiceHandler.cpp new file mode 100644 index 0000000000..c21b705b16 --- /dev/null +++ b/widget/gtk/MPRISServiceHandler.cpp @@ -0,0 +1,851 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MPRISServiceHandler.h" + +#include <stdint.h> +#include <inttypes.h> +#include <unordered_map> + +#include "MPRISInterfaceDescription.h" +#include "mozilla/dom/MediaControlUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "nsIXULAppInfo.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" + +// avoid redefined macro in unified build +#undef LOG +#define LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__)) + +namespace mozilla { +namespace widget { + +// A global counter tracking the total images saved in the system and it will be +// used to form a unique image file name. +static uint32_t gImageNumber = 0; + +static inline Maybe<mozilla::dom::MediaControlKey> GetMediaControlKey( + const gchar* aMethodName) { + const std::unordered_map<std::string, mozilla::dom::MediaControlKey> map = { + {"Raise", mozilla::dom::MediaControlKey::Focus}, + {"Next", mozilla::dom::MediaControlKey::Nexttrack}, + {"Previous", mozilla::dom::MediaControlKey::Previoustrack}, + {"Pause", mozilla::dom::MediaControlKey::Pause}, + {"PlayPause", mozilla::dom::MediaControlKey::Playpause}, + {"Stop", mozilla::dom::MediaControlKey::Stop}, + {"Play", mozilla::dom::MediaControlKey::Play}}; + + auto it = map.find(aMethodName); + return (it == map.end() ? Nothing() : Some(it->second)); +} + +static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aMethodName, GVariant* aParameters, + GDBusMethodInvocation* aInvocation, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + + Maybe<mozilla::dom::MediaControlKey> key = GetMediaControlKey(aMethodName); + if (key.isNothing()) { + g_dbus_method_invocation_return_error( + aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, + "Method %s.%s.%s not supported", aObjectPath, aInterfaceName, + aMethodName); + return; + } + + MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData); + if (handler->PressKey(key.value())) { + g_dbus_method_invocation_return_value(aInvocation, nullptr); + } else { + g_dbus_method_invocation_return_error( + aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "%s.%s.%s is not available now", aObjectPath, aInterfaceName, + aMethodName); + } +} + +enum class Property : uint8_t { + eIdentity, + eDesktopEntry, + eHasTrackList, + eCanRaise, + eCanQuit, + eSupportedUriSchemes, + eSupportedMimeTypes, + eCanGoNext, + eCanGoPrevious, + eCanPlay, + eCanPause, + eCanSeek, + eCanControl, + eGetPlaybackStatus, + eGetMetadata, +}; + +static inline Maybe<mozilla::dom::MediaControlKey> GetPairedKey( + Property aProperty) { + switch (aProperty) { + case Property::eCanRaise: + return Some(mozilla::dom::MediaControlKey::Focus); + case Property::eCanGoNext: + return Some(mozilla::dom::MediaControlKey::Nexttrack); + case Property::eCanGoPrevious: + return Some(mozilla::dom::MediaControlKey::Previoustrack); + case Property::eCanPlay: + return Some(mozilla::dom::MediaControlKey::Play); + case Property::eCanPause: + return Some(mozilla::dom::MediaControlKey::Pause); + default: + return Nothing(); + } +} + +static inline Maybe<Property> GetProperty(const gchar* aPropertyName) { + const std::unordered_map<std::string, Property> map = { + // org.mpris.MediaPlayer2 properties + {"Identity", Property::eIdentity}, + {"DesktopEntry", Property::eDesktopEntry}, + {"HasTrackList", Property::eHasTrackList}, + {"CanRaise", Property::eCanRaise}, + {"CanQuit", Property::eCanQuit}, + {"SupportedUriSchemes", Property::eSupportedUriSchemes}, + {"SupportedMimeTypes", Property::eSupportedMimeTypes}, + // org.mpris.MediaPlayer2.Player properties + {"CanGoNext", Property::eCanGoNext}, + {"CanGoPrevious", Property::eCanGoPrevious}, + {"CanPlay", Property::eCanPlay}, + {"CanPause", Property::eCanPause}, + {"CanSeek", Property::eCanSeek}, + {"CanControl", Property::eCanControl}, + {"PlaybackStatus", Property::eGetPlaybackStatus}, + {"Metadata", Property::eGetMetadata}}; + + auto it = map.find(aPropertyName); + return (it == map.end() ? Nothing() : Some(it->second)); +} + +static GVariant* HandleGetProperty(GDBusConnection* aConnection, + const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aPropertyName, GError** aError, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + + Maybe<Property> property = GetProperty(aPropertyName); + if (property.isNothing()) { + g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, + "%s.%s %s is not supported", aObjectPath, aInterfaceName, + aPropertyName); + return nullptr; + } + + MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData); + switch (property.value()) { + case Property::eSupportedUriSchemes: + case Property::eSupportedMimeTypes: + // No plan to implement OpenUri for now + return g_variant_new_strv(nullptr, 0); + case Property::eGetPlaybackStatus: + return handler->GetPlaybackStatus(); + case Property::eGetMetadata: + return handler->GetMetadataAsGVariant(); + case Property::eIdentity: + return g_variant_new_string(handler->Identity()); + case Property::eDesktopEntry: + return g_variant_new_string(handler->DesktopEntry()); + case Property::eHasTrackList: + case Property::eCanQuit: + case Property::eCanSeek: + return g_variant_new_boolean(false); + // Play/Pause would be blocked if CanControl is false + case Property::eCanControl: + return g_variant_new_boolean(true); + case Property::eCanRaise: + case Property::eCanGoNext: + case Property::eCanGoPrevious: + case Property::eCanPlay: + case Property::eCanPause: + Maybe<mozilla::dom::MediaControlKey> key = GetPairedKey(property.value()); + MOZ_ASSERT(key.isSome()); + return g_variant_new_boolean(handler->IsMediaKeySupported(key.value())); + } + + MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete"); + return nullptr; +} + +static gboolean HandleSetProperty(GDBusConnection* aConnection, + const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aPropertyName, GVariant* aValue, + GError** aError, gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s:%s setting is not supported", aInterfaceName, aPropertyName); + return false; +} + +static const GDBusInterfaceVTable gInterfaceVTable = { + HandleMethodCall, HandleGetProperty, HandleSetProperty}; + +void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection, + const gchar* aName, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection, + aName); +} + +void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection, + const gchar* aName, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName); +} + +void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection, + const gchar* aName, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection, + aName); +} + +void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection, + const gchar* aName) { + LOG("OnNameAcquired: %s", aName); + mConnection = aConnection; +} + +void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection, + const gchar* aName) { + LOG("OnNameLost: %s", aName); + mConnection = nullptr; + if (!mRootRegistrationId) { + return; + } + + if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) { + mRootRegistrationId = 0; + } else { + // Note: Most code examples in the internet probably dont't even check the + // result here, but + // according to the spec it _can_ return false. + LOG("Unable to unregister root object from within onNameLost!"); + } + + if (!mPlayerRegistrationId) { + return; + } + + if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) { + mPlayerRegistrationId = 0; + } else { + // Note: Most code examples in the internet probably dont't even check the + // result here, but + // according to the spec it _can_ return false. + LOG("Unable to unregister object from within onNameLost!"); + } +} + +void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection, + const gchar* aName) { + GError* error = nullptr; + LOG("OnBusAcquired: %s", aName); + + mRootRegistrationId = g_dbus_connection_register_object( + aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0], + &gInterfaceVTable, this, /* user_data */ + nullptr, /* user_data_free_func */ + &error); /* GError** */ + + if (mRootRegistrationId == 0) { + LOG("Failed at root registration: %s", + error ? error->message : "Unknown Error"); + if (error) { + g_error_free(error); + } + return; + } + + mPlayerRegistrationId = g_dbus_connection_register_object( + aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1], + &gInterfaceVTable, this, /* user_data */ + nullptr, /* user_data_free_func */ + &error); /* GError** */ + + if (mPlayerRegistrationId == 0) { + LOG("Failed at object registration: %s", + error ? error->message : "Unknown Error"); + if (error) { + g_error_free(error); + } + } +} + +bool MPRISServiceHandler::Open() { + MOZ_ASSERT(!mInitialized); + MOZ_ASSERT(NS_IsMainThread()); + GError* error = nullptr; + gchar serviceName[256]; + + InitIdentity(); + SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid()); + mOwnerId = + g_bus_own_name(G_BUS_TYPE_SESSION, serviceName, + // Enter a waiting queue until this service name is free + // (likely another FF instance is running/has been crashed) + G_BUS_NAME_OWNER_FLAGS_NONE, OnBusAcquiredStatic, + OnNameAcquiredStatic, OnNameLostStatic, this, nullptr); + + /* parse introspection data */ + mIntrospectionData = g_dbus_node_info_new_for_xml(introspection_xml, &error); + + if (!mIntrospectionData) { + LOG("Failed at parsing XML Interface definition: %s", + error ? error->message : "Unknown Error"); + if (error) { + g_error_free(error); + } + return false; + } + + mInitialized = true; + return true; +} + +MPRISServiceHandler::~MPRISServiceHandler() { + MOZ_ASSERT(!mInitialized); // Close hasn't been called! +} + +void MPRISServiceHandler::Close() { + gchar serviceName[256]; + SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid()); + + // Reset playback state and metadata before disconnect from dbus. + SetPlaybackState(dom::MediaSessionPlaybackState::None); + ClearMetadata(); + + OnNameLost(mConnection, serviceName); + + if (mOwnerId != 0) { + g_bus_unown_name(mOwnerId); + } + if (mIntrospectionData) { + g_dbus_node_info_unref(mIntrospectionData); + } + + mInitialized = false; + MediaControlKeySource::Close(); +} + +bool MPRISServiceHandler::IsOpened() const { return mInitialized; } + +void MPRISServiceHandler::InitIdentity() { + nsresult rv; + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1", &rv); + + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = appInfo->GetVendor(mIdentity); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = appInfo->GetName(mDesktopEntry); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mIdentity.Append(' '); + mIdentity.Append(mDesktopEntry); + + // Compute the desktop entry name like nsAppRunner does for g_set_prgname + ToLowerCase(mDesktopEntry); +} + +const char* MPRISServiceHandler::Identity() const { + MOZ_ASSERT(mInitialized); + return mIdentity.get(); +} + +const char* MPRISServiceHandler::DesktopEntry() const { + MOZ_ASSERT(mInitialized); + return mDesktopEntry.get(); +} + +bool MPRISServiceHandler::PressKey(mozilla::dom::MediaControlKey aKey) const { + MOZ_ASSERT(mInitialized); + if (!IsMediaKeySupported(aKey)) { + LOG("%s is not supported", ToMediaControlKeyStr(aKey)); + return false; + } + LOG("Press %s", ToMediaControlKeyStr(aKey)); + EmitEvent(aKey); + return true; +} + +void MPRISServiceHandler::SetPlaybackState( + dom::MediaSessionPlaybackState aState) { + LOG("SetPlaybackState"); + if (mPlaybackState == aState) { + return; + } + + MediaControlKeySource::SetPlaybackState(aState); + + GVariant* state = GetPlaybackStatus(); + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state); + + GVariant* parameters = g_variant_new( + "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr); + + LOG("Emitting MPRIS property changes for 'PlaybackStatus'"); + Unused << EmitPropertiesChangedSignal(parameters); +} + +GVariant* MPRISServiceHandler::GetPlaybackStatus() const { + switch (GetPlaybackState()) { + case dom::MediaSessionPlaybackState::Playing: + return g_variant_new_string("Playing"); + case dom::MediaSessionPlaybackState::Paused: + return g_variant_new_string("Paused"); + case dom::MediaSessionPlaybackState::None: + return g_variant_new_string("Stopped"); + default: + MOZ_ASSERT_UNREACHABLE("Invalid Playback State"); + return nullptr; + } +} + +void MPRISServiceHandler::SetMediaMetadata( + const dom::MediaMetadataBase& aMetadata) { + // Reset the index of the next available image to be fetched in the artwork, + // before checking the fetching process should be started or not. The image + // fetching process could be skipped if the image being fetching currently is + // in the artwork. If the current image fetching fails, the next availabe + // candidate should be the first image in the latest artwork + mNextImageIndex = 0; + + // No need to fetch a MPRIS image if + // 1) MPRIS image is being fetched, and the one in fetching is in the artwork + // 2) MPRIS image is not being fetched, and the one in use is in the artwork + if (!mFetchingUrl.IsEmpty()) { + if (mozilla::dom::IsImageIn(aMetadata.mArtwork, mFetchingUrl)) { + LOG("No need to load MPRIS image. The one being processed is in the " + "artwork"); + // Set MPRIS without the image first. The image will be loaded to MPRIS + // asynchronously once it's fetched and saved into a local file + SetMediaMetadataInternal(aMetadata); + return; + } + } else if (!mCurrentImageUrl.IsEmpty()) { + if (mozilla::dom::IsImageIn(aMetadata.mArtwork, mCurrentImageUrl)) { + LOG("No need to load MPRIS image. The one in use is in the artwork"); + SetMediaMetadataInternal(aMetadata, false); + return; + } + } + + // Set MPRIS without the image first then load the image to MPRIS + // asynchronously + SetMediaMetadataInternal(aMetadata); + LoadImageAtIndex(mNextImageIndex++); +} + +bool MPRISServiceHandler::EmitMetadataChanged() const { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "Metadata", GetMetadataAsGVariant()); + + GVariant* parameters = g_variant_new( + "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr); + + LOG("Emit MPRIS property changes for 'Metadata'"); + return EmitPropertiesChangedSignal(parameters); +} + +void MPRISServiceHandler::SetMediaMetadataInternal( + const dom::MediaMetadataBase& aMetadata, bool aClearArtUrl) { + mMPRISMetadata.UpdateFromMetadataBase(aMetadata); + if (aClearArtUrl) { + mMPRISMetadata.mArtUrl.Truncate(); + } + EmitMetadataChanged(); +} + +void MPRISServiceHandler::ClearMetadata() { + mMPRISMetadata.Clear(); + mImageFetchRequest.DisconnectIfExists(); + RemoveAllLocalImages(); + mCurrentImageUrl.Truncate(); + mFetchingUrl.Truncate(); + mNextImageIndex = 0; + mSupportedKeys = 0; + EmitMetadataChanged(); +} + +void MPRISServiceHandler::LoadImageAtIndex(const size_t aIndex) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aIndex >= mMPRISMetadata.mArtwork.Length()) { + LOG("Stop loading image to MPRIS. No available image"); + mImageFetchRequest.DisconnectIfExists(); + return; + } + + const mozilla::dom::MediaImage& image = mMPRISMetadata.mArtwork[aIndex]; + + if (!mozilla::dom::IsValidImageUrl(image.mSrc)) { + LOG("Skip the image with invalid URL. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + mImageFetchRequest.DisconnectIfExists(); + mFetchingUrl = image.mSrc; + + mImageFetcher = mozilla::MakeUnique<mozilla::dom::FetchImageHelper>(image); + RefPtr<MPRISServiceHandler> self = this; + mImageFetcher->FetchImage() + ->Then( + AbstractThread::MainThread(), __func__, + [this, self](const nsCOMPtr<imgIContainer>& aImage) { + LOG("The image is fetched successfully"); + mImageFetchRequest.Complete(); + + uint32_t size = 0; + char* data = nullptr; + // Only used to hold the image data + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = mozilla::dom::GetEncodedImageBuffer( + aImage, mMimeType, getter_AddRefs(inputStream), &size, &data); + if (NS_FAILED(rv) || !inputStream || size == 0 || !data) { + LOG("Failed to get the image buffer info. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + if (SetImageToDisplay(data, size)) { + mCurrentImageUrl = mFetchingUrl; + LOG("The MPRIS image is updated to the image from: %s", + NS_ConvertUTF16toUTF8(mCurrentImageUrl).get()); + } else { + LOG("Failed to set image to MPRIS"); + mCurrentImageUrl.Truncate(); + } + + mFetchingUrl.Truncate(); + }, + [this, self](bool) { + LOG("Failed to fetch image. Try next image"); + mImageFetchRequest.Complete(); + mFetchingUrl.Truncate(); + LoadImageAtIndex(mNextImageIndex++); + }) + ->Track(mImageFetchRequest); +} + +bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData, + uint32_t aDataSize) { + if (!RenewLocalImageFile(aImageData, aDataSize)) { + return false; + } + MOZ_ASSERT(mLocalImageFile); + + mMPRISMetadata.mArtUrl = nsCString("file://"); + mMPRISMetadata.mArtUrl.Append(mLocalImageFile->NativePath()); + + LOG("The image file is created at %s", mMPRISMetadata.mArtUrl.get()); + return EmitMetadataChanged(); +} + +bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData, + uint32_t aDataSize) { + MOZ_ASSERT(aImageData); + MOZ_ASSERT(aDataSize != 0); + + if (!InitLocalImageFile()) { + LOG("Failed to create a new image"); + return false; + } + + MOZ_ASSERT(mLocalImageFile); + nsCOMPtr<nsIOutputStream> out; + NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile, + PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + uint32_t written; + nsresult rv = out->Write(aImageData, aDataSize, &written); + if (NS_FAILED(rv) || written != aDataSize) { + LOG("Failed to write an image file"); + RemoveAllLocalImages(); + return false; + } + + return true; +} + +static const char* GetImageFileExtension(const char* aMimeType) { + MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0); + return "png"; +} + +bool MPRISServiceHandler::InitLocalImageFile() { + RemoveAllLocalImages(); + + if (!InitLocalImageFolder()) { + return false; + } + + MOZ_ASSERT(mLocalImageFolder); + MOZ_ASSERT(!mLocalImageFile); + nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile)); + if (NS_FAILED(rv)) { + LOG("Failed to get the image folder"); + return false; + } + + auto cleanup = + MakeScopeExit([this, self = RefPtr<MPRISServiceHandler>(this)] { + mLocalImageFile = nullptr; + }); + + // Create an unique file name to work around the file caching mechanism in the + // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's + // MPRIS, this pair will be cached. As long as the filename is same, even the + // file content specified by Y is changed to Z, the image will stay unchanged. + // The image shown in the Ubuntu's notification is still X instead of Z. + // Changing the filename constantly works around this problem + char filename[64]; + SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++, + GetImageFileExtension(mMimeType.get())); + + rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename)); + if (NS_FAILED(rv)) { + LOG("Failed to create an image filename"); + return false; + } + + rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600); + if (NS_FAILED(rv)) { + LOG("Failed to create an image file"); + return false; + } + + cleanup.release(); + return true; +} + +bool MPRISServiceHandler::InitLocalImageFolder() { + if (mLocalImageFolder && LocalImageFolderExists()) { + return true; + } + + nsresult rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR, + getter_AddRefs(mLocalImageFolder)); + if (NS_FAILED(rv) || !mLocalImageFolder) { + LOG("Failed to get the image folder"); + return false; + } + + auto cleanup = + MakeScopeExit([this, self = RefPtr<MPRISServiceHandler>(this)] { + mLocalImageFolder = nullptr; + }); + + rv = mLocalImageFolder->Append(u"firefox-mpris"_ns); + if (NS_FAILED(rv)) { + LOG("Failed to name an image folder"); + return false; + } + + if (!LocalImageFolderExists()) { + rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_FAILED(rv)) { + LOG("Failed to create an image folder"); + return false; + } + } + + cleanup.release(); + return true; +} + +void MPRISServiceHandler::RemoveAllLocalImages() { + if (!mLocalImageFolder || !LocalImageFolderExists()) { + return; + } + + nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true); + if (NS_FAILED(rv)) { + // It's ok to fail. The next removal is called when updating the + // media-session image, or closing the MPRIS. + LOG("Failed to remove images"); + } + + LOG("Abandon %s", + mLocalImageFile ? mLocalImageFile->NativePath().get() : "nothing"); + mMPRISMetadata.mArtUrl.Truncate(); + mLocalImageFile = nullptr; + mLocalImageFolder = nullptr; +} + +bool MPRISServiceHandler::LocalImageFolderExists() { + MOZ_ASSERT(mLocalImageFolder); + + bool exists; + nsresult rv = mLocalImageFolder->Exists(&exists); + return NS_SUCCEEDED(rv) && exists; +} + +GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "mpris:trackid", + g_variant_new("o", DBUS_MPRIS_TRACK_PATH)); + + g_variant_builder_add( + &builder, "{sv}", "xesam:title", + g_variant_new_string(static_cast<const gchar*>( + NS_ConvertUTF16toUTF8(mMPRISMetadata.mTitle).get()))); + + g_variant_builder_add( + &builder, "{sv}", "xesam:album", + g_variant_new_string(static_cast<const gchar*>( + NS_ConvertUTF16toUTF8(mMPRISMetadata.mAlbum).get()))); + + GVariantBuilder artistBuilder; + g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as")); + g_variant_builder_add( + &artistBuilder, "s", + static_cast<const gchar*>( + NS_ConvertUTF16toUTF8(mMPRISMetadata.mArtist).get())); + g_variant_builder_add(&builder, "{sv}", "xesam:artist", + g_variant_builder_end(&artistBuilder)); + + if (!mMPRISMetadata.mArtUrl.IsEmpty()) { + g_variant_builder_add(&builder, "{sv}", "mpris:artUrl", + g_variant_new_string(static_cast<const gchar*>( + mMPRISMetadata.mArtUrl.get()))); + } + + return g_variant_builder_end(&builder); +} + +void MPRISServiceHandler::EmitEvent(mozilla::dom::MediaControlKey aKey) const { + for (const auto& listener : mListeners) { + listener->OnActionPerformed(mozilla::dom::MediaControlAction(aKey)); + } +} + +struct InterfaceProperty { + const char* interface; + const char* property; +}; +static const std::unordered_map<mozilla::dom::MediaControlKey, + InterfaceProperty> + gKeyProperty = {{mozilla::dom::MediaControlKey::Focus, + {DBUS_MPRIS_INTERFACE, "CanRaise"}}, + {mozilla::dom::MediaControlKey::Nexttrack, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}}, + {mozilla::dom::MediaControlKey::Previoustrack, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}}, + {mozilla::dom::MediaControlKey::Play, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}}, + {mozilla::dom::MediaControlKey::Pause, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}}}; + +void MPRISServiceHandler::SetSupportedMediaKeys( + const MediaKeysArray& aSupportedKeys) { + uint32_t supportedKeys = 0; + for (const mozilla::dom::MediaControlKey& key : aSupportedKeys) { + supportedKeys |= GetMediaKeyMask(key); + } + + if (mSupportedKeys == supportedKeys) { + LOG("Supported keys stay the same"); + return; + } + + uint32_t oldSupportedKeys = mSupportedKeys; + mSupportedKeys = supportedKeys; + + // Emit related property changes + for (auto it : gKeyProperty) { + bool keyWasSupported = oldSupportedKeys & GetMediaKeyMask(it.first); + bool keyIsSupported = mSupportedKeys & GetMediaKeyMask(it.first); + if (keyWasSupported != keyIsSupported) { + LOG("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface, + it.second.property, keyIsSupported ? "true" : "false"); + EmitSupportedKeyChanged(it.first, keyIsSupported); + } + } +} + +bool MPRISServiceHandler::IsMediaKeySupported( + mozilla::dom::MediaControlKey aKey) const { + return mSupportedKeys & GetMediaKeyMask(aKey); +} + +bool MPRISServiceHandler::EmitSupportedKeyChanged( + mozilla::dom::MediaControlKey aKey, bool aSupported) const { + auto it = gKeyProperty.find(aKey); + if (it == gKeyProperty.end()) { + LOG("No property for %s", ToMediaControlKeyStr(aKey)); + return false; + } + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", + static_cast<const gchar*>(it->second.property), + g_variant_new_boolean(aSupported)); + + GVariant* parameters = g_variant_new( + "(sa{sv}as)", static_cast<const gchar*>(it->second.interface), &builder, + nullptr); + + LOG("Emit MPRIS property changes for '%s.%s'", it->second.interface, + it->second.property); + return EmitPropertiesChangedSignal(parameters); +} + +bool MPRISServiceHandler::EmitPropertiesChangedSignal( + GVariant* aParameters) const { + if (!mConnection) { + LOG("No D-Bus Connection. Cannot emit properties changed signal"); + return false; + } + + GError* error = nullptr; + if (!g_dbus_connection_emit_signal( + mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH, + "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters, + &error)) { + LOG("Failed to emit MPRIS property changes: %s", + error ? error->message : "Unknown Error"); + if (error) { + g_error_free(error); + } + return false; + } + + return true; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/MPRISServiceHandler.h b/widget/gtk/MPRISServiceHandler.h new file mode 100644 index 0000000000..6d6c898088 --- /dev/null +++ b/widget/gtk/MPRISServiceHandler.h @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_ +#define WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_ + +#include <gio/gio.h> +#include "mozilla/dom/FetchImageHelper.h" +#include "mozilla/dom/MediaControlKeySource.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsIFile.h" +#include "nsMimeTypes.h" +#include "nsString.h" + +#define DBUS_MPRIS_SERVICE_NAME "org.mpris.MediaPlayer2.firefox" +#define DBUS_MPRIS_OBJECT_PATH "/org/mpris/MediaPlayer2" +#define DBUS_MPRIS_INTERFACE "org.mpris.MediaPlayer2" +#define DBUS_MPRIS_PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player" +#define DBUS_MPRIS_TRACK_PATH "/org/mpris/MediaPlayer2/firefox" + +namespace mozilla { +namespace widget { + +/** + * This class implements the "MPRIS" D-Bus Service + * (https://specifications.freedesktop.org/mpris-spec/2.2), + * which is used to communicate with the Desktop Environment about the + * Multimedia playing in Gecko. + * Note that this interface requires many methods which may not be supported by + * Gecko, the interface + * however provides CanXYZ properties for these methods, so the method is + * defined but won't be executed. + * + * Also note that the following defines are for parts that the MPRIS Spec + * defines optional. The code won't + * compile with any of the defines set, yet, as those aren't implemented yet and + * probably never will be of + * use for gecko. For sake of completeness, they have been added until the + * decision about their implementation + * is finally made. + * + * The constexpr'ed methods are capabilities of the user agent known at compile + * time, e.g. we decided at + * compile time whether we ever want to support closing the user agent via MPRIS + * (Quit() and CanQuit()). + * + * Other properties like CanPlay() might depend on the runtime state (is there + * media available for playback?) + * and thus aren't a constexpr but merely a const method. + */ +class MPRISServiceHandler final : public dom::MediaControlKeySource { + NS_INLINE_DECL_REFCOUNTING(MPRISServiceHandler, override) + public: + // Note that this constructor does NOT initialize the MPRIS Service but only + // this class. The method Open() is responsible for registering and MAY FAIL. + + // The image format used in MPRIS is based on the mMimeType here. Although + // IMAGE_JPEG or IMAGE_BMP are valid types as well but a png image with + // transparent background will be converted into a jpeg/bmp file with a + // colored background IMAGE_PNG format seems to be the best choice for now. + MPRISServiceHandler() : mMimeType(IMAGE_PNG){}; + bool Open() override; + void Close() override; + bool IsOpened() const override; + + // From the EventSource. + void SetPlaybackState(dom::MediaSessionPlaybackState aState) override; + + // GetPlaybackState returns dom::PlaybackState. GetPlaybackStatus returns this + // state converted into d-bus variants. + GVariant* GetPlaybackStatus() const; + + const char* Identity() const; + const char* DesktopEntry() const; + bool PressKey(dom::MediaControlKey aKey) const; + + void SetMediaMetadata(const dom::MediaMetadataBase& aMetadata) override; + GVariant* GetMetadataAsGVariant() const; + + void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override; + + bool IsMediaKeySupported(dom::MediaControlKey aKey) const; + + private: + ~MPRISServiceHandler(); + + // Note: Registration Ids for the D-Bus start with 1, so a value of 0 + // indicates an error (or an object which wasn't initialized yet) + + // a handle to our bus registration/ownership + guint mOwnerId = 0; + // This is for the interface org.mpris.MediaPlayer2 + guint mRootRegistrationId = 0; + // This is for the interface org.mpris.MediaPlayer2.Player + guint mPlayerRegistrationId = 0; + GDBusNodeInfo* mIntrospectionData = nullptr; + GDBusConnection* mConnection = nullptr; + bool mInitialized = false; + nsAutoCString mIdentity; + nsAutoCString mDesktopEntry; + + nsCString mMimeType; + + // A bitmask indicating what keys are enabled + uint32_t mSupportedKeys = 0; + + class MPRISMetadata : public dom::MediaMetadataBase { + public: + MPRISMetadata() = default; + ~MPRISMetadata() = default; + + void UpdateFromMetadataBase(const dom::MediaMetadataBase& aMetadata) { + mTitle = aMetadata.mTitle; + mArtist = aMetadata.mArtist; + mAlbum = aMetadata.mAlbum; + mArtwork = aMetadata.mArtwork; + } + void Clear() { + UpdateFromMetadataBase(MediaMetadataBase::EmptyData()); + mArtUrl.Truncate(); + } + + nsCString mArtUrl; + }; + MPRISMetadata mMPRISMetadata; + + // The saved image file fetched from the URL + nsCOMPtr<nsIFile> mLocalImageFile; + nsCOMPtr<nsIFile> mLocalImageFolder; + + mozilla::UniquePtr<mozilla::dom::FetchImageHelper> mImageFetcher; + mozilla::MozPromiseRequestHolder<mozilla::dom::ImagePromise> + mImageFetchRequest; + + nsString mFetchingUrl; + nsString mCurrentImageUrl; + + size_t mNextImageIndex = 0; + + // Load the image at index aIndex of the metadta's artwork to MPRIS + // asynchronously + void LoadImageAtIndex(const size_t aIndex); + bool SetImageToDisplay(const char* aImageData, uint32_t aDataSize); + + bool RenewLocalImageFile(const char* aImageData, uint32_t aDataSize); + bool InitLocalImageFile(); + bool InitLocalImageFolder(); + void RemoveAllLocalImages(); + bool LocalImageFolderExists(); + + // Queries nsAppInfo to get the branded browser name and vendor + void InitIdentity(); + + // non-public API, called from events + void OnNameAcquired(GDBusConnection* aConnection, const gchar* aName); + void OnNameLost(GDBusConnection* aConnection, const gchar* aName); + void OnBusAcquired(GDBusConnection* aConnection, const gchar* aName); + + static void OnNameAcquiredStatic(GDBusConnection* aConnection, + const gchar* aName, gpointer aUserData); + static void OnNameLostStatic(GDBusConnection* aConnection, const gchar* aName, + gpointer aUserData); + static void OnBusAcquiredStatic(GDBusConnection* aConnection, + const gchar* aName, gpointer aUserData); + + void EmitEvent(dom::MediaControlKey aKey) const; + + bool EmitMetadataChanged() const; + + void SetMediaMetadataInternal(const dom::MediaMetadataBase& aMetadata, + bool aClearArtUrl = true); + + bool EmitSupportedKeyChanged(mozilla::dom::MediaControlKey aKey, + bool aSupported) const; + + bool EmitPropertiesChangedSignal(GVariant* aParameters) const; + + void ClearMetadata(); +}; + +} // namespace widget +} // namespace mozilla + +#endif // WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_ diff --git a/widget/gtk/MediaKeysEventSourceFactory.cpp b/widget/gtk/MediaKeysEventSourceFactory.cpp new file mode 100644 index 0000000000..b9a3dfde41 --- /dev/null +++ b/widget/gtk/MediaKeysEventSourceFactory.cpp @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MediaKeysEventSourceFactory.h" +#include "MPRISServiceHandler.h" + +namespace mozilla::widget { + +mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() { + return new MPRISServiceHandler(); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/MozContainer.cpp b/widget/gtk/MozContainer.cpp new file mode 100644 index 0000000000..334592eae1 --- /dev/null +++ b/widget/gtk/MozContainer.cpp @@ -0,0 +1,376 @@ +/* -*- 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 "MozContainer.h" + +#include <glib.h> +#include <gtk/gtk.h> +#include <gdk/gdkx.h> +#include <stdio.h> +#ifdef MOZ_WAYLAND +# include "gfxPlatformGtk.h" +#endif + +#ifdef ACCESSIBILITY +# include <atk/atk.h> +# include "maiRedundantObjectFactory.h" +#endif + +#undef LOG +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetLog; +# define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args) +#else +# define LOG(args) +#endif /* MOZ_LOGGING */ + +/* init methods */ +static void moz_container_class_init(MozContainerClass* klass); +static void moz_container_init(MozContainer* container); + +/* widget class methods */ +static void moz_container_map(GtkWidget* widget); +static void moz_container_unmap(GtkWidget* widget); +static void moz_container_size_allocate(GtkWidget* widget, + GtkAllocation* allocation); +void moz_container_realize(GtkWidget* widget); + +/* container class methods */ +static void moz_container_remove(GtkContainer* container, + GtkWidget* child_widget); +static void moz_container_forall(GtkContainer* container, + gboolean include_internals, + GtkCallback callback, gpointer callback_data); +static void moz_container_add(GtkContainer* container, GtkWidget* widget); + +typedef struct _MozContainerChild MozContainerChild; + +struct _MozContainerChild { + GtkWidget* widget; + gint x; + gint y; +}; + +static void moz_container_allocate_child(MozContainer* container, + MozContainerChild* child); +static MozContainerChild* moz_container_get_child(MozContainer* container, + GtkWidget* child); + +/* public methods */ + +GType moz_container_get_type(void) { + static GType moz_container_type = 0; + + if (!moz_container_type) { + static GTypeInfo moz_container_info = { + sizeof(MozContainerClass), /* class_size */ + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc)moz_container_class_init, /* class_init */ + NULL, /* class_destroy */ + NULL, /* class_data */ + sizeof(MozContainer), /* instance_size */ + 0, /* n_preallocs */ + (GInstanceInitFunc)moz_container_init, /* instance_init */ + NULL, /* value_table */ + }; + +#ifdef MOZ_WAYLAND + if (gfxPlatformGtk::GetPlatform()->IsWaylandDisplay()) { + moz_container_info.class_init = + (GClassInitFunc)moz_container_wayland_class_init; + } +#endif + + moz_container_type = + g_type_register_static(GTK_TYPE_CONTAINER, "MozContainer", + &moz_container_info, static_cast<GTypeFlags>(0)); +#ifdef ACCESSIBILITY + /* Set a factory to return accessible object with ROLE_REDUNDANT for + * MozContainer, so that gail won't send focus notification for it */ + atk_registry_set_factory_type(atk_get_default_registry(), + moz_container_type, + mai_redundant_object_factory_get_type()); +#endif + } + + return moz_container_type; +} + +GtkWidget* moz_container_new(void) { + MozContainer* container; + + container = + static_cast<MozContainer*>(g_object_new(MOZ_CONTAINER_TYPE, nullptr)); + + return GTK_WIDGET(container); +} + +void moz_container_put(MozContainer* container, GtkWidget* child_widget, gint x, + gint y) { + MozContainerChild* child; + + child = g_new(MozContainerChild, 1); + + child->widget = child_widget; + child->x = x; + child->y = y; + + /* printf("moz_container_put %p %p %d %d\n", (void *)container, + (void *)child_widget, x, y); */ + + container->children = g_list_append(container->children, child); + + /* we assume that the caller of this function will have already set + the parent GdkWindow because we can have many anonymous children. */ + gtk_widget_set_parent(child_widget, GTK_WIDGET(container)); +} + +void moz_container_class_init(MozContainerClass* klass) { + /*GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkObjectClass *object_class = GTK_OBJECT_CLASS (klass); */ + GtkContainerClass* container_class = GTK_CONTAINER_CLASS(klass); + GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass); + + widget_class->map = moz_container_map; + widget_class->unmap = moz_container_unmap; + widget_class->realize = moz_container_realize; + widget_class->size_allocate = moz_container_size_allocate; + + container_class->remove = moz_container_remove; + container_class->forall = moz_container_forall; + container_class->add = moz_container_add; +} + +void moz_container_init(MozContainer* container) { + gtk_widget_set_can_focus(GTK_WIDGET(container), TRUE); + gtk_container_set_resize_mode(GTK_CONTAINER(container), GTK_RESIZE_IMMEDIATE); + gtk_widget_set_redraw_on_allocate(GTK_WIDGET(container), FALSE); +#ifdef MOZ_WAYLAND + if (gfxPlatformGtk::GetPlatform()->IsWaylandDisplay()) { + moz_container_wayland_init(&container->wl_container); + } +#endif + LOG(("%s [%p]\n", __FUNCTION__, (void*)container)); +} + +void moz_container_map(GtkWidget* widget) { + MozContainer* container; + GList* tmp_list; + GtkWidget* tmp_child; + + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + container = MOZ_CONTAINER(widget); + + gtk_widget_set_mapped(widget, TRUE); + + tmp_list = container->children; + while (tmp_list) { + tmp_child = ((MozContainerChild*)tmp_list->data)->widget; + + if (gtk_widget_get_visible(tmp_child)) { + if (!gtk_widget_get_mapped(tmp_child)) gtk_widget_map(tmp_child); + } + tmp_list = tmp_list->next; + } + + if (gtk_widget_get_has_window(widget)) { + gdk_window_show(gtk_widget_get_window(widget)); + } +} + +void moz_container_unmap(GtkWidget* widget) { + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + + gtk_widget_set_mapped(widget, FALSE); + + if (gtk_widget_get_has_window(widget)) { + gdk_window_hide(gtk_widget_get_window(widget)); + } +} + +void moz_container_realize(GtkWidget* widget) { + GdkWindow* parent = gtk_widget_get_parent_window(widget); + GdkWindow* window; + + gtk_widget_set_realized(widget, TRUE); + + if (gtk_widget_get_has_window(widget)) { + GdkWindowAttr attributes; + gint attributes_mask = GDK_WA_VISUAL | GDK_WA_X | GDK_WA_Y; + GtkAllocation allocation; + + gtk_widget_get_allocation(widget, &allocation); + attributes.event_mask = gtk_widget_get_events(widget); + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.window_type = GDK_WINDOW_CHILD; + MozContainer* container = MOZ_CONTAINER(widget); + attributes.visual = + container->force_default_visual + ? gdk_screen_get_system_visual(gtk_widget_get_screen(widget)) + : gtk_widget_get_visual(widget); + + window = gdk_window_new(parent, &attributes, attributes_mask); + + LOG(("moz_container_realize() [%p] GdkWindow %p\n", (void*)container, + (void*)window)); + + gdk_window_set_user_data(window, widget); + } else { + window = parent; + g_object_ref(window); + } + + gtk_widget_set_window(widget, window); +} + +void moz_container_size_allocate(GtkWidget* widget, GtkAllocation* allocation) { + MozContainer* container; + GList* tmp_list; + GtkAllocation tmp_allocation; + + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + + LOG(("moz_container_size_allocate [%p] %d,%d -> %d x %d\n", (void*)widget, + allocation->x, allocation->y, allocation->width, allocation->height)); + + /* short circuit if you can */ + container = MOZ_CONTAINER(widget); + gtk_widget_get_allocation(widget, &tmp_allocation); + if (!container->children && tmp_allocation.x == allocation->x && + tmp_allocation.y == allocation->y && + tmp_allocation.width == allocation->width && + tmp_allocation.height == allocation->height) { + return; + } + + gtk_widget_set_allocation(widget, allocation); + + tmp_list = container->children; + + while (tmp_list) { + MozContainerChild* child = static_cast<MozContainerChild*>(tmp_list->data); + + moz_container_allocate_child(container, child); + + tmp_list = tmp_list->next; + } + + if (gtk_widget_get_has_window(widget) && gtk_widget_get_realized(widget)) { + gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x, + allocation->y, allocation->width, + allocation->height); + } +} + +void moz_container_remove(GtkContainer* container, GtkWidget* child_widget) { + MozContainerChild* child; + MozContainer* moz_container; + GdkWindow* parent_window; + + g_return_if_fail(IS_MOZ_CONTAINER(container)); + g_return_if_fail(GTK_IS_WIDGET(child_widget)); + + moz_container = MOZ_CONTAINER(container); + + child = moz_container_get_child(moz_container, child_widget); + g_return_if_fail(child); + + /* gtk_widget_unparent will remove the parent window (as well as the + * parent widget), but, in Mozilla's window hierarchy, the parent window + * may need to be kept because it may be part of a GdkWindow sub-hierarchy + * that is being moved to another MozContainer. + * + * (In a conventional GtkWidget hierarchy, GdkWindows being reparented + * would have their own GtkWidget and that widget would be the one being + * reparented. In Mozilla's hierarchy, the parent_window needs to be + * retained so that the GdkWindow sub-hierarchy is maintained.) + */ + parent_window = gtk_widget_get_parent_window(child_widget); + if (parent_window) g_object_ref(parent_window); + + gtk_widget_unparent(child_widget); + + if (parent_window) { + /* The child_widget will always still exist because g_signal_emit, + * which invokes this function, holds a reference. + * + * If parent_window is the container's root window then it will not be + * the parent_window if the child_widget is placed in another + * container. + */ + if (parent_window != gtk_widget_get_window(GTK_WIDGET(container))) + gtk_widget_set_parent_window(child_widget, parent_window); + + g_object_unref(parent_window); + } + + moz_container->children = g_list_remove(moz_container->children, child); + g_free(child); +} + +void moz_container_forall(GtkContainer* container, gboolean include_internals, + GtkCallback callback, gpointer callback_data) { + MozContainer* moz_container; + GList* tmp_list; + + g_return_if_fail(IS_MOZ_CONTAINER(container)); + g_return_if_fail(callback != NULL); + + moz_container = MOZ_CONTAINER(container); + + tmp_list = moz_container->children; + while (tmp_list) { + MozContainerChild* child; + child = static_cast<MozContainerChild*>(tmp_list->data); + tmp_list = tmp_list->next; + (*callback)(child->widget, callback_data); + } +} + +static void moz_container_allocate_child(MozContainer* container, + MozContainerChild* child) { + GtkAllocation allocation; + + gtk_widget_get_allocation(child->widget, &allocation); + allocation.x = child->x; + allocation.y = child->y; + + gtk_widget_size_allocate(child->widget, &allocation); +} + +MozContainerChild* moz_container_get_child(MozContainer* container, + GtkWidget* child_widget) { + GList* tmp_list; + + tmp_list = container->children; + while (tmp_list) { + MozContainerChild* child; + + child = static_cast<MozContainerChild*>(tmp_list->data); + tmp_list = tmp_list->next; + + if (child->widget == child_widget) return child; + } + + return NULL; +} + +static void moz_container_add(GtkContainer* container, GtkWidget* widget) { + moz_container_put(MOZ_CONTAINER(container), widget, 0, 0); +} + +void moz_container_force_default_visual(MozContainer* container) { + container->force_default_visual = true; +} diff --git a/widget/gtk/MozContainer.h b/widget/gtk/MozContainer.h new file mode 100644 index 0000000000..9faf909179 --- /dev/null +++ b/widget/gtk/MozContainer.h @@ -0,0 +1,90 @@ +/* -*- 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_CONTAINER_H__ +#define __MOZ_CONTAINER_H__ + +#ifdef MOZ_WAYLAND +# include "MozContainerWayland.h" +#endif + +#include <gtk/gtk.h> +#include <functional> + +/* + * MozContainer + * + * This class serves three purposes in the nsIWidget implementation. + * + * - It provides objects to receive signals from GTK for events on native + * windows. + * + * - It provides GdkWindow to draw content on Wayland or when Gtk+ renders + * client side decorations to mShell. + * + * - It provides a container parent for GtkWidgets. The only GtkWidgets + * that need this in Mozilla are the GtkSockets for windowed plugins (Xt + * and XEmbed). + * + * Note that the window hierarchy in Mozilla differs from conventional + * GtkWidget hierarchies. + * + * Mozilla's hierarchy exists through the GdkWindow hierarchy, and all child + * GdkWindows (within a child nsIWidget hierarchy) belong to one MozContainer + * GtkWidget. If the MozContainer is unrealized or its GdkWindows are + * destroyed for some other reason, then the hierarchy no longer exists. (In + * conventional GTK clients, the hierarchy is recorded by the GtkWidgets, and + * so can be re-established after destruction of the GdkWindows.) + * + * One consequence of this is that the MozContainer does not know which of its + * GdkWindows should parent child GtkWidgets. (Conventional GtkContainers + * determine which GdkWindow to assign child GtkWidgets.) + * + * Therefore, when adding a child GtkWidget to a MozContainer, + * gtk_widget_set_parent_window should be called on the child GtkWidget before + * it is realized. + */ + +#define MOZ_CONTAINER_TYPE (moz_container_get_type()) +#define MOZ_CONTAINER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), MOZ_CONTAINER_TYPE, MozContainer)) +#define MOZ_CONTAINER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), MOZ_CONTAINER_TYPE, MozContainerClass)) +#define IS_MOZ_CONTAINER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), MOZ_CONTAINER_TYPE)) +#define IS_MOZ_CONTAINER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), MOZ_CONTAINER_TYPE)) +#define MOZ_CONTAINER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), MOZ_CONTAINER_TYPE, MozContainerClass)) + +// We need to shape only a few pixels of the titlebar as we care about +// the corners only +#define TITLEBAR_SHAPE_MASK_HEIGHT 10 + +typedef struct _MozContainer MozContainer; +typedef struct _MozContainerClass MozContainerClass; + +struct _MozContainer { + GtkContainer container; + GList* children; + gboolean force_default_visual; +#ifdef MOZ_WAYLAND + MozContainerWayland wl_container; +#endif +}; + +struct _MozContainerClass { + GtkContainerClass parent_class; +}; + +GType moz_container_get_type(void); +GtkWidget* moz_container_new(void); +void moz_container_put(MozContainer* container, GtkWidget* child_widget, gint x, + gint y); +void moz_container_force_default_visual(MozContainer* container); + +#endif /* __MOZ_CONTAINER_H__ */ diff --git a/widget/gtk/MozContainerWayland.cpp b/widget/gtk/MozContainerWayland.cpp new file mode 100644 index 0000000000..88fbef7277 --- /dev/null +++ b/widget/gtk/MozContainerWayland.cpp @@ -0,0 +1,514 @@ +/* -*- 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 "MozContainer.h" + +#include <glib.h> +#include <gtk/gtk.h> +#include <gdk/gdkx.h> +#include "nsWaylandDisplay.h" +#include "gfxPlatformGtk.h" +#include <wayland-egl.h> +#include <stdio.h> +#include <dlfcn.h> + +#undef LOG +#ifdef MOZ_LOGGING + +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +# include "nsWindow.h" +extern mozilla::LazyLogModule gWidgetWaylandLog; +# define LOGWAYLAND(args) \ + MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, args) +#else +# define LOGWAYLAND(args) +#endif /* MOZ_LOGGING */ + +using namespace mozilla; +using namespace mozilla::widget; + +/* init methods */ +static void moz_container_wayland_destroy(GtkWidget* widget); + +/* widget class methods */ +static void moz_container_wayland_map(GtkWidget* widget); +static gboolean moz_container_wayland_map_event(GtkWidget* widget, + GdkEventAny* event); +static void moz_container_wayland_unmap(GtkWidget* widget); +static void moz_container_wayland_size_allocate(GtkWidget* widget, + GtkAllocation* allocation); + +// Imlemented in MozContainer.cpp +void moz_container_realize(GtkWidget* widget); + +static void moz_container_wayland_move_locked(MozContainer* container, int dx, + int dy) { + LOGWAYLAND(("moz_container_wayland_move_locked [%p] %d,%d\n", + (void*)container, dx, dy)); + + MozContainerWayland* wl_container = &container->wl_container; + + wl_container->subsurface_dx = dx; + wl_container->subsurface_dy = dy; + wl_container->surface_position_needs_update = true; + + // Wayland subsurface is not created yet. + if (!wl_container->subsurface) { + return; + } + + // wl_subsurface_set_position is actually property of parent surface + // which is effective when parent surface is commited. + wl_subsurface_set_position(wl_container->subsurface, + wl_container->subsurface_dx, + wl_container->subsurface_dy); + wl_container->surface_position_needs_update = false; + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + if (window) { + GdkRectangle rect = (GdkRectangle){0, 0, gdk_window_get_width(window), + gdk_window_get_height(window)}; + gdk_window_invalidate_rect(window, &rect, false); + } +} + +static void moz_container_wayland_move(MozContainer* container, int dx, + int dy) { + MutexAutoLock lock(*container->wl_container.container_lock); + LOGWAYLAND( + ("moz_container_wayland_move [%p] %d,%d\n", (void*)container, dx, dy)); + moz_container_wayland_move_locked(container, dx, dy); +} + +// This is called from layout/compositor code only with +// size equal to GL rendering context. Otherwise there are +// rendering artifacts as wl_egl_window size does not match +// GL rendering pipeline setup. +void moz_container_wayland_egl_window_set_size(MozContainer* container, + int width, int height) { + MozContainerWayland* wl_container = &container->wl_container; + MutexAutoLock lock(*wl_container->container_lock); + if (wl_container->eglwindow) { + wl_egl_window_resize(wl_container->eglwindow, width, height, 0, 0); + } +} + +void moz_container_wayland_class_init(MozContainerClass* klass) { + /*GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkObjectClass *object_class = GTK_OBJECT_CLASS (klass); */ + GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass); + + widget_class->map = moz_container_wayland_map; + widget_class->map_event = moz_container_wayland_map_event; + widget_class->destroy = moz_container_wayland_destroy; + widget_class->unmap = moz_container_wayland_unmap; + widget_class->realize = moz_container_realize; + widget_class->size_allocate = moz_container_wayland_size_allocate; +} + +void moz_container_wayland_init(MozContainerWayland* container) { + container->surface = nullptr; + container->subsurface = nullptr; + container->eglwindow = nullptr; + container->frame_callback_handler = nullptr; + container->frame_callback_handler_surface_id = -1; + container->ready_to_draw = false; + container->opaque_region_needs_update = false; + container->opaque_region_subtract_corners = false; + container->surface_needs_clear = true; + container->subsurface_dx = 0; + container->subsurface_dy = 0; + container->surface_position_needs_update = 0; + container->initial_draw_cbs.clear(); + container->container_lock = new mozilla::Mutex("MozContainer lock"); +} + +static void moz_container_wayland_destroy(GtkWidget* widget) { + MozContainerWayland* container = &MOZ_CONTAINER(widget)->wl_container; + delete container->container_lock; + container->container_lock = nullptr; +} + +void moz_container_wayland_add_initial_draw_callback( + MozContainer* container, const std::function<void(void)>& initial_draw_cb) { + container->wl_container.initial_draw_cbs.push_back(initial_draw_cb); +} + +wl_surface* moz_gtk_widget_get_wl_surface(GtkWidget* aWidget) { + GdkWindow* window = gtk_widget_get_window(aWidget); + wl_surface* surface = gdk_wayland_window_get_wl_surface(window); + + LOGWAYLAND(("moz_gtk_widget_get_wl_surface [%p] wl_surface %p ID %d\n", + (void*)aWidget, (void*)surface, + surface ? wl_proxy_get_id((struct wl_proxy*)surface) : -1)); + + return surface; +} + +static void moz_container_wayland_frame_callback_handler( + void* data, struct wl_callback* callback, uint32_t time) { + MozContainerWayland* wl_container = &MOZ_CONTAINER(data)->wl_container; + + LOGWAYLAND( + ("%s [%p] frame_callback_handler %p ready_to_draw %d (set to true)" + " initial_draw callback %zd\n", + __FUNCTION__, (void*)MOZ_CONTAINER(data), + (void*)wl_container->frame_callback_handler, wl_container->ready_to_draw, + wl_container->initial_draw_cbs.size())); + + g_clear_pointer(&wl_container->frame_callback_handler, wl_callback_destroy); + wl_container->frame_callback_handler_surface_id = -1; + + if (!wl_container->ready_to_draw) { + wl_container->ready_to_draw = true; + for (auto const& cb : wl_container->initial_draw_cbs) { + cb(); + } + wl_container->initial_draw_cbs.clear(); + } +} + +static const struct wl_callback_listener moz_container_frame_listener = { + moz_container_wayland_frame_callback_handler}; + +static void moz_container_wayland_request_parent_frame_callback( + MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + + wl_surface* gtk_container_surface = + moz_gtk_widget_get_wl_surface(GTK_WIDGET(container)); + int gtk_container_surface_id = + gtk_container_surface + ? wl_proxy_get_id((struct wl_proxy*)gtk_container_surface) + : -1; + + LOGWAYLAND( + ("%s [%p] frame_callback_handler %p " + "frame_callback_handler_surface_id %d\n", + __FUNCTION__, (void*)container, wl_container->frame_callback_handler, + wl_container->frame_callback_handler_surface_id)); + + if (wl_container->frame_callback_handler && + wl_container->frame_callback_handler_surface_id == + gtk_container_surface_id) { + return; + } + + // If there's pending frame callback, delete it. + if (wl_container->frame_callback_handler) { + g_clear_pointer(&wl_container->frame_callback_handler, wl_callback_destroy); + } + + if (gtk_container_surface) { + wl_container->frame_callback_handler_surface_id = gtk_container_surface_id; + wl_container->frame_callback_handler = + wl_surface_frame(gtk_container_surface); + wl_callback_add_listener(wl_container->frame_callback_handler, + &moz_container_frame_listener, container); + } else { + wl_container->frame_callback_handler_surface_id = -1; + } +} + +static gboolean moz_container_wayland_map_event(GtkWidget* widget, + GdkEventAny* event) { + MozContainerWayland* wl_container = &MOZ_CONTAINER(widget)->wl_container; + + LOGWAYLAND(("%s begin [%p] ready_to_draw %d\n", __FUNCTION__, + (void*)MOZ_CONTAINER(widget), wl_container->ready_to_draw)); + + if (wl_container->ready_to_draw) { + return FALSE; + } + + moz_container_wayland_request_parent_frame_callback(MOZ_CONTAINER(widget)); + return FALSE; +} + +static void moz_container_wayland_unmap_internal(MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + MutexAutoLock lock(*wl_container->container_lock); + + g_clear_pointer(&wl_container->eglwindow, wl_egl_window_destroy); + g_clear_pointer(&wl_container->subsurface, wl_subsurface_destroy); + g_clear_pointer(&wl_container->surface, wl_surface_destroy); + g_clear_pointer(&wl_container->frame_callback_handler, wl_callback_destroy); + wl_container->frame_callback_handler_surface_id = -1; + + wl_container->surface_needs_clear = true; + wl_container->ready_to_draw = false; + + LOGWAYLAND(("%s [%p]\n", __FUNCTION__, (void*)container)); +} + +void moz_container_wayland_map(GtkWidget* widget) { + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + gtk_widget_set_mapped(widget, TRUE); + + if (gtk_widget_get_has_window(widget)) { + gdk_window_show(gtk_widget_get_window(widget)); + moz_container_wayland_map_event(widget, nullptr); + } +} + +void moz_container_wayland_unmap(GtkWidget* widget) { + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + + gtk_widget_set_mapped(widget, FALSE); + + if (gtk_widget_get_has_window(widget)) { + gdk_window_hide(gtk_widget_get_window(widget)); + moz_container_wayland_unmap_internal(MOZ_CONTAINER(widget)); + } +} + +void moz_container_wayland_size_allocate(GtkWidget* widget, + GtkAllocation* allocation) { + MozContainer* container; + GtkAllocation tmp_allocation; + + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + + LOGWAYLAND(("moz_container_wayland_size_allocate [%p] %d,%d -> %d x %d\n", + (void*)widget, allocation->x, allocation->y, allocation->width, + allocation->height)); + + /* short circuit if you can */ + container = MOZ_CONTAINER(widget); + gtk_widget_get_allocation(widget, &tmp_allocation); + if (!container->children && tmp_allocation.x == allocation->x && + tmp_allocation.y == allocation->y && + tmp_allocation.width == allocation->width && + tmp_allocation.height == allocation->height) { + return; + } + + gtk_widget_set_allocation(widget, allocation); + + if (gtk_widget_get_has_window(widget) && gtk_widget_get_realized(widget)) { + gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x, + allocation->y, allocation->width, + allocation->height); + // We need to position our subsurface according to GdkWindow + // when offset changes (GdkWindow is maximized for instance). + // see gtk-clutter-embed.c for reference. + if (gfxPlatformGtk::GetPlatform()->IsWaylandDisplay()) { + moz_container_wayland_move(MOZ_CONTAINER(widget), allocation->x, + allocation->y); + } + } +} + +static wl_region* moz_container_wayland_create_opaque_region( + int aX, int aY, int aWidth, int aHeight, bool aSubtractCorners) { + struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor(); + wl_region* region = wl_compositor_create_region(compositor); + wl_region_add(region, aX, aY, aWidth, aHeight); + if (aSubtractCorners) { + wl_region_subtract(region, aX, aY, TITLEBAR_SHAPE_MASK_HEIGHT, + TITLEBAR_SHAPE_MASK_HEIGHT); + wl_region_subtract(region, aX + aWidth - TITLEBAR_SHAPE_MASK_HEIGHT, aY, + TITLEBAR_SHAPE_MASK_HEIGHT, TITLEBAR_SHAPE_MASK_HEIGHT); + } + return region; +} + +static void moz_container_wayland_set_opaque_region_locked( + MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + + if (!wl_container->opaque_region_needs_update || !wl_container->surface) { + return; + } + + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(container), &allocation); + + wl_region* region = moz_container_wayland_create_opaque_region( + 0, 0, allocation.width, allocation.height, + wl_container->opaque_region_subtract_corners); + wl_surface_set_opaque_region(wl_container->surface, region); + wl_region_destroy(region); + + wl_container->opaque_region_needs_update = false; +} + +static void moz_container_wayland_set_opaque_region(MozContainer* container) { + MutexAutoLock lock(*container->wl_container.container_lock); + moz_container_wayland_set_opaque_region_locked(container); +} + +static void moz_container_wayland_set_scale_factor_locked( + MozContainer* container) { + if (!container->wl_container.surface) { + return; + } + gpointer user_data = g_object_get_data(G_OBJECT(container), "nsWindow"); + nsWindow* wnd = static_cast<nsWindow*>(user_data); + + int scale = 1; + if (wnd) { + scale = wnd->GdkScaleFactor(); + } + wl_surface_set_buffer_scale(container->wl_container.surface, scale); +} + +void moz_container_wayland_set_scale_factor(MozContainer* container) { + MutexAutoLock lock(*container->wl_container.container_lock); + moz_container_wayland_set_scale_factor_locked(container); +} + +static struct wl_surface* moz_container_wayland_get_surface_locked( + MozContainer* container, nsWaylandDisplay* aWaylandDisplay) { + MozContainerWayland* wl_container = &container->wl_container; + + LOGWAYLAND(("%s [%p] surface %p ready_to_draw %d\n", __FUNCTION__, + (void*)container, (void*)wl_container->surface, + wl_container->ready_to_draw)); + + if (!wl_container->surface) { + if (!wl_container->ready_to_draw) { + moz_container_wayland_request_parent_frame_callback(container); + return nullptr; + } + wl_surface* parent_surface = + moz_gtk_widget_get_wl_surface(GTK_WIDGET(container)); + if (!parent_surface) { + return nullptr; + } + + // Available as of GTK 3.8+ + struct wl_compositor* compositor = aWaylandDisplay->GetCompositor(); + wl_container->surface = wl_compositor_create_surface(compositor); + if (!wl_container->surface) { + return nullptr; + } + + wl_container->subsurface = + wl_subcompositor_get_subsurface(aWaylandDisplay->GetSubcompositor(), + wl_container->surface, parent_surface); + if (!wl_container->subsurface) { + g_clear_pointer(&wl_container->surface, wl_surface_destroy); + return nullptr; + } + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + gint x, y; + gdk_window_get_position(window, &x, &y); + moz_container_wayland_move_locked(container, x, y); + wl_subsurface_set_desync(wl_container->subsurface); + + // Route input to parent wl_surface owned by Gtk+ so we get input + // events from Gtk+. + wl_region* region = wl_compositor_create_region(compositor); + wl_surface_set_input_region(wl_container->surface, region); + wl_region_destroy(region); + + wl_surface_commit(wl_container->surface); + wl_display_flush(aWaylandDisplay->GetDisplay()); + + LOGWAYLAND(("%s [%p] created surface %p\n", __FUNCTION__, (void*)container, + (void*)wl_container->surface)); + } + + if (wl_container->surface_position_needs_update) { + moz_container_wayland_move_locked(container, wl_container->subsurface_dx, + wl_container->subsurface_dy); + } + + moz_container_wayland_set_opaque_region_locked(container); + moz_container_wayland_set_scale_factor_locked(container); + + return wl_container->surface; +} + +struct wl_surface* moz_container_wayland_surface_lock(MozContainer* container) { + GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(container)); + RefPtr<nsWaylandDisplay> waylandDisplay = WaylandDisplayGet(display); + + LOGWAYLAND(("%s [%p] surface %p\n", __FUNCTION__, (void*)container, + (void*)container->wl_container.surface)); + + container->wl_container.container_lock->Lock(); + struct wl_surface* surface = + moz_container_wayland_get_surface_locked(container, waylandDisplay); + if (surface == nullptr) { + container->wl_container.container_lock->Unlock(); + } + return surface; +} + +void moz_container_wayland_surface_unlock(MozContainer* container, + struct wl_surface** surface) { + LOGWAYLAND(("%s [%p] surface %p\n", __FUNCTION__, (void*)container, + (void*)container->wl_container.surface)); + if (*surface) { + container->wl_container.container_lock->Unlock(); + *surface = nullptr; + } +} + +struct wl_egl_window* moz_container_wayland_get_egl_window( + MozContainer* container, int scale) { + GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(container)); + RefPtr<nsWaylandDisplay> waylandDisplay = WaylandDisplayGet(display); + MozContainerWayland* wl_container = &container->wl_container; + + LOGWAYLAND(("%s [%p] eglwindow %p\n", __FUNCTION__, (void*)container, + (void*)wl_container->eglwindow)); + + MutexAutoLock lock(*wl_container->container_lock); + + // Always call moz_container_get_wl_surface() to ensure underlying + // container->surface has correct scale and position. + wl_surface* surface = + moz_container_wayland_get_surface_locked(container, waylandDisplay); + if (!surface) { + return nullptr; + } + if (!wl_container->eglwindow) { + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + wl_container->eglwindow = + wl_egl_window_create(surface, gdk_window_get_width(window) * scale, + gdk_window_get_height(window) * scale); + + LOGWAYLAND(("%s [%p] created eglwindow %p\n", __FUNCTION__, + (void*)container, (void*)wl_container->eglwindow)); + } + + return wl_container->eglwindow; +} + +gboolean moz_container_wayland_has_egl_window(MozContainer* container) { + return container->wl_container.eglwindow ? true : false; +} + +gboolean moz_container_wayland_surface_needs_clear(MozContainer* container) { + int ret = container->wl_container.surface_needs_clear; + container->wl_container.surface_needs_clear = false; + return ret; +} + +void moz_container_wayland_update_opaque_region(MozContainer* container, + bool aSubtractCorners) { + MozContainerWayland* wl_container = &container->wl_container; + wl_container->opaque_region_needs_update = true; + wl_container->opaque_region_subtract_corners = aSubtractCorners; + + // When GL compositor / WebRender is used, + // moz_container_wayland_get_egl_window() is called only once when window + // is created or resized so update opaque region now. + if (moz_container_wayland_has_egl_window(container)) { + moz_container_wayland_set_opaque_region(container); + } +} + +gboolean moz_container_wayland_can_draw(MozContainer* container) { + return container ? container->wl_container.ready_to_draw : false; +} diff --git a/widget/gtk/MozContainerWayland.h b/widget/gtk/MozContainerWayland.h new file mode 100644 index 0000000000..6decd5646c --- /dev/null +++ b/widget/gtk/MozContainerWayland.h @@ -0,0 +1,83 @@ +/* -*- 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_CONTAINER_WAYLAND_H__ +#define __MOZ_CONTAINER_WAYLAND_H__ + +#include <gtk/gtk.h> +#include <functional> +#include <vector> +#include "mozilla/Mutex.h" + +/* + * MozContainer + * + * This class serves three purposes in the nsIWidget implementation. + * + * - It provides objects to receive signals from GTK for events on native + * windows. + * + * - It provides GdkWindow to draw content on Wayland or when Gtk+ renders + * client side decorations to mShell. + */ + +/* Workaround for bug at wayland-util.h, + * present in wayland-devel < 1.12 + */ +struct wl_surface; +struct wl_subsurface; + +struct MozContainerWayland { + struct wl_surface* surface; + struct wl_subsurface* subsurface; + int subsurface_dx, subsurface_dy; + struct wl_egl_window* eglwindow; + struct wl_callback* frame_callback_handler; + int frame_callback_handler_surface_id; + gboolean opaque_region_needs_update; + gboolean opaque_region_subtract_corners; + gboolean opaque_region_fullscreen; + gboolean surface_position_needs_update; + gboolean surface_needs_clear; + gboolean ready_to_draw; + std::vector<std::function<void(void)>> initial_draw_cbs; + // mozcontainer is used from Compositor and Rendering threads + // so we need to control access to mozcontainer where wayland internals + // are used directly. + mozilla::Mutex* container_lock; +}; + +struct _MozContainer; +struct _MozContainerClass; +typedef struct _MozContainer MozContainer; +typedef struct _MozContainerClass MozContainerClass; + +void moz_container_wayland_class_init(MozContainerClass* klass); +void moz_container_wayland_init(MozContainerWayland* container); + +struct wl_surface* moz_container_wayland_surface_lock(MozContainer* container); +void moz_container_wayland_surface_unlock(MozContainer* container, + struct wl_surface** surface); + +struct wl_egl_window* moz_container_wayland_get_egl_window( + MozContainer* container, int scale); + +gboolean moz_container_wayland_has_egl_window(MozContainer* container); +gboolean moz_container_wayland_surface_needs_clear(MozContainer* container); +void moz_container_wayland_move_resize(MozContainer* container, int dx, int dy, + int width, int height); +void moz_container_wayland_egl_window_set_size(MozContainer* container, + int width, int height); +void moz_container_wayland_set_scale_factor(MozContainer* container); +void moz_container_wayland_add_initial_draw_callback( + MozContainer* container, const std::function<void(void)>& initial_draw_cb); +wl_surface* moz_gtk_widget_get_wl_surface(GtkWidget* aWidget); +void moz_container_wayland_update_opaque_region(MozContainer* container, + bool aSubtractCorners); +gboolean moz_container_wayland_can_draw(MozContainer* container); + +#endif /* __MOZ_CONTAINER_WAYLAND_H__ */ diff --git a/widget/gtk/NativeKeyBindings.cpp b/widget/gtk/NativeKeyBindings.cpp new file mode 100644 index 0000000000..65843d7768 --- /dev/null +++ b/widget/gtk/NativeKeyBindings.cpp @@ -0,0 +1,346 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/TextEvents.h" + +#include "NativeKeyBindings.h" +#include "nsString.h" +#include "nsMemory.h" +#include "nsGtkKeyUtils.h" + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <gdk/gdk.h> + +namespace mozilla { +namespace widget { + +static nsTArray<CommandInt>* gCurrentCommands = nullptr; +static bool gHandled = false; + +inline void AddCommand(Command aCommand) { + MOZ_ASSERT(gCurrentCommands); + gCurrentCommands->AppendElement(static_cast<CommandInt>(aCommand)); +} + +// Common GtkEntry and GtkTextView signals +static void copy_clipboard_cb(GtkWidget* w, gpointer user_data) { + AddCommand(Command::Copy); + g_signal_stop_emission_by_name(w, "copy_clipboard"); + gHandled = true; +} + +static void cut_clipboard_cb(GtkWidget* w, gpointer user_data) { + AddCommand(Command::Cut); + g_signal_stop_emission_by_name(w, "cut_clipboard"); + gHandled = true; +} + +// GTK distinguishes between display lines (wrapped, as they appear on the +// screen) and paragraphs, which are runs of text terminated by a newline. +// We don't have this distinction, so we always use editor's notion of +// lines, which are newline-terminated. + +static const Command sDeleteCommands[][2] = { + // backward, forward + // CHARS + {Command::DeleteCharBackward, Command::DeleteCharForward}, + // WORD_ENDS + {Command::DeleteWordBackward, Command::DeleteWordForward}, + // WORDS + {Command::DeleteWordBackward, Command::DeleteWordForward}, + // LINES + {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine}, + // LINE_ENDS + {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine}, + // PARAGRAPH_ENDS + {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine}, + // PARAGRAPHS + {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine}, + // This deletes from the end of the previous word to the beginning of the + // next word, but only if the caret is not in a word. + // XXX need to implement in editor + {Command::DoNothing, Command::DoNothing} // WHITESPACE +}; + +static void delete_from_cursor_cb(GtkWidget* w, GtkDeleteType del_type, + gint count, gpointer user_data) { + g_signal_stop_emission_by_name(w, "delete_from_cursor"); + if (count == 0) { + // Nothing to do. + return; + } + + bool forward = count > 0; + + // Ignore GTK's Ctrl-K keybinding introduced in GTK 3.14 and removed in + // 3.18 if the user has custom bindings set. See bug 1176929. + if (del_type == GTK_DELETE_PARAGRAPH_ENDS && forward && GTK_IS_ENTRY(w) && + !gtk_check_version(3, 14, 1) && gtk_check_version(3, 17, 9)) { + GtkStyleContext* context = gtk_widget_get_style_context(w); + GtkStateFlags flags = gtk_widget_get_state_flags(w); + + GPtrArray* array; + gtk_style_context_get(context, flags, "gtk-key-bindings", &array, nullptr); + if (!array) return; + g_ptr_array_unref(array); + } + + gHandled = true; + if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) { + // unsupported deletion type + return; + } + + if (del_type == GTK_DELETE_WORDS) { + // This works like word_ends, except we first move the caret to the + // beginning/end of the current word. + if (forward) { + AddCommand(Command::WordNext); + AddCommand(Command::WordPrevious); + } else { + AddCommand(Command::WordPrevious); + AddCommand(Command::WordNext); + } + } else if (del_type == GTK_DELETE_DISPLAY_LINES || + del_type == GTK_DELETE_PARAGRAPHS) { + // This works like display_line_ends, except we first move the caret to the + // beginning/end of the current line. + if (forward) { + AddCommand(Command::BeginLine); + } else { + AddCommand(Command::EndLine); + } + } + + Command command = sDeleteCommands[del_type][forward]; + if (command == Command::DoNothing) { + return; + } + + unsigned int absCount = Abs(count); + for (unsigned int i = 0; i < absCount; ++i) { + AddCommand(command); + } +} + +static const Command sMoveCommands[][2][2] = { + // non-extend { backward, forward }, extend { backward, forward } + // GTK differentiates between logical position, which is prev/next, + // and visual position, which is always left/right. + // We should fix this to work the same way for RTL text input. + {// LOGICAL_POSITIONS + {Command::CharPrevious, Command::CharNext}, + {Command::SelectCharPrevious, Command::SelectCharNext}}, + {// VISUAL_POSITIONS + {Command::CharPrevious, Command::CharNext}, + {Command::SelectCharPrevious, Command::SelectCharNext}}, + {// WORDS + {Command::WordPrevious, Command::WordNext}, + {Command::SelectWordPrevious, Command::SelectWordNext}}, + {// DISPLAY_LINES + {Command::LinePrevious, Command::LineNext}, + {Command::SelectLinePrevious, Command::SelectLineNext}}, + {// DISPLAY_LINE_ENDS + {Command::BeginLine, Command::EndLine}, + {Command::SelectBeginLine, Command::SelectEndLine}}, + {// PARAGRAPHS + {Command::LinePrevious, Command::LineNext}, + {Command::SelectLinePrevious, Command::SelectLineNext}}, + {// PARAGRAPH_ENDS + {Command::BeginLine, Command::EndLine}, + {Command::SelectBeginLine, Command::SelectEndLine}}, + {// PAGES + {Command::MovePageUp, Command::MovePageDown}, + {Command::SelectPageUp, Command::SelectPageDown}}, + {// BUFFER_ENDS + {Command::MoveTop, Command::MoveBottom}, + {Command::SelectTop, Command::SelectBottom}}, + {// HORIZONTAL_PAGES (unsupported) + {Command::DoNothing, Command::DoNothing}, + {Command::DoNothing, Command::DoNothing}}}; + +static void move_cursor_cb(GtkWidget* w, GtkMovementStep step, gint count, + gboolean extend_selection, gpointer user_data) { + g_signal_stop_emission_by_name(w, "move_cursor"); + if (count == 0) { + // Nothing to do. + return; + } + + gHandled = true; + bool forward = count > 0; + if (uint32_t(step) >= ArrayLength(sMoveCommands)) { + // unsupported movement type + return; + } + + Command command = sMoveCommands[step][extend_selection][forward]; + if (command == Command::DoNothing) { + return; + } + + unsigned int absCount = Abs(count); + for (unsigned int i = 0; i < absCount; ++i) { + AddCommand(command); + } +} + +static void paste_clipboard_cb(GtkWidget* w, gpointer user_data) { + AddCommand(Command::Paste); + g_signal_stop_emission_by_name(w, "paste_clipboard"); + gHandled = true; +} + +// GtkTextView-only signals +static void select_all_cb(GtkWidget* w, gboolean select, gpointer user_data) { + AddCommand(Command::SelectAll); + g_signal_stop_emission_by_name(w, "select_all"); + gHandled = true; +} + +NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr; +NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr; + +// static +NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) { + switch (aType) { + case nsIWidget::NativeKeyBindingsForSingleLineEditor: + if (!sInstanceForSingleLineEditor) { + sInstanceForSingleLineEditor = new NativeKeyBindings(); + sInstanceForSingleLineEditor->Init(aType); + } + return sInstanceForSingleLineEditor; + + default: + // fallback to multiline editor case in release build + MOZ_FALLTHROUGH_ASSERT("aType is invalid or not yet implemented"); + case nsIWidget::NativeKeyBindingsForMultiLineEditor: + case nsIWidget::NativeKeyBindingsForRichTextEditor: + if (!sInstanceForMultiLineEditor) { + sInstanceForMultiLineEditor = new NativeKeyBindings(); + sInstanceForMultiLineEditor->Init(aType); + } + return sInstanceForMultiLineEditor; + } +} + +// static +void NativeKeyBindings::Shutdown() { + delete sInstanceForSingleLineEditor; + sInstanceForSingleLineEditor = nullptr; + delete sInstanceForMultiLineEditor; + sInstanceForMultiLineEditor = nullptr; +} + +void NativeKeyBindings::Init(NativeKeyBindingsType aType) { + switch (aType) { + case nsIWidget::NativeKeyBindingsForSingleLineEditor: + mNativeTarget = gtk_entry_new(); + break; + default: + mNativeTarget = gtk_text_view_new(); + if (gtk_major_version > 2 || + (gtk_major_version == 2 && + (gtk_minor_version > 2 || + (gtk_minor_version == 2 && gtk_micro_version >= 2)))) { + // select_all only exists in gtk >= 2.2.2. Prior to that, + // ctrl+a is bound to (move to beginning, select to end). + g_signal_connect(mNativeTarget, "select_all", G_CALLBACK(select_all_cb), + this); + } + break; + } + + g_object_ref_sink(mNativeTarget); + + g_signal_connect(mNativeTarget, "copy_clipboard", + G_CALLBACK(copy_clipboard_cb), this); + g_signal_connect(mNativeTarget, "cut_clipboard", G_CALLBACK(cut_clipboard_cb), + this); + g_signal_connect(mNativeTarget, "delete_from_cursor", + G_CALLBACK(delete_from_cursor_cb), this); + g_signal_connect(mNativeTarget, "move_cursor", G_CALLBACK(move_cursor_cb), + this); + g_signal_connect(mNativeTarget, "paste_clipboard", + G_CALLBACK(paste_clipboard_cb), this); +} + +NativeKeyBindings::~NativeKeyBindings() { + gtk_widget_destroy(mNativeTarget); + g_object_unref(mNativeTarget); +} + +void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent, + nsTArray<CommandInt>& aCommands) { + // If the native key event is set, it must be synthesized for tests. + // We just ignore such events because this behavior depends on system + // settings. + if (!aEvent.mNativeKeyEvent) { + // It must be synthesized event or dispatched DOM event from chrome. + return; + } + + guint keyval; + + if (aEvent.mCharCode) { + keyval = gdk_unicode_to_keyval(aEvent.mCharCode); + } else { + keyval = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->keyval; + } + + if (GetEditCommandsInternal(aEvent, aCommands, keyval)) { + return; + } + + for (uint32_t i = 0; i < aEvent.mAlternativeCharCodes.Length(); ++i) { + uint32_t ch = aEvent.IsShift() + ? aEvent.mAlternativeCharCodes[i].mShiftedCharCode + : aEvent.mAlternativeCharCodes[i].mUnshiftedCharCode; + if (ch && ch != aEvent.mCharCode) { + keyval = gdk_unicode_to_keyval(ch); + if (GetEditCommandsInternal(aEvent, aCommands, keyval)) { + return; + } + } + } + + /* + gtk_bindings_activate_event is preferable, but it has unresolved bug: + http://bugzilla.gnome.org/show_bug.cgi?id=162726 + The bug was already marked as FIXED. However, somebody reports that the + bug still exists. + Also gtk_bindings_activate may work with some non-shortcuts operations + (todo: check it). See bug 411005 and bug 406407. + + Code, which should be used after fixing GNOME bug 162726: + + gtk_bindings_activate_event(GTK_OBJECT(mNativeTarget), + static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)); + */ +} + +bool NativeKeyBindings::GetEditCommandsInternal( + const WidgetKeyboardEvent& aEvent, nsTArray<CommandInt>& aCommands, + guint aKeyval) { + guint modifiers = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->state; + + gCurrentCommands = &aCommands; + + gHandled = false; + gtk_bindings_activate(G_OBJECT(mNativeTarget), aKeyval, + GdkModifierType(modifiers)); + + gCurrentCommands = nullptr; + + MOZ_ASSERT(!gHandled || !aCommands.IsEmpty()); + + return gHandled; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/NativeKeyBindings.h b/widget/gtk/NativeKeyBindings.h new file mode 100644 index 0000000000..ead3f350d5 --- /dev/null +++ b/widget/gtk/NativeKeyBindings.h @@ -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/. */ + +#ifndef mozilla_widget_NativeKeyBindings_h_ +#define mozilla_widget_NativeKeyBindings_h_ + +#include <gtk/gtk.h> +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsIWidget.h" + +namespace mozilla { +namespace widget { + +class NativeKeyBindings final { + typedef nsIWidget::NativeKeyBindingsType NativeKeyBindingsType; + + public: + static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType); + static void Shutdown(); + + void Init(NativeKeyBindingsType aType); + + void GetEditCommands(const WidgetKeyboardEvent& aEvent, + nsTArray<CommandInt>& aCommands); + + private: + ~NativeKeyBindings(); + + bool GetEditCommandsInternal(const WidgetKeyboardEvent& aEvent, + nsTArray<CommandInt>& aCommands, guint aKeyval); + + GtkWidget* mNativeTarget; + + static NativeKeyBindings* sInstanceForSingleLineEditor; + static NativeKeyBindings* sInstanceForMultiLineEditor; +}; + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_NativeKeyBindings_h_ diff --git a/widget/gtk/PCompositorWidget.ipdl b/widget/gtk/PCompositorWidget.ipdl new file mode 100644 index 0000000000..178fe78e4d --- /dev/null +++ b/widget/gtk/PCompositorWidget.ipdl @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=99: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PCompositorBridge; + +using mozilla::LayoutDeviceIntSize from "Units.h"; + +namespace mozilla { +namespace widget { + +sync protocol PCompositorWidget +{ + manager PCompositorBridge; + +parent: + async __delete__(); + + async NotifyClientSizeChanged(LayoutDeviceIntSize aClientSize); + +child: + + async ObserveVsync(); + async UnobserveVsync(); +}; + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/PlatformWidgetTypes.ipdlh b/widget/gtk/PlatformWidgetTypes.ipdlh new file mode 100644 index 0000000000..2fe3ef148c --- /dev/null +++ b/widget/gtk/PlatformWidgetTypes.ipdlh @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=99: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include HeadlessWidgetTypes; + +using mozilla::LayoutDeviceIntSize from "Units.h"; + +namespace mozilla { +namespace widget { + +struct GtkCompositorWidgetInitData +{ + uintptr_t XWindow; + nsCString XDisplayString; + bool Shaped; + bool IsX11Display; + + LayoutDeviceIntSize InitialClientSize; +}; + +union CompositorWidgetInitData +{ + GtkCompositorWidgetInitData; + HeadlessCompositorWidgetInitData; +}; + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/ScreenHelperGTK.cpp b/widget/gtk/ScreenHelperGTK.cpp new file mode 100644 index 0000000000..67410b25a1 --- /dev/null +++ b/widget/gtk/ScreenHelperGTK.cpp @@ -0,0 +1,195 @@ +/* -*- 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 "ScreenHelperGTK.h" + +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +#endif /* MOZ_X11 */ +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +#endif /* MOZ_WAYLAND */ +#include <dlfcn.h> +#include <gtk/gtk.h> + +#include "gfxPlatformGtk.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/Logging.h" +#include "nsGtkUtils.h" +#include "nsTArray.h" + +namespace mozilla { +namespace widget { + +static LazyLogModule sScreenLog("WidgetScreen"); + +static void monitors_changed(GdkScreen* aScreen, gpointer aClosure) { + MOZ_LOG(sScreenLog, LogLevel::Debug, ("Received monitors-changed event")); + ScreenHelperGTK* self = static_cast<ScreenHelperGTK*>(aClosure); + self->RefreshScreens(); +} + +static void screen_resolution_changed(GdkScreen* aScreen, GParamSpec* aPspec, + ScreenHelperGTK* self) { + self->RefreshScreens(); +} + +static GdkFilterReturn root_window_event_filter(GdkXEvent* aGdkXEvent, + GdkEvent* aGdkEvent, + gpointer aClosure) { +#ifdef MOZ_X11 + ScreenHelperGTK* self = static_cast<ScreenHelperGTK*>(aClosure); + XEvent* xevent = static_cast<XEvent*>(aGdkXEvent); + + switch (xevent->type) { + case PropertyNotify: { + XPropertyEvent* propertyEvent = &xevent->xproperty; + if (propertyEvent->atom == self->NetWorkareaAtom()) { + MOZ_LOG(sScreenLog, LogLevel::Debug, ("Work area size changed")); + self->RefreshScreens(); + } + } break; + default: + break; + } +#endif + + return GDK_FILTER_CONTINUE; +} + +ScreenHelperGTK::ScreenHelperGTK() + : mRootWindow(nullptr) +#ifdef MOZ_X11 + , + mNetWorkareaAtom(0) +#endif +{ + MOZ_LOG(sScreenLog, LogLevel::Debug, ("ScreenHelperGTK created")); + GdkScreen* defaultScreen = gdk_screen_get_default(); + if (!defaultScreen) { + // Sometimes we don't initial X (e.g., xpcshell) + MOZ_LOG(sScreenLog, LogLevel::Debug, + ("defaultScreen is nullptr, running headless")); + return; + } + mRootWindow = gdk_get_default_root_window(); + MOZ_ASSERT(mRootWindow); + + g_object_ref(mRootWindow); + + // GDK_PROPERTY_CHANGE_MASK ==> PropertyChangeMask, for PropertyNotify + gdk_window_set_events(mRootWindow, + GdkEventMask(gdk_window_get_events(mRootWindow) | + GDK_PROPERTY_CHANGE_MASK)); + + g_signal_connect(defaultScreen, "monitors-changed", + G_CALLBACK(monitors_changed), this); + // Use _after to ensure this callback is run after gfxPlatformGtk.cpp's + // handler. + g_signal_connect_after(defaultScreen, "notify::resolution", + G_CALLBACK(screen_resolution_changed), this); +#ifdef MOZ_X11 + gdk_window_add_filter(mRootWindow, root_window_event_filter, this); + if (GDK_IS_X11_DISPLAY(gdk_display_get_default())) { + mNetWorkareaAtom = XInternAtom(GDK_WINDOW_XDISPLAY(mRootWindow), + "_NET_WORKAREA", X11False); + } +#endif + RefreshScreens(); +} + +ScreenHelperGTK::~ScreenHelperGTK() { + if (mRootWindow) { + g_signal_handlers_disconnect_by_data(gdk_screen_get_default(), this); + + gdk_window_remove_filter(mRootWindow, root_window_event_filter, this); + g_object_unref(mRootWindow); + mRootWindow = nullptr; + } +} + +gint ScreenHelperGTK::GetGTKMonitorScaleFactor(gint aMonitorNum) { + // Since GDK 3.10 + static auto sGdkScreenGetMonitorScaleFactorPtr = + (gint(*)(GdkScreen*, gint))dlsym(RTLD_DEFAULT, + "gdk_screen_get_monitor_scale_factor"); + if (sGdkScreenGetMonitorScaleFactorPtr) { + GdkScreen* screen = gdk_screen_get_default(); + return sGdkScreenGetMonitorScaleFactorPtr(screen, aMonitorNum); + } + return 1; +} + +static uint32_t GetGTKPixelDepth() { + GdkVisual* visual = gdk_screen_get_system_visual(gdk_screen_get_default()); + return gdk_visual_get_depth(visual); +} + +static already_AddRefed<Screen> MakeScreen(GdkScreen* aScreen, + gint aMonitorNum) { + GdkRectangle monitor; + GdkRectangle workarea; + gdk_screen_get_monitor_geometry(aScreen, aMonitorNum, &monitor); + gdk_screen_get_monitor_workarea(aScreen, aMonitorNum, &workarea); + gint gdkScaleFactor = ScreenHelperGTK::GetGTKMonitorScaleFactor(aMonitorNum); + + // gdk_screen_get_monitor_geometry / workarea returns application pixels + // (desktop pixels), so we need to convert it to device pixels with + // gdkScaleFactor. + LayoutDeviceIntRect rect( + monitor.x * gdkScaleFactor, monitor.y * gdkScaleFactor, + monitor.width * gdkScaleFactor, monitor.height * gdkScaleFactor); + LayoutDeviceIntRect availRect( + workarea.x * gdkScaleFactor, workarea.y * gdkScaleFactor, + workarea.width * gdkScaleFactor, workarea.height * gdkScaleFactor); + uint32_t pixelDepth = GetGTKPixelDepth(); + + // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise. + DesktopToLayoutDeviceScale contentsScale(1.0); +#ifdef MOZ_WAYLAND + GdkDisplay* gdkDisplay = gdk_display_get_default(); + if (!GDK_IS_X11_DISPLAY(gdkDisplay)) { + contentsScale.scale = gdkScaleFactor; + } +#endif + + CSSToLayoutDeviceScale defaultCssScale(gdkScaleFactor * + gfxPlatformGtk::GetFontScaleFactor()); + + float dpi = 96.0f; + gint heightMM = gdk_screen_get_monitor_height_mm(aScreen, aMonitorNum); + if (heightMM > 0) { + dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT); + } + + MOZ_LOG(sScreenLog, LogLevel::Debug, + ("New screen [%d %d %d %d (%d %d %d %d) %d %f %f %f]", rect.x, rect.y, + rect.width, rect.height, availRect.x, availRect.y, availRect.width, + availRect.height, pixelDepth, contentsScale.scale, + defaultCssScale.scale, dpi)); + RefPtr<Screen> screen = new Screen(rect, availRect, pixelDepth, pixelDepth, + contentsScale, defaultCssScale, dpi); + return screen.forget(); +} + +void ScreenHelperGTK::RefreshScreens() { + MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing screens")); + AutoTArray<RefPtr<Screen>, 4> screenList; + + GdkScreen* defaultScreen = gdk_screen_get_default(); + gint numScreens = gdk_screen_get_n_monitors(defaultScreen); + MOZ_LOG(sScreenLog, LogLevel::Debug, ("GDK reports %d screens", numScreens)); + + for (gint i = 0; i < numScreens; i++) { + screenList.AppendElement(MakeScreen(defaultScreen, i)); + } + + ScreenManager& screenManager = ScreenManager::GetSingleton(); + screenManager.Refresh(std::move(screenList)); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/ScreenHelperGTK.h b/widget/gtk/ScreenHelperGTK.h new file mode 100644 index 0000000000..78b4d7d823 --- /dev/null +++ b/widget/gtk/ScreenHelperGTK.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_widget_gtk_ScreenHelperGTK_h +#define mozilla_widget_gtk_ScreenHelperGTK_h + +#include "mozilla/widget/ScreenManager.h" + +#include "gdk/gdk.h" +#ifdef MOZ_X11 +# include <X11/Xlib.h> +# include "X11UndefineNone.h" +#endif + +namespace mozilla { +namespace widget { + +class ScreenHelperGTK final : public ScreenManager::Helper { + public: + ScreenHelperGTK(); + ~ScreenHelperGTK() override; + + static gint GetGTKMonitorScaleFactor(gint aMonitorNum = 0); + +#ifdef MOZ_X11 + Atom NetWorkareaAtom() { return mNetWorkareaAtom; } +#endif + + // For internal use from signal callback functions + void RefreshScreens(); + + private: + GdkWindow* mRootWindow; +#ifdef MOZ_X11 + Atom mNetWorkareaAtom; +#endif +}; + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_gtk_ScreenHelperGTK_h diff --git a/widget/gtk/TaskbarProgress.cpp b/widget/gtk/TaskbarProgress.cpp new file mode 100644 index 0000000000..2aad109eab --- /dev/null +++ b/widget/gtk/TaskbarProgress.cpp @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Logging.h" + +#include "TaskbarProgress.h" +#include "nsWindow.h" +#include "WidgetUtils.h" +#include "nsPIDOMWindow.h" + +using mozilla::LogLevel; +static mozilla::LazyLogModule gGtkTaskbarProgressLog("nsIGtkTaskbarProgress"); + +/****************************************************************************** + * TaskbarProgress + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(TaskbarProgress, nsIGtkTaskbarProgress, nsITaskbarProgress) + +TaskbarProgress::TaskbarProgress() : mPrimaryWindow(nullptr) { + MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Info, + ("%p TaskbarProgress()", this)); +} + +TaskbarProgress::~TaskbarProgress() { + MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Info, + ("%p ~TaskbarProgress()", this)); +} + +NS_IMETHODIMP +TaskbarProgress::SetProgressState(nsTaskbarProgressState aState, + uint64_t aCurrentValue, uint64_t aMaxValue) { +#ifdef MOZ_X11 + NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED); + + if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) { + NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG); + } + + NS_ENSURE_TRUE((aCurrentValue <= aMaxValue), NS_ERROR_ILLEGAL_VALUE); + + // See TaskbarProgress::SetPrimaryWindow: if we're running in headless + // mode, mPrimaryWindow will be null. + if (!mPrimaryWindow) { + return NS_OK; + } + + gulong progress; + + if (aMaxValue == 0) { + progress = 0; + } else { + // Rounding down to ensure we don't set to 'full' until the operation + // is completely finished. + progress = (gulong)(((double)aCurrentValue / aMaxValue) * 100.0); + } + + // Check if the resultant value is the same as the previous call, and + // ignore this update if it is. + + if (progress == mCurrentProgress) { + return NS_OK; + } + + mCurrentProgress = progress; + + MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Debug, + ("GtkTaskbarProgress::SetProgressState progress: %lu", progress)); + + mPrimaryWindow->SetProgress(progress); +#endif + + return NS_OK; +} + +NS_IMETHODIMP +TaskbarProgress::SetPrimaryWindow(mozIDOMWindowProxy* aWindow) { + NS_ENSURE_TRUE(aWindow != nullptr, NS_ERROR_ILLEGAL_VALUE); + + auto* parent = nsPIDOMWindowOuter::From(aWindow); + RefPtr<nsIWidget> widget = + mozilla::widget::WidgetUtils::DOMWindowToWidget(parent); + + // Only nsWindows have a native window, HeadlessWidgets do not. Stop here if + // the window does not have one. + if (!widget->GetNativeData(NS_NATIVE_WINDOW)) { + return NS_OK; + } + + mPrimaryWindow = static_cast<nsWindow*>(widget.get()); + + // Clear our current progress. We get a forced update from the + // DownloadsTaskbar after returning from this function - zeroing out our + // progress will make sure the new window gets the property set on it + // immediately, rather than waiting for the progress value to change (which + // could be a while depending on size.) + mCurrentProgress = 0; + + MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Debug, + ("GtkTaskbarProgress::SetPrimaryWindow window: %p", + mPrimaryWindow.get())); + + return NS_OK; +} diff --git a/widget/gtk/TaskbarProgress.h b/widget/gtk/TaskbarProgress.h new file mode 100644 index 0000000000..819df0c089 --- /dev/null +++ b/widget/gtk/TaskbarProgress.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; 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 TaskbarProgress_h_ +#define TaskbarProgress_h_ + +#include "nsIGtkTaskbarProgress.h" + +class nsWindow; + +class TaskbarProgress final : public nsIGtkTaskbarProgress { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGTKTASKBARPROGRESS + NS_DECL_NSITASKBARPROGRESS + + TaskbarProgress(); + + protected: + ~TaskbarProgress(); + + // We track the progress value so we can avoid updating the X window property + // unnecessarily. + unsigned long mCurrentProgress; + + RefPtr<nsWindow> mPrimaryWindow; +}; + +#endif // #ifndef TaskbarProgress_h_ diff --git a/widget/gtk/WakeLockListener.cpp b/widget/gtk/WakeLockListener.cpp new file mode 100644 index 0000000000..2192e3f492 --- /dev/null +++ b/widget/gtk/WakeLockListener.cpp @@ -0,0 +1,500 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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/. */ + +#ifdef MOZ_ENABLE_DBUS + +# include "WakeLockListener.h" + +# include <dbus/dbus.h> +# include <dbus/dbus-glib-lowlevel.h> + +# if defined(MOZ_X11) +# include "gfxPlatformGtk.h" +# include "prlink.h" +# include <gdk/gdk.h> +# include <gdk/gdkx.h> +# endif + +# if defined(MOZ_WAYLAND) +# include "mozilla/widget/nsWaylandDisplay.h" +# include "nsWindow.h" +# include "mozilla/dom/power/PowerManagerService.h" +# endif + +# define FREEDESKTOP_SCREENSAVER_TARGET "org.freedesktop.ScreenSaver" +# define FREEDESKTOP_SCREENSAVER_OBJECT "/ScreenSaver" +# define FREEDESKTOP_SCREENSAVER_INTERFACE "org.freedesktop.ScreenSaver" + +# define SESSION_MANAGER_TARGET "org.gnome.SessionManager" +# define SESSION_MANAGER_OBJECT "/org/gnome/SessionManager" +# define SESSION_MANAGER_INTERFACE "org.gnome.SessionManager" + +# define DBUS_TIMEOUT (-1) + +using namespace mozilla; +using namespace mozilla::widget; + +NS_IMPL_ISUPPORTS(WakeLockListener, nsIDOMMozWakeLockListener) + +StaticRefPtr<WakeLockListener> WakeLockListener::sSingleton; + +# define WAKE_LOCK_LOG(...) \ + MOZ_LOG(gLinuxWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +static mozilla::LazyLogModule gLinuxWakeLockLog("LinuxWakeLock"); + +enum DesktopEnvironment { + FreeDesktop, + GNOME, +# if defined(MOZ_X11) + XScreenSaver, +# endif +# if defined(MOZ_WAYLAND) + WaylandIdleInhibit, +# endif + Unsupported, +}; + +class WakeLockTopic { + public: + WakeLockTopic(const nsAString& aTopic, DBusConnection* aConnection) + : +# if defined(MOZ_WAYLAND) + mWaylandInhibitor(nullptr), +# endif + mTopic(NS_ConvertUTF16toUTF8(aTopic)), + mConnection(aConnection), + mDesktopEnvironment(FreeDesktop), + mInhibitRequest(0), + mShouldInhibit(false), + mWaitingForReply(false) { + } + + nsresult InhibitScreensaver(void); + nsresult UninhibitScreensaver(void); + + private: + bool SendInhibit(); + bool SendUninhibit(); + + bool SendFreeDesktopInhibitMessage(); + bool SendGNOMEInhibitMessage(); + bool SendMessage(DBusMessage* aMessage); + +# if defined(MOZ_X11) + static bool CheckXScreenSaverSupport(); + static bool InhibitXScreenSaver(bool inhibit); +# endif + +# if defined(MOZ_WAYLAND) + zwp_idle_inhibitor_v1* mWaylandInhibitor; + static bool CheckWaylandIdleInhibitSupport(); + bool InhibitWaylandIdle(); + bool UninhibitWaylandIdle(); +# endif + + static void ReceiveInhibitReply(DBusPendingCall* aPending, void* aUserData); + void InhibitFailed(); + void InhibitSucceeded(uint32_t aInhibitRequest); + + nsCString mTopic; + RefPtr<DBusConnection> mConnection; + + DesktopEnvironment mDesktopEnvironment; + + uint32_t mInhibitRequest; + + bool mShouldInhibit; + bool mWaitingForReply; +}; + +bool WakeLockTopic::SendMessage(DBusMessage* aMessage) { + // send message and get a handle for a reply + RefPtr<DBusPendingCall> reply; + dbus_connection_send_with_reply(mConnection, aMessage, + reply.StartAssignment(), DBUS_TIMEOUT); + if (!reply) { + return false; + } + + dbus_pending_call_set_notify(reply, &ReceiveInhibitReply, this, NULL); + + return true; +} + +bool WakeLockTopic::SendFreeDesktopInhibitMessage() { + RefPtr<DBusMessage> message = + already_AddRefed<DBusMessage>(dbus_message_new_method_call( + FREEDESKTOP_SCREENSAVER_TARGET, FREEDESKTOP_SCREENSAVER_OBJECT, + FREEDESKTOP_SCREENSAVER_INTERFACE, "Inhibit")); + + if (!message) { + return false; + } + + const char* app = g_get_prgname(); + const char* topic = mTopic.get(); + dbus_message_append_args(message, DBUS_TYPE_STRING, &app, DBUS_TYPE_STRING, + &topic, DBUS_TYPE_INVALID); + + return SendMessage(message); +} + +bool WakeLockTopic::SendGNOMEInhibitMessage() { + RefPtr<DBusMessage> message = + already_AddRefed<DBusMessage>(dbus_message_new_method_call( + SESSION_MANAGER_TARGET, SESSION_MANAGER_OBJECT, + SESSION_MANAGER_INTERFACE, "Inhibit")); + + if (!message) { + return false; + } + + static const uint32_t xid = 0; + static const uint32_t flags = (1 << 3); // Inhibit idle + const char* app = g_get_prgname(); + const char* topic = mTopic.get(); + dbus_message_append_args(message, DBUS_TYPE_STRING, &app, DBUS_TYPE_UINT32, + &xid, DBUS_TYPE_STRING, &topic, DBUS_TYPE_UINT32, + &flags, DBUS_TYPE_INVALID); + + return SendMessage(message); +} + +# if defined(MOZ_X11) + +typedef Bool (*_XScreenSaverQueryExtension_fn)(Display* dpy, int* event_base, + int* error_base); +typedef Bool (*_XScreenSaverQueryVersion_fn)(Display* dpy, int* major, + int* minor); +typedef void (*_XScreenSaverSuspend_fn)(Display* dpy, Bool suspend); + +static PRLibrary* sXssLib = nullptr; +static _XScreenSaverQueryExtension_fn _XSSQueryExtension = nullptr; +static _XScreenSaverQueryVersion_fn _XSSQueryVersion = nullptr; +static _XScreenSaverSuspend_fn _XSSSuspend = nullptr; + +/* static */ +bool WakeLockTopic::CheckXScreenSaverSupport() { + if (!sXssLib) { + sXssLib = PR_LoadLibrary("libXss.so.1"); + if (!sXssLib) { + return false; + } + } + + _XSSQueryExtension = (_XScreenSaverQueryExtension_fn)PR_FindFunctionSymbol( + sXssLib, "XScreenSaverQueryExtension"); + _XSSQueryVersion = (_XScreenSaverQueryVersion_fn)PR_FindFunctionSymbol( + sXssLib, "XScreenSaverQueryVersion"); + _XSSSuspend = (_XScreenSaverSuspend_fn)PR_FindFunctionSymbol( + sXssLib, "XScreenSaverSuspend"); + if (!_XSSQueryExtension || !_XSSQueryVersion || !_XSSSuspend) { + return false; + } + + GdkDisplay* gDisplay = gdk_display_get_default(); + if (!gDisplay || !GDK_IS_X11_DISPLAY(gDisplay)) { + return false; + } + Display* display = GDK_DISPLAY_XDISPLAY(gDisplay); + + int throwaway; + if (!_XSSQueryExtension(display, &throwaway, &throwaway)) return false; + + int major, minor; + if (!_XSSQueryVersion(display, &major, &minor)) return false; + // Needs to be compatible with version 1.1 + if (major != 1) return false; + if (minor < 1) return false; + + return true; +} + +/* static */ +bool WakeLockTopic::InhibitXScreenSaver(bool inhibit) { + // Should only be called if CheckXScreenSaverSupport returns true. + // There's a couple of safety checks here nonetheless. + if (!_XSSSuspend) { + return false; + } + GdkDisplay* gDisplay = gdk_display_get_default(); + if (!gDisplay || !GDK_IS_X11_DISPLAY(gDisplay)) { + return false; + } + Display* display = GDK_DISPLAY_XDISPLAY(gDisplay); + _XSSSuspend(display, inhibit); + return true; +} + +# endif + +# if defined(MOZ_WAYLAND) + +/* static */ +bool WakeLockTopic::CheckWaylandIdleInhibitSupport() { + RefPtr<nsWaylandDisplay> waylandDisplay = WaylandDisplayGet(); + return waylandDisplay && waylandDisplay->GetIdleInhibitManager() != nullptr; +} + +bool WakeLockTopic::InhibitWaylandIdle() { + RefPtr<nsWaylandDisplay> waylandDisplay = WaylandDisplayGet(); + if (!waylandDisplay) { + return false; + } + + nsWindow* focusedWindow = nsWindow::GetFocusedWindow(); + if (!focusedWindow) { + return false; + } + + UninhibitWaylandIdle(); + + MozContainer* container = focusedWindow->GetMozContainer(); + wl_surface* waylandSurface = moz_container_wayland_surface_lock(container); + if (waylandSurface) { + mWaylandInhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor( + waylandDisplay->GetIdleInhibitManager(), waylandSurface); + moz_container_wayland_surface_unlock(container, &waylandSurface); + } + return true; +} + +bool WakeLockTopic::UninhibitWaylandIdle() { + if (mWaylandInhibitor == nullptr) return false; + + zwp_idle_inhibitor_v1_destroy(mWaylandInhibitor); + mWaylandInhibitor = nullptr; + + return true; +} + +# endif + +bool WakeLockTopic::SendInhibit() { + bool sendOk = false; + + switch (mDesktopEnvironment) { + case FreeDesktop: + sendOk = SendFreeDesktopInhibitMessage(); + break; + case GNOME: + sendOk = SendGNOMEInhibitMessage(); + break; +# if defined(MOZ_X11) + case XScreenSaver: + return InhibitXScreenSaver(true); +# endif +# if defined(MOZ_WAYLAND) + case WaylandIdleInhibit: + return InhibitWaylandIdle(); +# endif + case Unsupported: + return false; + } + + if (sendOk) { + mWaitingForReply = true; + } + + return sendOk; +} + +bool WakeLockTopic::SendUninhibit() { + RefPtr<DBusMessage> message; + + if (mDesktopEnvironment == FreeDesktop) { + message = already_AddRefed<DBusMessage>(dbus_message_new_method_call( + FREEDESKTOP_SCREENSAVER_TARGET, FREEDESKTOP_SCREENSAVER_OBJECT, + FREEDESKTOP_SCREENSAVER_INTERFACE, "UnInhibit")); + } else if (mDesktopEnvironment == GNOME) { + message = already_AddRefed<DBusMessage>(dbus_message_new_method_call( + SESSION_MANAGER_TARGET, SESSION_MANAGER_OBJECT, + SESSION_MANAGER_INTERFACE, "Uninhibit")); + } +# if defined(MOZ_X11) + else if (mDesktopEnvironment == XScreenSaver) { + return InhibitXScreenSaver(false); + } +# endif +# if defined(MOZ_WAYLAND) + else if (mDesktopEnvironment == WaylandIdleInhibit) { + return UninhibitWaylandIdle(); + } +# endif + + if (!message) { + return false; + } + + dbus_message_append_args(message, DBUS_TYPE_UINT32, &mInhibitRequest, + DBUS_TYPE_INVALID); + + dbus_connection_send(mConnection, message, nullptr); + dbus_connection_flush(mConnection); + + mInhibitRequest = 0; + + return true; +} + +nsresult WakeLockTopic::InhibitScreensaver() { + if (mShouldInhibit) { + // Screensaver is inhibited. Nothing to do here. + return NS_OK; + } + + mShouldInhibit = true; + + if (mWaitingForReply) { + // We already have a screensaver inhibit request pending. This can happen + // if InhibitScreensaver is called, then UninhibitScreensaver, then + // InhibitScreensaver again quickly. + return NS_OK; + } + + return SendInhibit() ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult WakeLockTopic::UninhibitScreensaver() { + if (!mShouldInhibit) { + // Screensaver isn't inhibited. Nothing to do here. + return NS_OK; + } + + mShouldInhibit = false; + + if (mWaitingForReply) { + // If we're still waiting for a response to our inhibit request, we can't + // do anything until we get a dbus message back. The callbacks below will + // check |mShouldInhibit| and act accordingly. + return NS_OK; + } + + return SendUninhibit() ? NS_OK : NS_ERROR_FAILURE; +} + +void WakeLockTopic::InhibitFailed() { + mWaitingForReply = false; + + if (mDesktopEnvironment == FreeDesktop) { + mDesktopEnvironment = GNOME; +# if defined(MOZ_X11) + } else if (mDesktopEnvironment == GNOME && CheckXScreenSaverSupport()) { + mDesktopEnvironment = XScreenSaver; +# endif +# if defined(MOZ_WAYLAND) + } else if (mDesktopEnvironment == GNOME && CheckWaylandIdleInhibitSupport()) { + mDesktopEnvironment = WaylandIdleInhibit; +# endif + } else { + mDesktopEnvironment = Unsupported; + mShouldInhibit = false; + } + + if (!mShouldInhibit) { + // We were interrupted by UninhibitScreensaver() before we could find the + // correct desktop environment. + return; + } + + SendInhibit(); +} + +void WakeLockTopic::InhibitSucceeded(uint32_t aInhibitRequest) { + mWaitingForReply = false; + mInhibitRequest = aInhibitRequest; + + if (!mShouldInhibit) { + // We successfully inhibited the screensaver, but UninhibitScreensaver() + // was called while we were waiting for a reply. + SendUninhibit(); + } +} + +/* static */ +void WakeLockTopic::ReceiveInhibitReply(DBusPendingCall* pending, + void* user_data) { + if (!WakeLockListener::GetSingleton(false)) { + // The WakeLockListener (and therefore our topic) was deleted while we were + // waiting for a reply. + return; + } + + WakeLockTopic* self = static_cast<WakeLockTopic*>(user_data); + + RefPtr<DBusMessage> msg = + already_AddRefed<DBusMessage>(dbus_pending_call_steal_reply(pending)); + if (!msg) { + return; + } + + if (dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_METHOD_RETURN) { + uint32_t inhibitRequest; + + if (dbus_message_get_args(msg, nullptr, DBUS_TYPE_UINT32, &inhibitRequest, + DBUS_TYPE_INVALID)) { + self->InhibitSucceeded(inhibitRequest); + } + } else { + self->InhibitFailed(); + } +} + +WakeLockListener::WakeLockListener() : mConnection(nullptr) {} + +/* static */ +WakeLockListener* WakeLockListener::GetSingleton(bool aCreate) { + if (!sSingleton && aCreate) { + sSingleton = new WakeLockListener(); + } + + return sSingleton; +} + +/* static */ +void WakeLockListener::Shutdown() { sSingleton = nullptr; } + +bool WakeLockListener::EnsureDBusConnection() { + if (!mConnection) { + mConnection = already_AddRefed<DBusConnection>( + dbus_bus_get(DBUS_BUS_SESSION, nullptr)); + + if (mConnection) { + dbus_connection_set_exit_on_disconnect(mConnection, false); + dbus_connection_setup_with_g_main(mConnection, nullptr); + } + } + + return mConnection != nullptr; +} + +nsresult WakeLockListener::Callback(const nsAString& topic, + const nsAString& state) { + if (!EnsureDBusConnection()) { + return NS_ERROR_FAILURE; + } + + if (!topic.Equals(u"screen"_ns) && !topic.Equals(u"audio-playing"_ns) && + !topic.Equals(u"video-playing"_ns)) + return NS_OK; + + WakeLockTopic* topicLock = mTopics.Get(topic); + if (!topicLock) { + topicLock = new WakeLockTopic(topic, mConnection); + mTopics.Put(topic, topicLock); + } + + // Treat "locked-background" the same as "unlocked" on desktop linux. + bool shouldLock = state.EqualsLiteral("locked-foreground"); + WAKE_LOCK_LOG("topic=%s, shouldLock=%d", NS_ConvertUTF16toUTF8(topic).get(), + shouldLock); + + return shouldLock ? topicLock->InhibitScreensaver() + : topicLock->UninhibitScreensaver(); +} + +#endif diff --git a/widget/gtk/WakeLockListener.h b/widget/gtk/WakeLockListener.h new file mode 100644 index 0000000000..4f163434df --- /dev/null +++ b/widget/gtk/WakeLockListener.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 __WakeLockListener_h__ +#define __WakeLockListener_h__ + +#include <unistd.h> + +#include "mozilla/StaticPtr.h" +#include "nsHashKeys.h" +#include "nsClassHashtable.h" + +#include "nsIDOMWakeLockListener.h" + +#ifdef MOZ_ENABLE_DBUS +# include "mozilla/DBusHelpers.h" +#endif + +class WakeLockTopic; + +/** + * Receives WakeLock events and simply passes it on to the right WakeLockTopic + * to inhibit the screensaver. + */ +class WakeLockListener final : public nsIDOMMozWakeLockListener { + public: + NS_DECL_ISUPPORTS; + + static WakeLockListener* GetSingleton(bool aCreate = true); + static void Shutdown(); + + virtual nsresult Callback(const nsAString& topic, + const nsAString& state) override; + + private: + WakeLockListener(); + ~WakeLockListener() = default; + + bool EnsureDBusConnection(); + + static mozilla::StaticRefPtr<WakeLockListener> sSingleton; + +#ifdef MOZ_ENABLE_DBUS + RefPtr<DBusConnection> mConnection; +#endif + // Map of topic names to |WakeLockTopic|s. + // We assume a small, finite-sized set of topics. + nsClassHashtable<nsStringHashKey, WakeLockTopic> mTopics; +}; + +#endif // __WakeLockListener_h__ diff --git a/widget/gtk/WaylandVsyncSource.cpp b/widget/gtk/WaylandVsyncSource.cpp new file mode 100644 index 0000000000..02f9d9c38b --- /dev/null +++ b/widget/gtk/WaylandVsyncSource.cpp @@ -0,0 +1,214 @@ +/* -*- 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/. */ + +#ifdef MOZ_WAYLAND + +# include "WaylandVsyncSource.h" +# include "nsThreadUtils.h" +# include "nsISupportsImpl.h" +# include "MainThreadUtils.h" + +# include <gdk/gdkwayland.h> + +using namespace mozilla::widget; + +namespace mozilla { + +static void WaylandVsyncSourceCallbackHandler(void* data, + struct wl_callback* callback, + uint32_t time) { + WaylandVsyncSource::WaylandDisplay* context = + (WaylandVsyncSource::WaylandDisplay*)data; + wl_callback_destroy(callback); + context->FrameCallback(time); +} + +static const struct wl_callback_listener WaylandVsyncSourceCallbackListener = { + WaylandVsyncSourceCallbackHandler}; + +WaylandVsyncSource::WaylandDisplay::WaylandDisplay(MozContainer* container) + : mEnabledLock("WaylandVsyncEnabledLock"), + mIsShutdown(false), + mVsyncEnabled(false), + mMonitorEnabled(false), + mCallback(nullptr), + mContainer(container), + mLastVsyncTimeStamp(TimeStamp::Now()) { + MOZ_ASSERT(NS_IsMainThread()); + + // We store the display here so all the frame callbacks won't have to look it + // up all the time. + mDisplay = WaylandDisplayGetWLDisplay(); + + mVsyncRate = TimeDuration::FromMilliseconds(1000.0 / 60.0); +} + +void WaylandVsyncSource::WaylandDisplay::ClearFrameCallback() { + if (mCallback) { + wl_callback_destroy(mCallback); + mCallback = nullptr; + } +} + +void WaylandVsyncSource::WaylandDisplay::Refresh() { + TimeStamp outputTimestamp; + { + MutexAutoLock lock(mEnabledLock); + if (!mMonitorEnabled || !mVsyncEnabled || mCallback) { + // We don't need to do anything because: + // * We are unwanted by our widget or monitor, or + // * The last frame callback hasn't yet run to see that it had been shut + // down, so we can reuse it after having set mVsyncEnabled to true. + return; + } + + struct wl_surface* surface = moz_container_wayland_surface_lock(mContainer); + if (!surface) { + // The surface hasn't been created yet. Try again when the surface is + // ready. + RefPtr<WaylandVsyncSource::WaylandDisplay> self(this); + moz_container_wayland_add_initial_draw_callback( + mContainer, [self]() -> void { self->Refresh(); }); + return; + } + moz_container_wayland_surface_unlock(mContainer, &surface); + + // Vsync is enabled, but we don't have a callback configured. Set one up so + // we can get to work. + SetupFrameCallback(); + mLastVsyncTimeStamp = TimeStamp::Now(); + outputTimestamp = mLastVsyncTimeStamp + GetVsyncRate(); + } + NotifyVsync(mLastVsyncTimeStamp, outputTimestamp); +} + +void WaylandVsyncSource::WaylandDisplay::EnableMonitor() { + { + MutexAutoLock lock(mEnabledLock); + if (mMonitorEnabled) { + return; + } + mMonitorEnabled = true; + } + Refresh(); +} + +void WaylandVsyncSource::WaylandDisplay::DisableMonitor() { + MutexAutoLock lock(mEnabledLock); + if (!mMonitorEnabled) { + return; + } + mMonitorEnabled = false; + ClearFrameCallback(); +} + +void WaylandVsyncSource::WaylandDisplay::SetupFrameCallback() { + MOZ_ASSERT(mCallback == nullptr); + struct wl_surface* surface = moz_container_wayland_surface_lock(mContainer); + if (!surface) { + // We don't have a surface, either due to being called before it was made + // available in the mozcontainer, or after it was destroyed. We're all done + // regardless. + ClearFrameCallback(); + return; + } + + mCallback = wl_surface_frame(surface); + wl_callback_add_listener(mCallback, &WaylandVsyncSourceCallbackListener, + this); + wl_surface_commit(surface); + wl_display_flush(mDisplay); + moz_container_wayland_surface_unlock(mContainer, &surface); +} + +void WaylandVsyncSource::WaylandDisplay::FrameCallback(uint32_t timestampTime) { + TimeStamp outputTimestamp; + { + MutexAutoLock lock(mEnabledLock); + mCallback = nullptr; + + if (!mVsyncEnabled || !mMonitorEnabled) { + // We are unwanted by either our creator or our consumer, so we just stop + // here without setting up a new frame callback. + return; + } + + // Configure our next frame callback. + SetupFrameCallback(); + + int64_t tick = + BaseTimeDurationPlatformUtils::TicksFromMilliseconds(timestampTime); + TimeStamp callbackTimeStamp = TimeStamp::FromSystemTime(tick); + double duration = (TimeStamp::Now() - callbackTimeStamp).ToMilliseconds(); + + TimeStamp vsyncTimestamp; + if (duration < 50 && duration > -50) { + vsyncTimestamp = callbackTimeStamp; + } else { + vsyncTimestamp = TimeStamp::Now(); + } + + CalculateVsyncRate(vsyncTimestamp); + mLastVsyncTimeStamp = vsyncTimestamp; + outputTimestamp = vsyncTimestamp + GetVsyncRate(); + } + NotifyVsync(mLastVsyncTimeStamp, outputTimestamp); +} + +TimeDuration WaylandVsyncSource::WaylandDisplay::GetVsyncRate() { + return mVsyncRate; +} + +void WaylandVsyncSource::WaylandDisplay::CalculateVsyncRate( + TimeStamp vsyncTimestamp) { + double duration = (vsyncTimestamp - mLastVsyncTimeStamp).ToMilliseconds(); + double curVsyncRate = mVsyncRate.ToMilliseconds(); + double correction; + + if (duration > curVsyncRate) { + correction = fmin(curVsyncRate, (duration - curVsyncRate) / 10); + mVsyncRate += TimeDuration::FromMilliseconds(correction); + } else { + correction = fmin(curVsyncRate / 2, (curVsyncRate - duration) / 10); + mVsyncRate -= TimeDuration::FromMilliseconds(correction); + } +} + +void WaylandVsyncSource::WaylandDisplay::EnableVsync() { + MOZ_ASSERT(NS_IsMainThread()); + { + MutexAutoLock lock(mEnabledLock); + if (mVsyncEnabled || mIsShutdown) { + return; + } + mVsyncEnabled = true; + } + Refresh(); +} + +void WaylandVsyncSource::WaylandDisplay::DisableVsync() { + MutexAutoLock lock(mEnabledLock); + mVsyncEnabled = false; + ClearFrameCallback(); +} + +bool WaylandVsyncSource::WaylandDisplay::IsVsyncEnabled() { + MutexAutoLock lock(mEnabledLock); + return mVsyncEnabled; +} + +void WaylandVsyncSource::WaylandDisplay::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mEnabledLock); + mIsShutdown = true; + mVsyncEnabled = false; + ClearFrameCallback(); + wl_display_roundtrip(mDisplay); +} + +} // namespace mozilla + +#endif // MOZ_WAYLAND diff --git a/widget/gtk/WaylandVsyncSource.h b/widget/gtk/WaylandVsyncSource.h new file mode 100644 index 0000000000..fc6fcc10cf --- /dev/null +++ b/widget/gtk/WaylandVsyncSource.h @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _WaylandVsyncSource_h_ +#define _WaylandVsyncSource_h_ + +#include "mozilla/RefPtr.h" +#include "mozilla/Mutex.h" +#include "mozilla/Monitor.h" +#include "MozContainer.h" +#include "VsyncSource.h" +#include "base/thread.h" +#include "nsWaylandDisplay.h" + +namespace mozilla { + +/* + * WaylandVsyncSource + * + * This class provides a per-widget VsyncSource under Wayland, emulated using + * frame callbacks on the widget surface with empty surface commits. + * + * Wayland does not expose vsync/vblank, as it considers that an implementation + * detail the clients should not concern themselves with. Instead, frame + * callbacks are provided whenever the compositor believes it is a good time to + * start drawing the next frame for a particular surface, giving us as much + * time as possible to do so. + * + * Note that the compositor sends frame callbacks only when it sees fit, and + * when that may be is entirely up to the compositor. One cannot expect a + * certain rate of callbacks, or any callbacks at all. Examples of common + * variations would be surfaces moved between outputs with different refresh + * rates, and surfaces that are hidden and therefore do not receieve any + * callbacks at all. Other hypothetical scenarios of variation could be + * throttling to conserve power, or because a user has requested it. + * + */ +class WaylandVsyncSource final : public gfx::VsyncSource { + public: + explicit WaylandVsyncSource(MozContainer* container) { + MOZ_ASSERT(NS_IsMainThread()); + mGlobalDisplay = new WaylandDisplay(container); + } + + virtual ~WaylandVsyncSource() { MOZ_ASSERT(NS_IsMainThread()); } + + virtual Display& GetGlobalDisplay() override { return *mGlobalDisplay; } + + class WaylandDisplay final : public mozilla::gfx::VsyncSource::Display { + public: + explicit WaylandDisplay(MozContainer* container); + + void EnableMonitor(); + void DisableMonitor(); + + void FrameCallback(uint32_t timestampTime); + void Notify(); + + TimeDuration GetVsyncRate() override; + + virtual void EnableVsync() override; + + virtual void DisableVsync() override; + + virtual bool IsVsyncEnabled() override; + + virtual void Shutdown() override; + + private: + virtual ~WaylandDisplay() = default; + void Refresh(); + void SetupFrameCallback(); + void ClearFrameCallback(); + void CalculateVsyncRate(TimeStamp vsyncTimestamp); + + Mutex mEnabledLock; + bool mIsShutdown; + bool mVsyncEnabled; + bool mMonitorEnabled; + struct wl_display* mDisplay; + struct wl_callback* mCallback; + MozContainer* mContainer; + TimeDuration mVsyncRate; + TimeStamp mLastVsyncTimeStamp; + }; + + private: + // We need a refcounted VsyncSource::Display to use chromium IPC runnables. + RefPtr<WaylandDisplay> mGlobalDisplay; +}; + +} // namespace mozilla + +#endif // _WaylandVsyncSource_h_ diff --git a/widget/gtk/WidgetStyleCache.cpp b/widget/gtk/WidgetStyleCache.cpp new file mode 100644 index 0000000000..15f66f4876 --- /dev/null +++ b/widget/gtk/WidgetStyleCache.cpp @@ -0,0 +1,1464 @@ +/* -*- 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 <dlfcn.h> +#include <gtk/gtk.h> +#include "WidgetStyleCache.h" +#include "gtkdrawing.h" +#include "mozilla/Assertions.h" +#include "mozilla/PodOperations.h" +#include "nsDebug.h" +#include "nsPrintfCString.h" +#include "nsString.h" + +#define STATE_FLAG_DIR_LTR (1U << 7) +#define STATE_FLAG_DIR_RTL (1U << 8) +static_assert(GTK_STATE_FLAG_DIR_LTR == STATE_FLAG_DIR_LTR && + GTK_STATE_FLAG_DIR_RTL == STATE_FLAG_DIR_RTL, + "incorrect direction state flags"); + +static GtkWidget* sWidgetStorage[MOZ_GTK_WIDGET_NODE_COUNT]; +static GtkStyleContext* sStyleStorage[MOZ_GTK_WIDGET_NODE_COUNT]; + +static GtkStyleContext* GetWidgetRootStyle(WidgetNodeType aNodeType); +static GtkStyleContext* GetCssNodeStyleInternal(WidgetNodeType aNodeType); + +static GtkWidget* CreateWindowWidget() { + GtkWidget* widget = gtk_window_new(GTK_WINDOW_POPUP); + MOZ_RELEASE_ASSERT(widget, "We're missing GtkWindow widget!"); + gtk_widget_set_name(widget, "MozillaGtkWidget"); + return widget; +} + +static GtkWidget* CreateWindowContainerWidget() { + GtkWidget* widget = gtk_fixed_new(); + gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_WINDOW)), widget); + return widget; +} + +static void AddToWindowContainer(GtkWidget* widget) { + gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_WINDOW_CONTAINER)), widget); +} + +static GtkWidget* CreateScrollbarWidget(WidgetNodeType aAppearance, + GtkOrientation aOrientation) { + GtkWidget* widget = gtk_scrollbar_new(aOrientation, nullptr); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateCheckboxWidget() { + GtkWidget* widget = gtk_check_button_new_with_label("M"); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateRadiobuttonWidget() { + GtkWidget* widget = gtk_radio_button_new_with_label(nullptr, "M"); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateMenuBarWidget() { + GtkWidget* widget = gtk_menu_bar_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateMenuPopupWidget() { + GtkWidget* widget = gtk_menu_new(); + gtk_menu_attach_to_widget(GTK_MENU(widget), GetWidget(MOZ_GTK_WINDOW), + nullptr); + return widget; +} + +static GtkWidget* CreateProgressWidget() { + GtkWidget* widget = gtk_progress_bar_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateTooltipWidget() { + MOZ_ASSERT(gtk_check_version(3, 20, 0) != nullptr, + "CreateTooltipWidget should be used for Gtk < 3.20 only."); + GtkWidget* widget = CreateWindowWidget(); + GtkStyleContext* style = gtk_widget_get_style_context(widget); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_TOOLTIP); + return widget; +} + +static GtkWidget* CreateExpanderWidget() { + GtkWidget* widget = gtk_expander_new("M"); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateFrameWidget() { + GtkWidget* widget = gtk_frame_new(nullptr); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateGripperWidget() { + GtkWidget* widget = gtk_handle_box_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateToolbarWidget() { + GtkWidget* widget = gtk_toolbar_new(); + gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_GRIPPER)), widget); + return widget; +} + +static GtkWidget* CreateToolbarSeparatorWidget() { + GtkWidget* widget = GTK_WIDGET(gtk_separator_tool_item_new()); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateInfoBarWidget() { + GtkWidget* widget = gtk_info_bar_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateButtonWidget() { + GtkWidget* widget = gtk_button_new_with_label("M"); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateToggleButtonWidget() { + GtkWidget* widget = gtk_toggle_button_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateButtonArrowWidget() { + GtkWidget* widget = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_OUT); + gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_TOGGLE_BUTTON)), widget); + gtk_widget_show(widget); + return widget; +} + +static GtkWidget* CreateSpinWidget() { + GtkWidget* widget = gtk_spin_button_new(nullptr, 1, 0); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateEntryWidget() { + GtkWidget* widget = gtk_entry_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateComboBoxWidget() { + GtkWidget* widget = gtk_combo_box_new(); + AddToWindowContainer(widget); + return widget; +} + +typedef struct { + GType type; + GtkWidget** widget; +} GtkInnerWidgetInfo; + +static void GetInnerWidget(GtkWidget* widget, gpointer client_data) { + auto info = static_cast<GtkInnerWidgetInfo*>(client_data); + + if (G_TYPE_CHECK_INSTANCE_TYPE(widget, info->type)) { + *info->widget = widget; + } +} + +static GtkWidget* CreateComboBoxButtonWidget() { + GtkWidget* comboBox = GetWidget(MOZ_GTK_COMBOBOX); + GtkWidget* comboBoxButton = nullptr; + + /* Get its inner Button */ + GtkInnerWidgetInfo info = {GTK_TYPE_TOGGLE_BUTTON, &comboBoxButton}; + gtk_container_forall(GTK_CONTAINER(comboBox), GetInnerWidget, &info); + + if (!comboBoxButton) { + /* Shouldn't be reached with current internal gtk implementation; we + * use a generic toggle button as last resort fallback to avoid + * crashing. */ + comboBoxButton = GetWidget(MOZ_GTK_TOGGLE_BUTTON); + } else { + /* We need to have pointers to the inner widgets (button, separator, arrow) + * of the ComboBox to get the correct rendering from theme engines which + * special cases their look. Since the inner layout can change, we ask GTK + * to NULL our pointers when they are about to become invalid because the + * corresponding widgets don't exist anymore. It's the role of + * g_object_add_weak_pointer(). + * Note that if we don't find the inner widgets (which shouldn't happen), we + * fallback to use generic "non-inner" widgets, and they don't need that + * kind of weak pointer since they are explicit children of gProtoLayout and + * as such GTK holds a strong reference to them. */ + g_object_add_weak_pointer( + G_OBJECT(comboBoxButton), + reinterpret_cast<gpointer*>(sWidgetStorage) + MOZ_GTK_COMBOBOX_BUTTON); + } + + return comboBoxButton; +} + +static GtkWidget* CreateComboBoxArrowWidget() { + GtkWidget* comboBoxButton = GetWidget(MOZ_GTK_COMBOBOX_BUTTON); + GtkWidget* comboBoxArrow = nullptr; + + /* Get the widgets inside the Button */ + GtkWidget* buttonChild = gtk_bin_get_child(GTK_BIN(comboBoxButton)); + if (GTK_IS_BOX(buttonChild)) { + /* appears-as-list = FALSE, cell-view = TRUE; the button + * contains an hbox. This hbox is there because the ComboBox + * needs to place a cell renderer, a separator, and an arrow in + * the button when appears-as-list is FALSE. */ + GtkInnerWidgetInfo info = {GTK_TYPE_ARROW, &comboBoxArrow}; + gtk_container_forall(GTK_CONTAINER(buttonChild), GetInnerWidget, &info); + } else if (GTK_IS_ARROW(buttonChild)) { + /* appears-as-list = TRUE, or cell-view = FALSE; + * the button only contains an arrow */ + comboBoxArrow = buttonChild; + } + + if (!comboBoxArrow) { + /* Shouldn't be reached with current internal gtk implementation; + * we gButtonArrowWidget as last resort fallback to avoid + * crashing. */ + comboBoxArrow = GetWidget(MOZ_GTK_BUTTON_ARROW); + } else { + g_object_add_weak_pointer( + G_OBJECT(comboBoxArrow), + reinterpret_cast<gpointer*>(sWidgetStorage) + MOZ_GTK_COMBOBOX_ARROW); + } + + return comboBoxArrow; +} + +static GtkWidget* CreateComboBoxSeparatorWidget() { + // Ensure to search for separator only once as it can fail + // TODO - it won't initialize after ResetWidgetCache() call + static bool isMissingSeparator = false; + if (isMissingSeparator) return nullptr; + + /* Get the widgets inside the Button */ + GtkWidget* comboBoxSeparator = nullptr; + GtkWidget* buttonChild = + gtk_bin_get_child(GTK_BIN(GetWidget(MOZ_GTK_COMBOBOX_BUTTON))); + if (GTK_IS_BOX(buttonChild)) { + /* appears-as-list = FALSE, cell-view = TRUE; the button + * contains an hbox. This hbox is there because the ComboBox + * needs to place a cell renderer, a separator, and an arrow in + * the button when appears-as-list is FALSE. */ + GtkInnerWidgetInfo info = {GTK_TYPE_SEPARATOR, &comboBoxSeparator}; + gtk_container_forall(GTK_CONTAINER(buttonChild), GetInnerWidget, &info); + } + + if (comboBoxSeparator) { + g_object_add_weak_pointer(G_OBJECT(comboBoxSeparator), + reinterpret_cast<gpointer*>(sWidgetStorage) + + MOZ_GTK_COMBOBOX_SEPARATOR); + } else { + /* comboBoxSeparator may be NULL + * when "appears-as-list" = TRUE or "cell-view" = FALSE; + * if there is no separator, then we just won't paint it. */ + isMissingSeparator = true; + } + + return comboBoxSeparator; +} + +static GtkWidget* CreateComboBoxEntryWidget() { + GtkWidget* widget = gtk_combo_box_new_with_entry(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateComboBoxEntryTextareaWidget() { + GtkWidget* comboBoxTextarea = nullptr; + + /* Get its inner Entry and Button */ + GtkInnerWidgetInfo info = {GTK_TYPE_ENTRY, &comboBoxTextarea}; + gtk_container_forall(GTK_CONTAINER(GetWidget(MOZ_GTK_COMBOBOX_ENTRY)), + GetInnerWidget, &info); + + if (!comboBoxTextarea) { + comboBoxTextarea = GetWidget(MOZ_GTK_ENTRY); + } else { + g_object_add_weak_pointer( + G_OBJECT(comboBoxTextarea), + reinterpret_cast<gpointer*>(sWidgetStorage) + MOZ_GTK_COMBOBOX_ENTRY); + } + + return comboBoxTextarea; +} + +static GtkWidget* CreateComboBoxEntryButtonWidget() { + GtkWidget* comboBoxButton = nullptr; + + /* Get its inner Entry and Button */ + GtkInnerWidgetInfo info = {GTK_TYPE_TOGGLE_BUTTON, &comboBoxButton}; + gtk_container_forall(GTK_CONTAINER(GetWidget(MOZ_GTK_COMBOBOX_ENTRY)), + GetInnerWidget, &info); + + if (!comboBoxButton) { + comboBoxButton = GetWidget(MOZ_GTK_TOGGLE_BUTTON); + } else { + g_object_add_weak_pointer(G_OBJECT(comboBoxButton), + reinterpret_cast<gpointer*>(sWidgetStorage) + + MOZ_GTK_COMBOBOX_ENTRY_BUTTON); + } + + return comboBoxButton; +} + +static GtkWidget* CreateComboBoxEntryArrowWidget() { + GtkWidget* comboBoxArrow = nullptr; + + /* Get the Arrow inside the Button */ + GtkWidget* buttonChild = + gtk_bin_get_child(GTK_BIN(GetWidget(MOZ_GTK_COMBOBOX_ENTRY_BUTTON))); + + if (GTK_IS_BOX(buttonChild)) { + /* appears-as-list = FALSE, cell-view = TRUE; the button + * contains an hbox. This hbox is there because the ComboBox + * needs to place a cell renderer, a separator, and an arrow in + * the button when appears-as-list is FALSE. */ + GtkInnerWidgetInfo info = {GTK_TYPE_ARROW, &comboBoxArrow}; + gtk_container_forall(GTK_CONTAINER(buttonChild), GetInnerWidget, &info); + } else if (GTK_IS_ARROW(buttonChild)) { + /* appears-as-list = TRUE, or cell-view = FALSE; + * the button only contains an arrow */ + comboBoxArrow = buttonChild; + } + + if (!comboBoxArrow) { + /* Shouldn't be reached with current internal gtk implementation; + * we gButtonArrowWidget as last resort fallback to avoid + * crashing. */ + comboBoxArrow = GetWidget(MOZ_GTK_BUTTON_ARROW); + } else { + g_object_add_weak_pointer(G_OBJECT(comboBoxArrow), + reinterpret_cast<gpointer*>(sWidgetStorage) + + MOZ_GTK_COMBOBOX_ENTRY_ARROW); + } + + return comboBoxArrow; +} + +static GtkWidget* CreateScrolledWindowWidget() { + GtkWidget* widget = gtk_scrolled_window_new(nullptr, nullptr); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateMenuSeparatorWidget() { + GtkWidget* widget = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(GetWidget(MOZ_GTK_MENUPOPUP)), widget); + return widget; +} + +static GtkWidget* CreateTreeViewWidget() { + GtkWidget* widget = gtk_tree_view_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateTreeHeaderCellWidget() { + /* + * Some GTK engines paint the first and last cell + * of a TreeView header with a highlight. + * Since we do not know where our widget will be relative + * to the other buttons in the TreeView header, we must + * paint it as a button that is between two others, + * thus ensuring it is neither the first or last button + * in the header. + * GTK doesn't give us a way to do this explicitly, + * so we must paint with a button that is between two + * others. + */ + GtkTreeViewColumn* firstTreeViewColumn; + GtkTreeViewColumn* middleTreeViewColumn; + GtkTreeViewColumn* lastTreeViewColumn; + + GtkWidget* treeView = GetWidget(MOZ_GTK_TREEVIEW); + + /* Create and append our three columns */ + firstTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(firstTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(treeView), firstTreeViewColumn); + + middleTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(middleTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(treeView), middleTreeViewColumn); + + lastTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(lastTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(treeView), lastTreeViewColumn); + + /* Use the middle column's header for our button */ + return gtk_tree_view_column_get_button(middleTreeViewColumn); +} + +static GtkWidget* CreateTreeHeaderSortArrowWidget() { + /* TODO, but it can't be NULL */ + GtkWidget* widget = gtk_button_new(); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateHPanedWidget() { + GtkWidget* widget = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateVPanedWidget() { + GtkWidget* widget = gtk_paned_new(GTK_ORIENTATION_VERTICAL); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateScaleWidget(GtkOrientation aOrientation) { + GtkWidget* widget = gtk_scale_new(aOrientation, nullptr); + AddToWindowContainer(widget); + return widget; +} + +static GtkWidget* CreateNotebookWidget() { + GtkWidget* widget = gtk_notebook_new(); + AddToWindowContainer(widget); + return widget; +} + +void GtkWindowSetTitlebar(GtkWindow* aWindow, GtkWidget* aWidget) { + static auto sGtkWindowSetTitlebar = (void (*)(GtkWindow*, GtkWidget*))dlsym( + RTLD_DEFAULT, "gtk_window_set_titlebar"); + sGtkWindowSetTitlebar(aWindow, aWidget); +} + +GtkWidget* GtkHeaderBarNew() { + static auto sGtkHeaderBarNewPtr = + (GtkWidget * (*)()) dlsym(RTLD_DEFAULT, "gtk_header_bar_new"); + return sGtkHeaderBarNewPtr(); +} + +bool IsSolidCSDStyleUsed() { + static bool isSolidCSDStyleUsed = []() { + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + GtkWindowSetTitlebar(GTK_WINDOW(window), GtkHeaderBarNew()); + gtk_widget_realize(window); + GtkStyleContext* windowStyle = gtk_widget_get_style_context(window); + bool ret = gtk_style_context_has_class(windowStyle, "solid-csd"); + gtk_widget_destroy(window); + return ret; + }(); + return isSolidCSDStyleUsed; +} + +static void CreateHeaderBarWidget(WidgetNodeType aAppearance) { + sWidgetStorage[aAppearance] = GtkHeaderBarNew(); + + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + GtkStyleContext* style = gtk_widget_get_style_context(window); + + if (aAppearance == MOZ_GTK_HEADER_BAR_MAXIMIZED) { + gtk_style_context_add_class(style, "maximized"); + MOZ_ASSERT(sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED] == nullptr, + "Window widget is already created!"); + sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED] = window; + } else { + MOZ_ASSERT(sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW] == nullptr, + "Window widget is already created!"); + sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW] = window; + } + + // Headerbar has to be placed to window with csd or solid-csd style + // to properly draw the decorated. + gtk_style_context_add_class(style, + IsSolidCSDStyleUsed() ? "solid-csd" : "csd"); + + GtkWidget* fixed = gtk_fixed_new(); + gtk_container_add(GTK_CONTAINER(window), fixed); + gtk_container_add(GTK_CONTAINER(fixed), sWidgetStorage[aAppearance]); + + // Emulate what create_titlebar() at gtkwindow.c does. + style = gtk_widget_get_style_context(sWidgetStorage[aAppearance]); + gtk_style_context_add_class(style, "titlebar"); + + // TODO: Define default-decoration titlebar style as workaround + // to ensure the titlebar buttons does not overflow outside. + // Recently the titlebar size is calculated as + // tab size + titlebar border/padding (default-decoration has 6px padding + // at default Adwaita theme). + // We need to fix titlebar size calculation to also include + // titlebar button sizes. (Bug 1419442) + gtk_style_context_add_class(style, "default-decoration"); +} + +#define ICON_SCALE_VARIANTS 2 + +static void LoadWidgetIconPixbuf(GtkWidget* aWidgetIcon) { + GtkStyleContext* style = gtk_widget_get_style_context(aWidgetIcon); + + const gchar* iconName; + GtkIconSize gtkIconSize; + gtk_image_get_icon_name(GTK_IMAGE(aWidgetIcon), &iconName, >kIconSize); + + gint iconWidth, iconHeight; + gtk_icon_size_lookup(gtkIconSize, &iconWidth, &iconHeight); + + /* Those are available since Gtk+ 3.10 as well as GtkHeaderBar */ + static auto sGtkIconThemeLookupIconForScalePtr = + (GtkIconInfo * + (*)(GtkIconTheme*, const gchar*, gint, gint, GtkIconLookupFlags)) + dlsym(RTLD_DEFAULT, "gtk_icon_theme_lookup_icon_for_scale"); + static auto sGdkCairoSurfaceCreateFromPixbufPtr = + (cairo_surface_t * (*)(const GdkPixbuf*, int, GdkWindow*)) + dlsym(RTLD_DEFAULT, "gdk_cairo_surface_create_from_pixbuf"); + + for (int scale = 1; scale < ICON_SCALE_VARIANTS + 1; scale++) { + GtkIconInfo* gtkIconInfo = sGtkIconThemeLookupIconForScalePtr( + gtk_icon_theme_get_default(), iconName, iconWidth, scale, + (GtkIconLookupFlags)0); + + if (!gtkIconInfo) { + // We miss the icon, nothing to do here. + return; + } + + gboolean unused; + GdkPixbuf* iconPixbuf = gtk_icon_info_load_symbolic_for_context( + gtkIconInfo, style, &unused, nullptr); + g_object_unref(G_OBJECT(gtkIconInfo)); + + cairo_surface_t* iconSurface = + sGdkCairoSurfaceCreateFromPixbufPtr(iconPixbuf, scale, nullptr); + g_object_unref(iconPixbuf); + + nsAutoCString surfaceName; + surfaceName = nsPrintfCString("MozillaIconSurface%d", scale); + g_object_set_data_full(G_OBJECT(aWidgetIcon), surfaceName.get(), + iconSurface, (GDestroyNotify)cairo_surface_destroy); + } +} + +cairo_surface_t* GetWidgetIconSurface(GtkWidget* aWidgetIcon, int aScale) { + if (aScale > ICON_SCALE_VARIANTS) aScale = ICON_SCALE_VARIANTS; + + nsAutoCString surfaceName; + surfaceName = nsPrintfCString("MozillaIconSurface%d", aScale); + return (cairo_surface_t*)g_object_get_data(G_OBJECT(aWidgetIcon), + surfaceName.get()); +} + +static void CreateHeaderBarButton(GtkWidget* aParentWidget, + WidgetNodeType aAppearance) { + GtkWidget* widget = gtk_button_new(); + + // We have to add button to widget hierarchy now to pick + // right icon style at LoadWidgetIconPixbuf(). + if (GTK_IS_BOX(aParentWidget)) { + gtk_box_pack_start(GTK_BOX(aParentWidget), widget, FALSE, FALSE, 0); + } else { + gtk_container_add(GTK_CONTAINER(aParentWidget), widget); + } + + // We bypass GetWidget() here because we create all titlebar + // buttons at once when a first one is requested. + NS_ASSERTION(sWidgetStorage[aAppearance] == nullptr, + "Titlebar button is already created!"); + sWidgetStorage[aAppearance] = widget; + + // We need to show the button widget now as GtkBox does not + // place invisible widgets and we'll miss first-child/last-child + // css selectors at the buttons otherwise. + gtk_widget_show(widget); + + GtkStyleContext* style = gtk_widget_get_style_context(widget); + gtk_style_context_add_class(style, "titlebutton"); + + GtkWidget* image = nullptr; + switch (aAppearance) { + case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE: + gtk_style_context_add_class(style, "close"); + image = gtk_image_new_from_icon_name("window-close-symbolic", + GTK_ICON_SIZE_MENU); + break; + case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE: + gtk_style_context_add_class(style, "minimize"); + image = gtk_image_new_from_icon_name("window-minimize-symbolic", + GTK_ICON_SIZE_MENU); + break; + + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE: + gtk_style_context_add_class(style, "maximize"); + image = gtk_image_new_from_icon_name("window-maximize-symbolic", + GTK_ICON_SIZE_MENU); + break; + + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE: + gtk_style_context_add_class(style, "maximize"); + image = gtk_image_new_from_icon_name("window-restore-symbolic", + GTK_ICON_SIZE_MENU); + break; + default: + break; + } + + gtk_widget_set_valign(widget, GTK_ALIGN_CENTER); + g_object_set(image, "use-fallback", TRUE, NULL); + gtk_container_add(GTK_CONTAINER(widget), image); + + // We bypass GetWidget() here by explicit sWidgetStorage[] update so + // invalidate the style as well as GetWidget() does. + style = gtk_widget_get_style_context(image); + gtk_style_context_invalidate(style); + + LoadWidgetIconPixbuf(image); +} + +static bool IsToolbarButtonEnabled(ButtonLayout* aButtonLayout, + size_t aButtonNums, + WidgetNodeType aAppearance) { + for (size_t i = 0; i < aButtonNums; i++) { + if (aButtonLayout[i].mType == aAppearance) { + return true; + } + } + return false; +} + +static void CreateHeaderBarButtons() { + GtkWidget* headerBar = sWidgetStorage[MOZ_GTK_HEADER_BAR]; + MOZ_ASSERT(headerBar != nullptr, "We're missing header bar widget!"); + + gint buttonSpacing = 6; + g_object_get(headerBar, "spacing", &buttonSpacing, nullptr); + + GtkWidget* buttonBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, buttonSpacing); + gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_HEADER_BAR)), buttonBox); + // We support only LTR headerbar layout for now. + gtk_style_context_add_class(gtk_widget_get_style_context(buttonBox), + GTK_STYLE_CLASS_LEFT); + + ButtonLayout buttonLayout[TOOLBAR_BUTTONS]; + + size_t activeButtons = + GetGtkHeaderBarButtonLayout(mozilla::Span(buttonLayout), nullptr); + + if (IsToolbarButtonEnabled(buttonLayout, activeButtons, + MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE)) { + CreateHeaderBarButton(buttonBox, MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE); + } + if (IsToolbarButtonEnabled(buttonLayout, activeButtons, + MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE)) { + CreateHeaderBarButton(buttonBox, MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE); + // We don't pack "restore" headerbar button to box as it's an icon + // placeholder. Pack it only to header bar to get correct style. + CreateHeaderBarButton(GetWidget(MOZ_GTK_HEADER_BAR), + MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE); + } + if (IsToolbarButtonEnabled(buttonLayout, activeButtons, + MOZ_GTK_HEADER_BAR_BUTTON_CLOSE)) { + CreateHeaderBarButton(buttonBox, MOZ_GTK_HEADER_BAR_BUTTON_CLOSE); + } +} + +static void CreateHeaderBar() { + CreateHeaderBarWidget(MOZ_GTK_HEADER_BAR); + CreateHeaderBarWidget(MOZ_GTK_HEADER_BAR_MAXIMIZED); + CreateHeaderBarButtons(); +} + +static GtkWidget* CreateWidget(WidgetNodeType aAppearance) { + switch (aAppearance) { + case MOZ_GTK_WINDOW: + return CreateWindowWidget(); + case MOZ_GTK_WINDOW_CONTAINER: + return CreateWindowContainerWidget(); + case MOZ_GTK_CHECKBUTTON_CONTAINER: + return CreateCheckboxWidget(); + case MOZ_GTK_PROGRESSBAR: + return CreateProgressWidget(); + case MOZ_GTK_RADIOBUTTON_CONTAINER: + return CreateRadiobuttonWidget(); + case MOZ_GTK_SCROLLBAR_HORIZONTAL: + return CreateScrollbarWidget(aAppearance, GTK_ORIENTATION_HORIZONTAL); + case MOZ_GTK_SCROLLBAR_VERTICAL: + return CreateScrollbarWidget(aAppearance, GTK_ORIENTATION_VERTICAL); + case MOZ_GTK_MENUBAR: + return CreateMenuBarWidget(); + case MOZ_GTK_MENUPOPUP: + return CreateMenuPopupWidget(); + case MOZ_GTK_MENUSEPARATOR: + return CreateMenuSeparatorWidget(); + case MOZ_GTK_EXPANDER: + return CreateExpanderWidget(); + case MOZ_GTK_FRAME: + return CreateFrameWidget(); + case MOZ_GTK_GRIPPER: + return CreateGripperWidget(); + case MOZ_GTK_TOOLBAR: + return CreateToolbarWidget(); + case MOZ_GTK_TOOLBAR_SEPARATOR: + return CreateToolbarSeparatorWidget(); + case MOZ_GTK_INFO_BAR: + return CreateInfoBarWidget(); + case MOZ_GTK_SPINBUTTON: + return CreateSpinWidget(); + case MOZ_GTK_BUTTON: + return CreateButtonWidget(); + case MOZ_GTK_TOGGLE_BUTTON: + return CreateToggleButtonWidget(); + case MOZ_GTK_BUTTON_ARROW: + return CreateButtonArrowWidget(); + case MOZ_GTK_ENTRY: + case MOZ_GTK_DROPDOWN_ENTRY: + return CreateEntryWidget(); + case MOZ_GTK_SCROLLED_WINDOW: + return CreateScrolledWindowWidget(); + case MOZ_GTK_TREEVIEW: + return CreateTreeViewWidget(); + case MOZ_GTK_TREE_HEADER_CELL: + return CreateTreeHeaderCellWidget(); + case MOZ_GTK_TREE_HEADER_SORTARROW: + return CreateTreeHeaderSortArrowWidget(); + case MOZ_GTK_SPLITTER_HORIZONTAL: + return CreateHPanedWidget(); + case MOZ_GTK_SPLITTER_VERTICAL: + return CreateVPanedWidget(); + case MOZ_GTK_SCALE_HORIZONTAL: + return CreateScaleWidget(GTK_ORIENTATION_HORIZONTAL); + case MOZ_GTK_SCALE_VERTICAL: + return CreateScaleWidget(GTK_ORIENTATION_VERTICAL); + case MOZ_GTK_NOTEBOOK: + return CreateNotebookWidget(); + case MOZ_GTK_COMBOBOX: + return CreateComboBoxWidget(); + case MOZ_GTK_COMBOBOX_BUTTON: + return CreateComboBoxButtonWidget(); + case MOZ_GTK_COMBOBOX_ARROW: + return CreateComboBoxArrowWidget(); + case MOZ_GTK_COMBOBOX_SEPARATOR: + return CreateComboBoxSeparatorWidget(); + case MOZ_GTK_COMBOBOX_ENTRY: + return CreateComboBoxEntryWidget(); + case MOZ_GTK_COMBOBOX_ENTRY_TEXTAREA: + return CreateComboBoxEntryTextareaWidget(); + case MOZ_GTK_COMBOBOX_ENTRY_BUTTON: + return CreateComboBoxEntryButtonWidget(); + case MOZ_GTK_COMBOBOX_ENTRY_ARROW: + return CreateComboBoxEntryArrowWidget(); + case MOZ_GTK_HEADERBAR_WINDOW: + case MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED: + case MOZ_GTK_HEADER_BAR: + case MOZ_GTK_HEADER_BAR_MAXIMIZED: + case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE: + case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE: + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE: + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE: + /* Create header bar widgets once and fill with child elements as we need + the header bar fully configured to get a correct style */ + CreateHeaderBar(); + return sWidgetStorage[aAppearance]; + default: + /* Not implemented */ + return nullptr; + } +} + +GtkWidget* GetWidget(WidgetNodeType aAppearance) { + GtkWidget* widget = sWidgetStorage[aAppearance]; + if (!widget) { + widget = CreateWidget(aAppearance); + // Some widgets (MOZ_GTK_COMBOBOX_SEPARATOR for instance) may not be + // available or implemented. + if (!widget) { + return nullptr; + } + // In GTK versions prior to 3.18, automatic invalidation of style contexts + // for widgets was delayed until the next resize event. Gecko however, + // typically uses the style context before the resize event runs and so an + // explicit invalidation may be required. This is necessary if a style + // property was retrieved before all changes were made to the style + // context. One such situation is where gtk_button_construct_child() + // retrieves the style property "image-spacing" during construction of the + // GtkButton, before its parent is set to provide inheritance of ancestor + // properties. More recent GTK versions do not need this, but do not + // re-resolve until required and so invalidation does not trigger + // unnecessary resolution in general. + GtkStyleContext* style = gtk_widget_get_style_context(widget); + gtk_style_context_invalidate(style); + + sWidgetStorage[aAppearance] = widget; + } + return widget; +} + +static void AddStyleClassesFromStyle(GtkStyleContext* aDest, + GtkStyleContext* aSrc) { + GList* classes = gtk_style_context_list_classes(aSrc); + for (GList* link = classes; link; link = link->next) { + gtk_style_context_add_class(aDest, static_cast<gchar*>(link->data)); + } + g_list_free(classes); +} + +GtkStyleContext* CreateStyleForWidget(GtkWidget* aWidget, + GtkStyleContext* aParentStyle) { + static auto sGtkWidgetClassGetCSSName = + reinterpret_cast<const char* (*)(GtkWidgetClass*)>( + dlsym(RTLD_DEFAULT, "gtk_widget_class_get_css_name")); + + GtkWidgetClass* widgetClass = GTK_WIDGET_GET_CLASS(aWidget); + const gchar* name = sGtkWidgetClassGetCSSName + ? sGtkWidgetClassGetCSSName(widgetClass) + : nullptr; + + GtkStyleContext* context = + CreateCSSNode(name, aParentStyle, G_TYPE_FROM_CLASS(widgetClass)); + + // Classes are stored on the style context instead of the path so that any + // future gtk_style_context_save() will inherit classes on the head CSS + // node, in the same way as happens when called on a style context owned by + // a widget. + // + // Classes can be stored on a GtkCssNodeDeclaration and/or the path. + // gtk_style_context_save() reuses the GtkCssNodeDeclaration, and appends a + // new object to the path, without copying the classes from the old path + // head. The new head picks up classes from the GtkCssNodeDeclaration, but + // not the path. GtkWidgets store their classes on the + // GtkCssNodeDeclaration, so make sure to add classes there. + // + // Picking up classes from the style context also means that + // https://bugzilla.gnome.org/show_bug.cgi?id=767312, which can stop + // gtk_widget_path_append_for_widget() from finding classes in GTK 3.20, + // is not a problem. + GtkStyleContext* widgetStyle = gtk_widget_get_style_context(aWidget); + AddStyleClassesFromStyle(context, widgetStyle); + + // Release any floating reference on aWidget. + g_object_ref_sink(aWidget); + g_object_unref(aWidget); + + return context; +} + +static GtkStyleContext* CreateStyleForWidget(GtkWidget* aWidget, + WidgetNodeType aParentType) { + return CreateStyleForWidget(aWidget, GetWidgetRootStyle(aParentType)); +} + +GtkStyleContext* CreateCSSNode(const char* aName, GtkStyleContext* aParentStyle, + GType aType) { + static auto sGtkWidgetPathIterSetObjectName = + reinterpret_cast<void (*)(GtkWidgetPath*, gint, const char*)>( + dlsym(RTLD_DEFAULT, "gtk_widget_path_iter_set_object_name")); + + GtkWidgetPath* path; + if (aParentStyle) { + path = gtk_widget_path_copy(gtk_style_context_get_path(aParentStyle)); + // Copy classes from the parent style context to its corresponding node in + // the path, because GTK will only match against ancestor classes if they + // are on the path. + GList* classes = gtk_style_context_list_classes(aParentStyle); + for (GList* link = classes; link; link = link->next) { + gtk_widget_path_iter_add_class(path, -1, static_cast<gchar*>(link->data)); + } + g_list_free(classes); + } else { + path = gtk_widget_path_new(); + } + + gtk_widget_path_append_type(path, aType); + + if (sGtkWidgetPathIterSetObjectName) { + (*sGtkWidgetPathIterSetObjectName)(path, -1, aName); + } + + GtkStyleContext* context = gtk_style_context_new(); + gtk_style_context_set_path(context, path); + gtk_style_context_set_parent(context, aParentStyle); + gtk_widget_path_unref(path); + + // In GTK 3.4, gtk_render_* functions use |theming_engine| on the style + // context without ensuring any style resolution sets it appropriately + // in style_data_lookup(). e.g. + // https://git.gnome.org/browse/gtk+/tree/gtk/gtkstylecontext.c?h=3.4.4#n3847 + // + // That can result in incorrect drawing on first draw. To work around this, + // force a style look-up to set |theming_engine|. It is sufficient to do + // this only on context creation, instead of after every modification to the + // context, because themes typically (Ambiance and oxygen-gtk, at least) set + // the "engine" property with the '*' selector. + if (GTK_MAJOR_VERSION == 3 && gtk_get_minor_version() < 6) { + GdkRGBA unused; + gtk_style_context_get_color(context, GTK_STATE_FLAG_NORMAL, &unused); + } + + return context; +} + +// Return a style context matching that of the root CSS node of a widget. +// This is used by all GTK versions. +static GtkStyleContext* GetWidgetRootStyle(WidgetNodeType aNodeType) { + GtkStyleContext* style = sStyleStorage[aNodeType]; + if (style) return style; + + switch (aNodeType) { + case MOZ_GTK_MENUBARITEM: + style = CreateStyleForWidget(gtk_menu_item_new(), MOZ_GTK_MENUBAR); + break; + case MOZ_GTK_MENUITEM: + style = CreateStyleForWidget(gtk_menu_item_new(), MOZ_GTK_MENUPOPUP); + break; + case MOZ_GTK_CHECKMENUITEM: + style = + CreateStyleForWidget(gtk_check_menu_item_new(), MOZ_GTK_MENUPOPUP); + break; + case MOZ_GTK_RADIOMENUITEM: + style = CreateStyleForWidget(gtk_radio_menu_item_new(nullptr), + MOZ_GTK_MENUPOPUP); + break; + case MOZ_GTK_TEXT_VIEW: + style = + CreateStyleForWidget(gtk_text_view_new(), MOZ_GTK_SCROLLED_WINDOW); + break; + case MOZ_GTK_TOOLTIP: + if (gtk_check_version(3, 20, 0) != nullptr) { + // The tooltip style class is added first in CreateTooltipWidget() + // and transfered to style in CreateStyleForWidget(). + GtkWidget* tooltipWindow = CreateTooltipWidget(); + style = CreateStyleForWidget(tooltipWindow, nullptr); + gtk_widget_destroy(tooltipWindow); // Release GtkWindow self-reference. + } else { + // We create this from the path because GtkTooltipWindow is not public. + style = CreateCSSNode("tooltip", nullptr, GTK_TYPE_TOOLTIP); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_BACKGROUND); + } + break; + case MOZ_GTK_TOOLTIP_BOX: + style = CreateStyleForWidget(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0), + MOZ_GTK_TOOLTIP); + break; + case MOZ_GTK_TOOLTIP_BOX_LABEL: + style = CreateStyleForWidget(gtk_label_new(nullptr), MOZ_GTK_TOOLTIP_BOX); + break; + default: + GtkWidget* widget = GetWidget(aNodeType); + MOZ_ASSERT(widget); + return gtk_widget_get_style_context(widget); + } + + MOZ_ASSERT(style); + sStyleStorage[aNodeType] = style; + return style; +} + +static GtkStyleContext* CreateChildCSSNode(const char* aName, + WidgetNodeType aParentNodeType) { + return CreateCSSNode(aName, GetCssNodeStyleInternal(aParentNodeType)); +} + +// Create a style context equivalent to a saved root style context of +// |aAppearance| with |aStyleClass| as an additional class. This is used to +// produce a context equivalent to what GTK versions < 3.20 use for many +// internal parts of widgets. +static GtkStyleContext* CreateSubStyleWithClass(WidgetNodeType aAppearance, + const gchar* aStyleClass) { + static auto sGtkWidgetPathIterGetObjectName = + reinterpret_cast<const char* (*)(const GtkWidgetPath*, gint)>( + dlsym(RTLD_DEFAULT, "gtk_widget_path_iter_get_object_name")); + + GtkStyleContext* parentStyle = GetWidgetRootStyle(aAppearance); + + // Create a new context that behaves like |parentStyle| would after + // gtk_style_context_save(parentStyle). + // + // Avoiding gtk_style_context_save() avoids the need to manage the + // restore, and a new context permits caching style resolution. + // + // gtk_style_context_save(context) changes the node hierarchy of |context| + // to add a new GtkCssNodeDeclaration that is a copy of its original node. + // The new node is a child of the original node, and so the new heirarchy is + // one level deeper. The new node receives the same classes as the + // original, but any changes to the classes on |context| will change only + // the new node. The new node inherits properties from the original node + // (which retains the original heirarchy and classes) and matches CSS rules + // with the new heirarchy and any changes to the classes. + // + // The change in hierarchy can produce some surprises in matching theme CSS + // rules (e.g. https://bugzilla.gnome.org/show_bug.cgi?id=761870#c2), but it + // is important here to produce the same behavior so that rules match the + // same widget parts in Gecko as they do in GTK. + // + // When using public GTK API to construct style contexts, a widget path is + // required. CSS rules are not matched against the style context heirarchy + // but according to the heirarchy in the widget path. The path that matches + // the same CSS rules as a saved context is like the path of |parentStyle| + // but with an extra copy of the head (last) object appended. Setting + // |parentStyle| as the parent context provides the same inheritance of + // properties from the widget root node. + const GtkWidgetPath* parentPath = gtk_style_context_get_path(parentStyle); + const gchar* name = sGtkWidgetPathIterGetObjectName + ? sGtkWidgetPathIterGetObjectName(parentPath, -1) + : nullptr; + GType objectType = gtk_widget_path_get_object_type(parentPath); + + GtkStyleContext* style = CreateCSSNode(name, parentStyle, objectType); + + // Start with the same classes on the new node as were on |parentStyle|. + // GTK puts no regions or junction_sides on widget root nodes, and so there + // is no need to copy these. + AddStyleClassesFromStyle(style, parentStyle); + + gtk_style_context_add_class(style, aStyleClass); + return style; +} + +/* GetCssNodeStyleInternal is used by Gtk >= 3.20 */ +static GtkStyleContext* GetCssNodeStyleInternal(WidgetNodeType aNodeType) { + GtkStyleContext* style = sStyleStorage[aNodeType]; + if (style) return style; + + switch (aNodeType) { + case MOZ_GTK_SCROLLBAR_CONTENTS_HORIZONTAL: + style = CreateChildCSSNode("contents", MOZ_GTK_SCROLLBAR_HORIZONTAL); + break; + case MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH, + MOZ_GTK_SCROLLBAR_CONTENTS_HORIZONTAL); + break; + case MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER, + MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL); + break; + case MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL: + style = CreateChildCSSNode("contents", MOZ_GTK_SCROLLBAR_VERTICAL); + break; + case MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH, + MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL); + break; + case MOZ_GTK_SCROLLBAR_THUMB_VERTICAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER, + MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL); + break; + case MOZ_GTK_SCROLLBAR_BUTTON: + style = CreateChildCSSNode(GTK_STYLE_CLASS_BUTTON, + MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL); + break; + case MOZ_GTK_RADIOBUTTON: + style = CreateChildCSSNode(GTK_STYLE_CLASS_RADIO, + MOZ_GTK_RADIOBUTTON_CONTAINER); + break; + case MOZ_GTK_CHECKBUTTON: + style = CreateChildCSSNode(GTK_STYLE_CLASS_CHECK, + MOZ_GTK_CHECKBUTTON_CONTAINER); + break; + case MOZ_GTK_RADIOMENUITEM_INDICATOR: + style = CreateChildCSSNode(GTK_STYLE_CLASS_RADIO, MOZ_GTK_RADIOMENUITEM); + break; + case MOZ_GTK_CHECKMENUITEM_INDICATOR: + style = CreateChildCSSNode(GTK_STYLE_CLASS_CHECK, MOZ_GTK_CHECKMENUITEM); + break; + case MOZ_GTK_PROGRESS_TROUGH: + /* Progress bar background (trough) */ + style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH, MOZ_GTK_PROGRESSBAR); + break; + case MOZ_GTK_PROGRESS_CHUNK: + style = CreateChildCSSNode("progress", MOZ_GTK_PROGRESS_TROUGH); + break; + case MOZ_GTK_GRIPPER: + // TODO - create from CSS node + style = CreateSubStyleWithClass(MOZ_GTK_GRIPPER, GTK_STYLE_CLASS_GRIP); + break; + case MOZ_GTK_INFO_BAR: + // TODO - create from CSS node + style = CreateSubStyleWithClass(MOZ_GTK_INFO_BAR, GTK_STYLE_CLASS_INFO); + break; + case MOZ_GTK_SPINBUTTON_ENTRY: + // TODO - create from CSS node + style = + CreateSubStyleWithClass(MOZ_GTK_SPINBUTTON, GTK_STYLE_CLASS_ENTRY); + break; + case MOZ_GTK_SCROLLED_WINDOW: + // TODO - create from CSS node + style = CreateSubStyleWithClass(MOZ_GTK_SCROLLED_WINDOW, + GTK_STYLE_CLASS_FRAME); + break; + case MOZ_GTK_TEXT_VIEW_TEXT_SELECTION: + style = CreateChildCSSNode("selection", MOZ_GTK_TEXT_VIEW_TEXT); + break; + case MOZ_GTK_TEXT_VIEW_TEXT: + case MOZ_GTK_RESIZER: + style = CreateChildCSSNode("text", MOZ_GTK_TEXT_VIEW); + if (aNodeType == MOZ_GTK_RESIZER) { + // The "grip" class provides the correct builtin icon from + // gtk_render_handle(). The icon is drawn with shaded variants of + // the background color, and so a transparent background would lead to + // a transparent resizer. gtk_render_handle() also uses the + // background color to draw a background, and so this style otherwise + // matches what is used in GtkTextView to match the background with + // textarea elements. + GdkRGBA color; + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, + &color); + if (color.alpha == 0.0) { + g_object_unref(style); + style = CreateStyleForWidget(gtk_text_view_new(), + MOZ_GTK_SCROLLED_WINDOW); + } + gtk_style_context_add_class(style, GTK_STYLE_CLASS_GRIP); + } + break; + case MOZ_GTK_FRAME_BORDER: + style = CreateChildCSSNode("border", MOZ_GTK_FRAME); + break; + case MOZ_GTK_TREEVIEW_VIEW: + // TODO - create from CSS node + style = CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_VIEW); + break; + case MOZ_GTK_TREEVIEW_EXPANDER: + // TODO - create from CSS node + style = + CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_EXPANDER); + break; + case MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL: + style = CreateChildCSSNode("separator", MOZ_GTK_SPLITTER_HORIZONTAL); + break; + case MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL: + style = CreateChildCSSNode("separator", MOZ_GTK_SPLITTER_VERTICAL); + break; + case MOZ_GTK_SCALE_CONTENTS_HORIZONTAL: + style = CreateChildCSSNode("contents", MOZ_GTK_SCALE_HORIZONTAL); + break; + case MOZ_GTK_SCALE_CONTENTS_VERTICAL: + style = CreateChildCSSNode("contents", MOZ_GTK_SCALE_VERTICAL); + break; + case MOZ_GTK_SCALE_TROUGH_HORIZONTAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH, + MOZ_GTK_SCALE_CONTENTS_HORIZONTAL); + break; + case MOZ_GTK_SCALE_TROUGH_VERTICAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH, + MOZ_GTK_SCALE_CONTENTS_VERTICAL); + break; + case MOZ_GTK_SCALE_THUMB_HORIZONTAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER, + MOZ_GTK_SCALE_TROUGH_HORIZONTAL); + break; + case MOZ_GTK_SCALE_THUMB_VERTICAL: + style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER, + MOZ_GTK_SCALE_TROUGH_VERTICAL); + break; + case MOZ_GTK_TAB_TOP: { + // TODO - create from CSS node + style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_TOP); + gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB, + static_cast<GtkRegionFlags>(0)); + break; + } + case MOZ_GTK_TAB_BOTTOM: { + // TODO - create from CSS node + style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_BOTTOM); + gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB, + static_cast<GtkRegionFlags>(0)); + break; + } + case MOZ_GTK_NOTEBOOK: + case MOZ_GTK_NOTEBOOK_HEADER: + case MOZ_GTK_TABPANELS: + case MOZ_GTK_TAB_SCROLLARROW: { + // TODO - create from CSS node + GtkWidget* widget = GetWidget(MOZ_GTK_NOTEBOOK); + return gtk_widget_get_style_context(widget); + } + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE: { + NS_ASSERTION( + false, "MOZ_GTK_HEADER_BAR_BUTTON_RESTORE is used as an icon only!"); + return nullptr; + } + case MOZ_GTK_WINDOW_DECORATION: { + GtkStyleContext* parentStyle = + CreateSubStyleWithClass(MOZ_GTK_WINDOW, "csd"); + style = CreateCSSNode("decoration", parentStyle); + g_object_unref(parentStyle); + break; + } + case MOZ_GTK_WINDOW_DECORATION_SOLID: { + GtkStyleContext* parentStyle = + CreateSubStyleWithClass(MOZ_GTK_WINDOW, "solid-csd"); + style = CreateCSSNode("decoration", parentStyle); + g_object_unref(parentStyle); + break; + } + default: + return GetWidgetRootStyle(aNodeType); + } + + MOZ_ASSERT(style, "missing style context for node type"); + sStyleStorage[aNodeType] = style; + return style; +} + +/* GetWidgetStyleInternal is used by Gtk < 3.20 */ +static GtkStyleContext* GetWidgetStyleInternal(WidgetNodeType aNodeType) { + GtkStyleContext* style = sStyleStorage[aNodeType]; + if (style) return style; + + switch (aNodeType) { + case MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCROLLBAR_HORIZONTAL, + GTK_STYLE_CLASS_TROUGH); + break; + case MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCROLLBAR_HORIZONTAL, + GTK_STYLE_CLASS_SLIDER); + break; + case MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCROLLBAR_VERTICAL, + GTK_STYLE_CLASS_TROUGH); + break; + case MOZ_GTK_SCROLLBAR_THUMB_VERTICAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCROLLBAR_VERTICAL, + GTK_STYLE_CLASS_SLIDER); + break; + case MOZ_GTK_RADIOBUTTON: + style = CreateSubStyleWithClass(MOZ_GTK_RADIOBUTTON_CONTAINER, + GTK_STYLE_CLASS_RADIO); + break; + case MOZ_GTK_CHECKBUTTON: + style = CreateSubStyleWithClass(MOZ_GTK_CHECKBUTTON_CONTAINER, + GTK_STYLE_CLASS_CHECK); + break; + case MOZ_GTK_RADIOMENUITEM_INDICATOR: + style = + CreateSubStyleWithClass(MOZ_GTK_RADIOMENUITEM, GTK_STYLE_CLASS_RADIO); + break; + case MOZ_GTK_CHECKMENUITEM_INDICATOR: + style = + CreateSubStyleWithClass(MOZ_GTK_CHECKMENUITEM, GTK_STYLE_CLASS_CHECK); + break; + case MOZ_GTK_PROGRESS_TROUGH: + style = + CreateSubStyleWithClass(MOZ_GTK_PROGRESSBAR, GTK_STYLE_CLASS_TROUGH); + break; + case MOZ_GTK_PROGRESS_CHUNK: + style = CreateSubStyleWithClass(MOZ_GTK_PROGRESSBAR, + GTK_STYLE_CLASS_PROGRESSBAR); + gtk_style_context_remove_class(style, GTK_STYLE_CLASS_TROUGH); + break; + case MOZ_GTK_GRIPPER: + style = CreateSubStyleWithClass(MOZ_GTK_GRIPPER, GTK_STYLE_CLASS_GRIP); + break; + case MOZ_GTK_INFO_BAR: + style = CreateSubStyleWithClass(MOZ_GTK_INFO_BAR, GTK_STYLE_CLASS_INFO); + break; + case MOZ_GTK_SPINBUTTON_ENTRY: + style = + CreateSubStyleWithClass(MOZ_GTK_SPINBUTTON, GTK_STYLE_CLASS_ENTRY); + break; + case MOZ_GTK_SCROLLED_WINDOW: + style = CreateSubStyleWithClass(MOZ_GTK_SCROLLED_WINDOW, + GTK_STYLE_CLASS_FRAME); + break; + case MOZ_GTK_TEXT_VIEW_TEXT: + case MOZ_GTK_RESIZER: + // GTK versions prior to 3.20 do not have the view class on the root + // node, but add this to determine the background for the text window. + style = CreateSubStyleWithClass(MOZ_GTK_TEXT_VIEW, GTK_STYLE_CLASS_VIEW); + if (aNodeType == MOZ_GTK_RESIZER) { + // The "grip" class provides the correct builtin icon from + // gtk_render_handle(). The icon is drawn with shaded variants of + // the background color, and so a transparent background would lead to + // a transparent resizer. gtk_render_handle() also uses the + // background color to draw a background, and so this style otherwise + // matches MOZ_GTK_TEXT_VIEW_TEXT to match the background with + // textarea elements. GtkTextView creates a separate text window and + // so the background should not be transparent. + gtk_style_context_add_class(style, GTK_STYLE_CLASS_GRIP); + } + break; + case MOZ_GTK_FRAME_BORDER: + return GetWidgetRootStyle(MOZ_GTK_FRAME); + case MOZ_GTK_TREEVIEW_VIEW: + style = CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_VIEW); + break; + case MOZ_GTK_TREEVIEW_EXPANDER: + style = + CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_EXPANDER); + break; + case MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL: + style = CreateSubStyleWithClass(MOZ_GTK_SPLITTER_HORIZONTAL, + GTK_STYLE_CLASS_PANE_SEPARATOR); + break; + case MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL: + style = CreateSubStyleWithClass(MOZ_GTK_SPLITTER_VERTICAL, + GTK_STYLE_CLASS_PANE_SEPARATOR); + break; + case MOZ_GTK_SCALE_TROUGH_HORIZONTAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCALE_HORIZONTAL, + GTK_STYLE_CLASS_TROUGH); + break; + case MOZ_GTK_SCALE_TROUGH_VERTICAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCALE_VERTICAL, + GTK_STYLE_CLASS_TROUGH); + break; + case MOZ_GTK_SCALE_THUMB_HORIZONTAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCALE_HORIZONTAL, + GTK_STYLE_CLASS_SLIDER); + break; + case MOZ_GTK_SCALE_THUMB_VERTICAL: + style = CreateSubStyleWithClass(MOZ_GTK_SCALE_VERTICAL, + GTK_STYLE_CLASS_SLIDER); + break; + case MOZ_GTK_TAB_TOP: + style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_TOP); + gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB, + static_cast<GtkRegionFlags>(0)); + break; + case MOZ_GTK_TAB_BOTTOM: + style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_BOTTOM); + gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB, + static_cast<GtkRegionFlags>(0)); + break; + case MOZ_GTK_NOTEBOOK: + case MOZ_GTK_NOTEBOOK_HEADER: + case MOZ_GTK_TABPANELS: + case MOZ_GTK_TAB_SCROLLARROW: { + GtkWidget* widget = GetWidget(MOZ_GTK_NOTEBOOK); + return gtk_widget_get_style_context(widget); + } + default: + return GetWidgetRootStyle(aNodeType); + } + + MOZ_ASSERT(style); + sStyleStorage[aNodeType] = style; + return style; +} + +void ResetWidgetCache(void) { + for (int i = 0; i < MOZ_GTK_WIDGET_NODE_COUNT; i++) { + if (sStyleStorage[i]) g_object_unref(sStyleStorage[i]); + } + mozilla::PodArrayZero(sStyleStorage); + + /* This will destroy all of our widgets */ + if (sWidgetStorage[MOZ_GTK_WINDOW]) { + gtk_widget_destroy(sWidgetStorage[MOZ_GTK_WINDOW]); + } + if (sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW]) { + gtk_widget_destroy(sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW]); + } + if (sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED]) { + gtk_widget_destroy(sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED]); + } + + /* Clear already freed arrays */ + mozilla::PodArrayZero(sWidgetStorage); +} + +GtkStyleContext* GetStyleContext(WidgetNodeType aNodeType, int aScale, + GtkTextDirection aDirection, + GtkStateFlags aStateFlags) { + GtkStyleContext* style; + if (gtk_check_version(3, 20, 0) != nullptr) { + style = GetWidgetStyleInternal(aNodeType); + } else { + style = GetCssNodeStyleInternal(aNodeType); + StyleContextSetScale(style, aScale); + } + bool stateChanged = false; + bool stateHasDirection = gtk_get_minor_version() >= 8; + GtkStateFlags oldState = gtk_style_context_get_state(style); + MOZ_ASSERT(!(aStateFlags & (STATE_FLAG_DIR_LTR | STATE_FLAG_DIR_RTL))); + unsigned newState = aStateFlags; + if (stateHasDirection) { + switch (aDirection) { + case GTK_TEXT_DIR_LTR: + newState |= STATE_FLAG_DIR_LTR; + break; + case GTK_TEXT_DIR_RTL: + newState |= STATE_FLAG_DIR_RTL; + break; + default: + MOZ_FALLTHROUGH_ASSERT("Bad GtkTextDirection"); + case GTK_TEXT_DIR_NONE: + // GtkWidget uses a default direction if neither is explicitly + // specified, but here DIR_NONE is interpreted as meaning the + // direction is not important, so don't change the direction + // unnecessarily. + newState |= oldState & (STATE_FLAG_DIR_LTR | STATE_FLAG_DIR_RTL); + } + } else if (aDirection != GTK_TEXT_DIR_NONE) { + GtkTextDirection oldDirection = gtk_style_context_get_direction(style); + if (aDirection != oldDirection) { + gtk_style_context_set_direction(style, aDirection); + stateChanged = true; + } + } + if (oldState != newState) { + gtk_style_context_set_state(style, static_cast<GtkStateFlags>(newState)); + stateChanged = true; + } + // This invalidate is necessary for unsaved style contexts from GtkWidgets + // in pre-3.18 GTK, because automatic invalidation of such contexts + // was delayed until a resize event runs. + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=1272194#c7 + // + // Avoid calling invalidate on contexts that are not owned and constructed + // by widgets to avoid performing build_properties() (in 3.16 stylecontext.c) + // unnecessarily early. + if (stateChanged && sWidgetStorage[aNodeType]) { + gtk_style_context_invalidate(style); + } + return style; +} + +GtkStyleContext* CreateStyleContextWithStates(WidgetNodeType aNodeType, + int aScale, + GtkTextDirection aDirection, + GtkStateFlags aStateFlags) { + GtkStyleContext* style = + GetStyleContext(aNodeType, aScale, aDirection, aStateFlags); + GtkWidgetPath* path = gtk_widget_path_copy(gtk_style_context_get_path(style)); + + static auto sGtkWidgetPathIterGetState = + (GtkStateFlags(*)(const GtkWidgetPath*, gint))dlsym( + RTLD_DEFAULT, "gtk_widget_path_iter_get_state"); + static auto sGtkWidgetPathIterSetState = + (void (*)(GtkWidgetPath*, gint, GtkStateFlags))dlsym( + RTLD_DEFAULT, "gtk_widget_path_iter_set_state"); + + int pathLength = gtk_widget_path_length(path); + for (int i = 0; i < pathLength; i++) { + unsigned state = aStateFlags | sGtkWidgetPathIterGetState(path, i); + sGtkWidgetPathIterSetState(path, i, GtkStateFlags(state)); + } + + style = gtk_style_context_new(); + gtk_style_context_set_path(style, path); + gtk_widget_path_unref(path); + + return style; +} + +void StyleContextSetScale(GtkStyleContext* style, gint aScaleFactor) { + // Support HiDPI styles on Gtk 3.20+ + static auto sGtkStyleContextSetScalePtr = + (void (*)(GtkStyleContext*, gint))dlsym(RTLD_DEFAULT, + "gtk_style_context_set_scale"); + if (sGtkStyleContextSetScalePtr && style) { + sGtkStyleContextSetScalePtr(style, aScaleFactor); + } +} diff --git a/widget/gtk/WidgetStyleCache.h b/widget/gtk/WidgetStyleCache.h new file mode 100644 index 0000000000..13fa53d4b4 --- /dev/null +++ b/widget/gtk/WidgetStyleCache.h @@ -0,0 +1,59 @@ +/* -*- 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 WidgetStyleCache_h +#define WidgetStyleCache_h + +#include <gtk/gtk.h> +#include "gtkdrawing.h" + +GtkWidget* GetWidget(WidgetNodeType aNodeType); + +cairo_surface_t* GetWidgetIconSurface(GtkWidget* aWidgetIcon, int aScale); + +/* + * Return a new style context based on aWidget, as a child of aParentStyle. + * If aWidget still has a floating reference, then it is sunk and released. + */ +GtkStyleContext* CreateStyleForWidget(GtkWidget* aWidget, + GtkStyleContext* aParentStyle); + +GtkStyleContext* CreateCSSNode(const char* aName, GtkStyleContext* aParentStyle, + GType aType = G_TYPE_NONE); + +/* + * Returns a pointer to a style context for the specified node and state. + * aStateFlags is applied only to last widget in css style path, + * for instance GetStyleContext(MOZ_GTK_BUTTON, .., GTK_STATE_FLAG_HOVER) + * you get "window button:hover" css selector. + * If you want aStateFlags applied to all path elements use + * CreateStyleContextWithStates(). + * + * The context is owned by WidgetStyleCache. Do not unref. + */ +GtkStyleContext* GetStyleContext( + WidgetNodeType aNodeType, int aScale = 1, + GtkTextDirection aDirection = GTK_TEXT_DIR_NONE, + GtkStateFlags aStateFlags = GTK_STATE_FLAG_NORMAL); + +/* + * Returns a pointer to a style context for the specified node + * and state applied to all elements at widget style path. + * + * The context is owned by caller and must be released by g_object_unref(). + */ +GtkStyleContext* CreateStyleContextWithStates( + WidgetNodeType aNodeType, int aScale = 1, + GtkTextDirection aDirection = GTK_TEXT_DIR_NONE, + GtkStateFlags aStateFlags = GTK_STATE_FLAG_NORMAL); + +void ResetWidgetCache(void); + +bool IsSolidCSDStyleUsed(); + +void StyleContextSetScale(GtkStyleContext* style, gint aScaleFactor); + +#endif // WidgetStyleCache_h diff --git a/widget/gtk/WidgetTraceEvent.cpp b/widget/gtk/WidgetTraceEvent.cpp new file mode 100644 index 0000000000..161e5302cc --- /dev/null +++ b/widget/gtk/WidgetTraceEvent.cpp @@ -0,0 +1,68 @@ +/* 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/WidgetTraceEvent.h" + +#include <glib.h> +#include <mozilla/CondVar.h> +#include <mozilla/Mutex.h> +#include <stdio.h> + +using mozilla::CondVar; +using mozilla::Mutex; +using mozilla::MutexAutoLock; + +namespace { + +Mutex* sMutex = nullptr; +CondVar* sCondVar = nullptr; +bool sTracerProcessed = false; + +// This function is called from the main (UI) thread. +gboolean TracerCallback(gpointer data) { + mozilla::SignalTracerThread(); + return FALSE; +} + +} // namespace + +namespace mozilla { + +bool InitWidgetTracing() { + sMutex = new Mutex("Event tracer thread mutex"); + sCondVar = new CondVar(*sMutex, "Event tracer thread condvar"); + return true; +} + +void CleanUpWidgetTracing() { + delete sMutex; + delete sCondVar; + sMutex = nullptr; + sCondVar = nullptr; +} + +// This function is called from the background tracer thread. +bool FireAndWaitForTracerEvent() { + MOZ_ASSERT(sMutex && sCondVar, "Tracing not initialized!"); + + // Send a default-priority idle event through the + // event loop, and wait for it to finish. + MutexAutoLock lock(*sMutex); + MOZ_ASSERT(!sTracerProcessed, "Tracer synchronization state is wrong"); + g_idle_add_full(G_PRIORITY_DEFAULT, TracerCallback, nullptr, nullptr); + while (!sTracerProcessed) sCondVar->Wait(); + sTracerProcessed = false; + return true; +} + +void SignalTracerThread() { + if (!sMutex || !sCondVar) return; + MutexAutoLock lock(*sMutex); + if (!sTracerProcessed) { + sTracerProcessed = true; + sCondVar->Notify(); + } +} + +} // namespace mozilla diff --git a/widget/gtk/WidgetUtilsGtk.cpp b/widget/gtk/WidgetUtilsGtk.cpp new file mode 100644 index 0000000000..a0299bdf14 --- /dev/null +++ b/widget/gtk/WidgetUtilsGtk.cpp @@ -0,0 +1,52 @@ +/* -*- 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 "WidgetUtilsGtk.h" +#include "nsWindow.h" +#include <gtk/gtk.h> + +namespace mozilla { + +namespace widget { + +int32_t WidgetUtilsGTK::IsTouchDeviceSupportPresent() { + int32_t result = 0; + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + return 0; + } + + GdkDeviceManager* manager = gdk_display_get_device_manager(display); + if (!manager) { + return 0; + } + + GList* devices = + gdk_device_manager_list_devices(manager, GDK_DEVICE_TYPE_SLAVE); + GList* list = devices; + + while (devices) { + GdkDevice* device = static_cast<GdkDevice*>(devices->data); + if (gdk_device_get_source(device) == GDK_SOURCE_TOUCHSCREEN) { + result = 1; + break; + } + devices = devices->next; + } + + if (list) { + g_list_free(list); + } + + return result; +} + +bool IsMainWindowTransparent() { + return nsWindow::IsToplevelWindowTransparent(); +} + +} // namespace widget + +} // namespace mozilla diff --git a/widget/gtk/WidgetUtilsGtk.h b/widget/gtk/WidgetUtilsGtk.h new file mode 100644 index 0000000000..5ede9aa520 --- /dev/null +++ b/widget/gtk/WidgetUtilsGtk.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WidgetUtilsGtk_h__ +#define WidgetUtilsGtk_h__ + +#include <stdint.h> + +namespace mozilla { +namespace widget { + +class WidgetUtilsGTK { + public: + /* See WidgetUtils::IsTouchDeviceSupportPresent(). */ + static int32_t IsTouchDeviceSupportPresent(); +}; + +bool IsMainWindowTransparent(); + +} // namespace widget + +} // namespace mozilla + +#endif // WidgetUtilsGtk_h__ diff --git a/widget/gtk/WindowSurfaceProvider.cpp b/widget/gtk/WindowSurfaceProvider.cpp new file mode 100644 index 0000000000..282847a512 --- /dev/null +++ b/widget/gtk/WindowSurfaceProvider.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WindowSurfaceProvider.h" + +#include "gfxPlatformGtk.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/layers/LayersTypes.h" +#include "nsWindow.h" +#include "WindowSurfaceX11Image.h" +#include "WindowSurfaceX11SHM.h" +#include "WindowSurfaceXRender.h" +#ifdef MOZ_WAYLAND +# include "WindowSurfaceWayland.h" +#endif + +namespace mozilla { +namespace widget { + +using namespace mozilla::layers; + +WindowSurfaceProvider::WindowSurfaceProvider() + : mIsX11Display(false), + mXDisplay(nullptr), + mXWindow(0), + mXVisual(nullptr), + mXDepth(0), + mWindowSurface(nullptr) +#ifdef MOZ_WAYLAND + , + mWidget(nullptr) +#endif + , + mIsShaped(false) { +} + +void WindowSurfaceProvider::Initialize(Display* aDisplay, Window aWindow, + Visual* aVisual, int aDepth, + bool aIsShaped) { + // We should not be initialized + MOZ_ASSERT(!mXDisplay); + + // This should also be a valid initialization + MOZ_ASSERT(aDisplay && aWindow != X11None && aVisual); + + mXDisplay = aDisplay; + mXWindow = aWindow; + mXVisual = aVisual; + mXDepth = aDepth; + mIsShaped = aIsShaped; + mIsX11Display = true; +} + +#ifdef MOZ_WAYLAND +void WindowSurfaceProvider::Initialize(nsWindow* aWidget) { + mWidget = aWidget; + mIsX11Display = false; +} +#endif + +void WindowSurfaceProvider::CleanupResources() { mWindowSurface = nullptr; } + +UniquePtr<WindowSurface> WindowSurfaceProvider::CreateWindowSurface() { +#ifdef MOZ_WAYLAND + if (!mIsX11Display) { + LOGDRAW(("Drawing to nsWindow %p will use wl_surface\n", mWidget)); + return MakeUnique<WindowSurfaceWayland>(mWidget); + } +#endif + + // We should be initialized + MOZ_ASSERT(mXDisplay); + + // Blit to the window with the following priority: + // 1. XRender (iff XRender is enabled && we are in-process) + // 2. MIT-SHM + // 3. XPutImage + if (!mIsShaped && gfx::gfxVars::UseXRender()) { + LOGDRAW(("Drawing to Window 0x%lx will use XRender\n", mXWindow)); + return MakeUnique<WindowSurfaceXRender>(mXDisplay, mXWindow, mXVisual, + mXDepth); + } + +#ifdef MOZ_HAVE_SHMIMAGE + if (!mIsShaped && nsShmImage::UseShm()) { + LOGDRAW(("Drawing to Window 0x%lx will use MIT-SHM\n", mXWindow)); + return MakeUnique<WindowSurfaceX11SHM>(mXDisplay, mXWindow, mXVisual, + mXDepth); + } +#endif // MOZ_HAVE_SHMIMAGE + + LOGDRAW(("Drawing to Window 0x%lx will use XPutImage\n", mXWindow)); + return MakeUnique<WindowSurfaceX11Image>(mXDisplay, mXWindow, mXVisual, + mXDepth, mIsShaped); +} + +already_AddRefed<gfx::DrawTarget> +WindowSurfaceProvider::StartRemoteDrawingInRegion( + LayoutDeviceIntRegion& aInvalidRegion, layers::BufferMode* aBufferMode) { + if (aInvalidRegion.IsEmpty()) return nullptr; + + if (!mWindowSurface) { + mWindowSurface = CreateWindowSurface(); + if (!mWindowSurface) return nullptr; + } + + *aBufferMode = BufferMode::BUFFER_NONE; + RefPtr<gfx::DrawTarget> dt = nullptr; + if (!(dt = mWindowSurface->Lock(aInvalidRegion)) && mIsX11Display && + !mWindowSurface->IsFallback()) { + // We can't use WindowSurfaceX11Image fallback on Wayland but + // Lock() call on WindowSurfaceWayland should never fail. + gfxWarningOnce() + << "Failed to lock WindowSurface, falling back to XPutImage backend."; + mWindowSurface = MakeUnique<WindowSurfaceX11Image>( + mXDisplay, mXWindow, mXVisual, mXDepth, mIsShaped); + dt = mWindowSurface->Lock(aInvalidRegion); + } + return dt.forget(); +} + +void WindowSurfaceProvider::EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + if (mWindowSurface) mWindowSurface->Commit(aInvalidRegion); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/WindowSurfaceProvider.h b/widget/gtk/WindowSurfaceProvider.h new file mode 100644 index 0000000000..cbd36fa10b --- /dev/null +++ b/widget/gtk/WindowSurfaceProvider.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H +#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H + +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/layers/LayersTypes.h" +#include "mozilla/widget/WindowSurface.h" +#include "Units.h" + +#include <gdk/gdk.h> +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +#endif +#include <X11/Xlib.h> // for Window, Display, Visual, etc. +#include "X11UndefineNone.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +/* + * Holds the logic for creating WindowSurface's for a GTK nsWindow. + * The main purpose of this class is to allow sharing of logic between + * nsWindow and GtkCompositorWidget, for when OMTC is enabled or disabled. + */ +class WindowSurfaceProvider final { + public: + WindowSurfaceProvider(); + + /** + * Initializes the WindowSurfaceProvider by giving it the window + * handle and display to attach to. WindowSurfaceProvider doesn't + * own the Display, Window, etc, and they must continue to exist + * while WindowSurfaceProvider is used. + */ + void Initialize(Display* aDisplay, Window aWindow, Visual* aVisual, + int aDepth, bool aIsShaped); + +#ifdef MOZ_WAYLAND + void Initialize(nsWindow* aWidget); +#endif + + /** + * Releases any surfaces created by this provider. + * This is used by GtkCompositorWidget to get rid + * of resources before we close the display connection. + */ + void CleanupResources(); + + already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion( + LayoutDeviceIntRegion& aInvalidRegion, layers::BufferMode* aBufferMode); + void EndRemoteDrawingInRegion(gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion); + + private: + UniquePtr<WindowSurface> CreateWindowSurface(); + + // Can we access X? + bool mIsX11Display; + Display* mXDisplay; + Window mXWindow; + Visual* mXVisual; + int mXDepth; + UniquePtr<WindowSurface> mWindowSurface; +#ifdef MOZ_WAYLAND + nsWindow* mWidget; +#endif + bool mIsShaped; +}; + +} // namespace widget +} // namespace mozilla + +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H diff --git a/widget/gtk/WindowSurfaceWayland.cpp b/widget/gtk/WindowSurfaceWayland.cpp new file mode 100644 index 0000000000..2855ff7121 --- /dev/null +++ b/widget/gtk/WindowSurfaceWayland.cpp @@ -0,0 +1,1116 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWaylandDisplay.h" +#include "WindowSurfaceWayland.h" + +#include "nsPrintfCString.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Tools.h" +#include "gfx2DGlue.h" +#include "gfxPlatform.h" +#include "MozContainer.h" +#include "nsTArray.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_widget.h" + +#include <sys/mman.h> +#include <fcntl.h> +#include <errno.h> + +#undef LOG +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetWaylandLog; +# define LOGWAYLAND(args) \ + MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, args) +#else +# define LOGWAYLAND(args) +#endif /* MOZ_LOGGING */ + +// Maximal compositing timeout it miliseconds +#define COMPOSITING_TIMEOUT 200 + +namespace mozilla { +namespace widget { + +/* + Wayland multi-thread rendering scheme + + Every rendering thread (main thread, compositor thread) contains its own + nsWaylandDisplay object connected to Wayland compositor (Mutter, Weston, etc.) + + WindowSurfaceWayland implements WindowSurface class and draws nsWindow by + WindowSurface interface (Lock, Commit) to screen through nsWaylandDisplay. + + ---------------------- + | Wayland compositor | + ---------------------- + ^ + | + ---------------------- + | nsWaylandDisplay | + ---------------------- + ^ ^ + | | + | | + | --------------------------------- ------------------ + | | WindowSurfaceWayland |<------>| nsWindow | + | | | ------------------ + | | ----------------------- | + | | | WindowBackBuffer | | + | | | | | + | | | ------------------- | | + | | | | WaylandShmPool | | | + | | | ------------------- | | + | | ----------------------- | + | | | + | | ----------------------- | + | | | WindowBackBuffer | | + | | | | | + | | | ------------------- | | + | | | | WaylandShmPool | | | + | | | ------------------- | | + | | ----------------------- | + | --------------------------------- + | + | + --------------------------------- ------------------ + | WindowSurfaceWayland |<------>| nsWindow | + | | ------------------ + | ----------------------- | + | | WindowBackBuffer | | + | | | | + | | ------------------- | | + | | | WaylandShmPool | | | + | | ------------------- | | + | ----------------------- | + | | + | ----------------------- | + | | WindowBackBuffer | | + | | | | + | | ------------------- | | + | | | WaylandShmPool | | | + | | ------------------- | | + | ----------------------- | + --------------------------------- + + +nsWaylandDisplay + +Is our connection to Wayland display server, +holds our display connection (wl_display) and event queue (wl_event_queue). + +nsWaylandDisplay is created for every thread which sends data to Wayland +compositor. Wayland events for main thread is served by default Gtk+ loop, +for other threads (compositor) we must create wl_event_queue and run event loop. + + +WindowSurfaceWayland + +Is a Wayland implementation of WindowSurface class for WindowSurfaceProvider, +we implement Lock() and Commit() interfaces from WindowSurface +for actual drawing. + +One WindowSurfaceWayland draws one nsWindow so those are tied 1:1. +At Wayland level it holds one wl_surface object. + +To perform visualiation of nsWindow, WindowSurfaceWayland contains one +wl_surface and two wl_buffer objects (owned by WindowBackBuffer) +as we use double buffering. When nsWindow drawing is finished to wl_buffer, +the wl_buffer is attached to wl_surface and it's sent to Wayland compositor. + +When there's no wl_buffer available for drawing (all wl_buffers are locked in +compositor for instance) we store the drawing to WindowImageSurface object +and draw later when wl_buffer becomes availabe or discard the +WindowImageSurface cache when whole screen is invalidated. + +WindowBackBuffer + +Is a class which provides a wl_buffer for drawing. +Wl_buffer is a main Wayland object with actual graphics data. +Wl_buffer basically represent one complete window screen. +When double buffering is involved every window (GdkWindow for instance) +utilises two wl_buffers which are cycled. One is filed with data by application +and one is rendered by compositor. + +WindowBackBuffer is implemented by shared memory (shm). +It owns wl_buffer object, owns WaylandShmPool +(which provides the shared memory) and ties them together. + +WaylandShmPool + +WaylandShmPool acts as a manager of shared memory for WindowBackBuffer. +Allocates it, holds reference to it and releases it. + +We allocate shared memory (shm) by mmap(..., MAP_SHARED,...) as an interface +between us and wayland compositor. We draw our graphics data to the shm and +handle to wayland compositor by WindowBackBuffer/WindowSurfaceWayland +(wl_buffer/wl_surface). +*/ + +#define EVENT_LOOP_DELAY (1000 / 240) + +#define BUFFER_BPP 4 +gfx::SurfaceFormat WindowBackBuffer::mFormat = gfx::SurfaceFormat::B8G8R8A8; + +int WindowBackBuffer::mDumpSerial = + PR_GetEnv("MOZ_WAYLAND_DUMP_WL_BUFFERS") ? 1 : 0; +char* WindowBackBuffer::mDumpDir = PR_GetEnv("MOZ_WAYLAND_DUMP_DIR"); + +RefPtr<nsWaylandDisplay> WindowBackBuffer::GetWaylandDisplay() { + return mWindowSurfaceWayland->GetWaylandDisplay(); +} + +static int WaylandAllocateShmMemory(int aSize) { + int fd = -1; + do { + static int counter = 0; + nsPrintfCString shmName("/wayland.mozilla.ipc.%d", counter++); + fd = shm_open(shmName.get(), O_CREAT | O_RDWR | O_EXCL, 0600); + if (fd >= 0) { + // We don't want to use leaked file + if (shm_unlink(shmName.get()) != 0) { + NS_WARNING("shm_unlink failed"); + return -1; + } + } + } while (fd < 0 && errno == EEXIST); + + if (fd < 0) { + NS_WARNING(nsPrintfCString("shm_open failed: %s", strerror(errno)).get()); + return -1; + } + + int ret = 0; +#ifdef HAVE_POSIX_FALLOCATE + do { + ret = posix_fallocate(fd, 0, aSize); + } while (ret == EINTR); + if (ret != 0) { + NS_WARNING( + nsPrintfCString("posix_fallocate() fails to allocate shm memory: %s", + strerror(ret)) + .get()); + close(fd); + return -1; + } +#else + do { + ret = ftruncate(fd, aSize); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + NS_WARNING(nsPrintfCString("ftruncate() fails to allocate shm memory: %s", + strerror(ret)) + .get()); + close(fd); + fd = -1; + } +#endif + + return fd; +} + +static bool WaylandReAllocateShmMemory(int aFd, int aSize) { + if (ftruncate(aFd, aSize) < 0) { + return false; + } +#ifdef HAVE_POSIX_FALLOCATE + do { + errno = posix_fallocate(aFd, 0, aSize); + } while (errno == EINTR); + if (errno != 0) { + return false; + } +#endif + return true; +} + +WaylandShmPool::WaylandShmPool() + : mShmPool(nullptr), + mShmPoolFd(-1), + mAllocatedSize(0), + mImageData(MAP_FAILED){}; + +void WaylandShmPool::Release() { + if (mImageData != MAP_FAILED) { + munmap(mImageData, mAllocatedSize); + mImageData = MAP_FAILED; + } + if (mShmPool) { + wl_shm_pool_destroy(mShmPool); + mShmPool = 0; + } + if (mShmPoolFd >= 0) { + close(mShmPoolFd); + mShmPoolFd = -1; + } +} + +bool WaylandShmPool::Create(RefPtr<nsWaylandDisplay> aWaylandDisplay, + int aSize) { + // We do size increase only + if (aSize <= mAllocatedSize) { + return true; + } + + if (mShmPoolFd < 0) { + mShmPoolFd = WaylandAllocateShmMemory(aSize); + if (mShmPoolFd < 0) { + return false; + } + } else { + if (!WaylandReAllocateShmMemory(mShmPoolFd, aSize)) { + Release(); + return false; + } + } + + if (mImageData != MAP_FAILED) { + munmap(mImageData, mAllocatedSize); + } + mImageData = + mmap(nullptr, aSize, PROT_READ | PROT_WRITE, MAP_SHARED, mShmPoolFd, 0); + if (mImageData == MAP_FAILED) { + NS_WARNING("Unable to map drawing surface!"); + Release(); + return false; + } + + if (mShmPool) { + wl_shm_pool_resize(mShmPool, aSize); + } else { + mShmPool = wl_shm_create_pool(aWaylandDisplay->GetShm(), mShmPoolFd, aSize); + // We set our queue to get mShmPool events at compositor thread. + wl_proxy_set_queue((struct wl_proxy*)mShmPool, + aWaylandDisplay->GetEventQueue()); + } + + mAllocatedSize = aSize; + return true; +} + +void WaylandShmPool::SetImageDataFromPool(class WaylandShmPool* aSourcePool, + int aImageDataSize) { + MOZ_ASSERT(mAllocatedSize >= aImageDataSize, "WaylandShmPool overflows!"); + memcpy(mImageData, aSourcePool->GetImageData(), aImageDataSize); +} + +WaylandShmPool::~WaylandShmPool() { Release(); } + +static void buffer_release(void* data, wl_buffer* buffer) { + auto surface = reinterpret_cast<WindowBackBuffer*>(data); + surface->Detach(buffer); +} + +static const struct wl_buffer_listener buffer_listener = {buffer_release}; + +bool WindowBackBuffer::Create(int aWidth, int aHeight) { + MOZ_ASSERT(!IsAttached(), "We can't create attached buffers."); + + ReleaseWLBuffer(); + + int size = aWidth * aHeight * BUFFER_BPP; + if (!mShmPool.Create(GetWaylandDisplay(), size)) { + return false; + } + + mWLBuffer = + wl_shm_pool_create_buffer(mShmPool.GetShmPool(), 0, aWidth, aHeight, + aWidth * BUFFER_BPP, WL_SHM_FORMAT_ARGB8888); + wl_proxy_set_queue((struct wl_proxy*)mWLBuffer, + GetWaylandDisplay()->GetEventQueue()); + wl_buffer_add_listener(mWLBuffer, &buffer_listener, this); + + mWidth = aWidth; + mHeight = aHeight; + + LOGWAYLAND(("WindowBackBuffer::Create [%p] wl_buffer %p ID %d\n", (void*)this, + (void*)mWLBuffer, + mWLBuffer ? wl_proxy_get_id((struct wl_proxy*)mWLBuffer) : -1)); + return true; +} + +void WindowBackBuffer::ReleaseWLBuffer() { + LOGWAYLAND(("WindowBackBuffer::Release [%p]\n", (void*)this)); + if (mWLBuffer) { + wl_buffer_destroy(mWLBuffer); + mWLBuffer = nullptr; + } + mWidth = mHeight = 0; +} + +void WindowBackBuffer::Clear() { + memset(mShmPool.GetImageData(), 0, mHeight * mWidth * BUFFER_BPP); +} + +WindowBackBuffer::WindowBackBuffer(WindowSurfaceWayland* aWindowSurfaceWayland) + : mWindowSurfaceWayland(aWindowSurfaceWayland), + mShmPool(), + mWLBuffer(nullptr), + mWidth(0), + mHeight(0), + mAttached(false) {} + +WindowBackBuffer::~WindowBackBuffer() { ReleaseWLBuffer(); } + +bool WindowBackBuffer::Resize(int aWidth, int aHeight) { + if (aWidth == mWidth && aHeight == mHeight) { + return true; + } + LOGWAYLAND( + ("WindowBackBuffer::Resize [%p] %d %d\n", (void*)this, aWidth, aHeight)); + Create(aWidth, aHeight); + return (mWLBuffer != nullptr); +} + +void WindowBackBuffer::Attach(wl_surface* aSurface) { + LOGWAYLAND( + ("WindowBackBuffer::Attach [%p] wl_surface %p ID %d wl_buffer %p ID %d\n", + (void*)this, (void*)aSurface, + aSurface ? wl_proxy_get_id((struct wl_proxy*)aSurface) : -1, + (void*)GetWlBuffer(), + GetWlBuffer() ? wl_proxy_get_id((struct wl_proxy*)GetWlBuffer()) : -1)); + + wl_buffer* buffer = GetWlBuffer(); + if (buffer) { + mAttached = true; + wl_surface_attach(aSurface, buffer, 0, 0); + wl_surface_commit(aSurface); + wl_display_flush(WaylandDisplayGetWLDisplay()); + } +} + +void WindowBackBuffer::Detach(wl_buffer* aBuffer) { + LOGWAYLAND(("WindowBackBuffer::Detach [%p] wl_buffer %p ID %d\n", (void*)this, + (void*)aBuffer, + aBuffer ? wl_proxy_get_id((struct wl_proxy*)aBuffer) : -1)); + mAttached = false; + + // Commit any potential cached drawings from latest Lock()/Commit() cycle. + mWindowSurfaceWayland->FlushPendingCommits(); +} + +bool WindowBackBuffer::SetImageDataFromBuffer( + class WindowBackBuffer* aSourceBuffer) { + auto sourceBuffer = static_cast<class WindowBackBuffer*>(aSourceBuffer); + if (!IsMatchingSize(sourceBuffer)) { + if (!Resize(sourceBuffer->mWidth, sourceBuffer->mHeight)) { + return false; + } + } + + mShmPool.SetImageDataFromPool( + &sourceBuffer->mShmPool, + sourceBuffer->mWidth * sourceBuffer->mHeight * BUFFER_BPP); + return true; +} + +already_AddRefed<gfx::DrawTarget> WindowBackBuffer::Lock() { + LOGWAYLAND(("WindowBackBuffer::Lock [%p] [%d x %d] wl_buffer %p ID %d\n", + (void*)this, mWidth, mHeight, (void*)mWLBuffer, + mWLBuffer ? wl_proxy_get_id((struct wl_proxy*)mWLBuffer) : -1)); + + gfx::IntSize lockSize(mWidth, mHeight); + mIsLocked = true; + return gfxPlatform::CreateDrawTargetForData( + static_cast<unsigned char*>(mShmPool.GetImageData()), lockSize, + BUFFER_BPP * mWidth, GetSurfaceFormat()); +} + +#ifdef MOZ_LOGGING +void WindowBackBuffer::DumpToFile(const char* aHint) { + if (!mDumpSerial) { + return; + } + + cairo_surface_t* surface = nullptr; + auto unmap = MakeScopeExit([&] { + if (surface) { + cairo_surface_destroy(surface); + } + }); + surface = cairo_image_surface_create_for_data( + (unsigned char*)mShmPool.GetImageData(), CAIRO_FORMAT_ARGB32, mWidth, + mHeight, BUFFER_BPP * mWidth); + if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { + nsCString filename; + if (mDumpDir) { + filename.Append(mDumpDir); + filename.Append('/'); + } + filename.Append( + nsPrintfCString("firefox-wl-buffer-%.5d-%s.png", mDumpSerial++, aHint)); + cairo_surface_write_to_png(surface, filename.get()); + LOGWAYLAND(("Dumped wl_buffer to %s\n", filename.get())); + } +} +#endif + +static void frame_callback_handler(void* data, struct wl_callback* callback, + uint32_t time) { + auto surface = reinterpret_cast<WindowSurfaceWayland*>(data); + surface->FrameCallbackHandler(); +} + +static const struct wl_callback_listener frame_listener = { + frame_callback_handler}; + +WindowSurfaceWayland::WindowSurfaceWayland(nsWindow* aWindow) + : mWindow(aWindow), + mWaylandDisplay(WaylandDisplayGet()), + mWaylandBuffer(nullptr), + mWaylandFullscreenDamage(false), + mFrameCallback(nullptr), + mLastCommittedSurface(nullptr), + mLastCommitTime(0), + mDrawToWaylandBufferDirectly(true), + mCanSwitchWaylandBuffer(true), + mBufferPendingCommit(false), + mBufferCommitAllowed(false), + mBufferNeedsClear(false), + mSmoothRendering(StaticPrefs::widget_wayland_smooth_rendering()), + mSurfaceReadyTimerID(), + mSurfaceLock("WindowSurfaceWayland lock") { + for (int i = 0; i < BACK_BUFFER_NUM; i++) { + mShmBackupBuffer[i] = nullptr; + } +} + +WindowSurfaceWayland::~WindowSurfaceWayland() { + MutexAutoLock lock(mSurfaceLock); + + if (mSurfaceReadyTimerID) { + g_source_remove(mSurfaceReadyTimerID); + mSurfaceReadyTimerID = 0; + } + + if (mBufferPendingCommit) { + NS_WARNING("Deleted WindowSurfaceWayland with a pending commit!"); + } + + if (mFrameCallback) { + wl_callback_destroy(mFrameCallback); + } + + mWaylandBuffer = nullptr; + + for (int i = 0; i < BACK_BUFFER_NUM; i++) { + if (mShmBackupBuffer[i]) { + delete mShmBackupBuffer[i]; + } + } +} + +WindowBackBuffer* WindowSurfaceWayland::CreateWaylandBuffer(int aWidth, + int aHeight) { + int availableBuffer; + for (availableBuffer = 0; availableBuffer < BACK_BUFFER_NUM; + availableBuffer++) { + if (!mShmBackupBuffer[availableBuffer]) { + break; + } + } + + // There isn't any free slot for additional buffer. + if (availableBuffer == BACK_BUFFER_NUM) { + return nullptr; + } + + WindowBackBuffer* buffer = new WindowBackBuffer(this); + if (!buffer->Create(aWidth, aHeight)) { + delete buffer; + return nullptr; + } + + mShmBackupBuffer[availableBuffer] = buffer; + return buffer; +} + +WindowBackBuffer* WindowSurfaceWayland::WaylandBufferFindAvailable( + int aWidth, int aHeight) { + int availableBuffer; + // Try to find a buffer which matches the size + for (availableBuffer = 0; availableBuffer < BACK_BUFFER_NUM; + availableBuffer++) { + WindowBackBuffer* buffer = mShmBackupBuffer[availableBuffer]; + if (buffer && !buffer->IsAttached() && + buffer->IsMatchingSize(aWidth, aHeight)) { + return buffer; + } + } + + // Try to find any buffer + for (availableBuffer = 0; availableBuffer < BACK_BUFFER_NUM; + availableBuffer++) { + WindowBackBuffer* buffer = mShmBackupBuffer[availableBuffer]; + if (buffer && !buffer->IsAttached()) { + return buffer; + } + } + + return nullptr; +} + +WindowBackBuffer* WindowSurfaceWayland::SetNewWaylandBuffer() { + LOGWAYLAND( + ("WindowSurfaceWayland::NewWaylandBuffer [%p] Requested buffer [%d " + "x %d]\n", + (void*)this, mWLBufferRect.width, mWLBufferRect.height)); + + mWaylandBuffer = + WaylandBufferFindAvailable(mWLBufferRect.width, mWLBufferRect.height); + if (mWaylandBuffer) { + if (!mWaylandBuffer->Resize(mWLBufferRect.width, mWLBufferRect.height)) { + return nullptr; + } + return mWaylandBuffer; + } + + mWaylandBuffer = + CreateWaylandBuffer(mWLBufferRect.width, mWLBufferRect.height); + return mWaylandBuffer; +} + +// Recent +WindowBackBuffer* WindowSurfaceWayland::GetWaylandBuffer() { + LOGWAYLAND( + ("WindowSurfaceWayland::GetWaylandBuffer [%p] Requested buffer [%d " + "x %d] can switch %d\n", + (void*)this, mWLBufferRect.width, mWLBufferRect.height, + mCanSwitchWaylandBuffer)); + + // There's no buffer created yet, create a new one for partial screen updates. + if (!mWaylandBuffer) { + return SetNewWaylandBuffer(); + } + + if (mWaylandBuffer->IsAttached()) { + if (mCanSwitchWaylandBuffer) { + return SetNewWaylandBuffer(); + } + LOGWAYLAND((" Buffer is attached and we can't switch, return null\n")); + return nullptr; + } + + if (mWaylandBuffer->IsMatchingSize(mWLBufferRect.width, + mWLBufferRect.height)) { + LOGWAYLAND((" Size is ok, use the buffer [%d x %d]\n", + mWLBufferRect.width, mWLBufferRect.height)); + return mWaylandBuffer; + } + + if (mCanSwitchWaylandBuffer) { + // Reuse existing buffer + LOGWAYLAND((" Reuse buffer with resize [%d x %d]\n", mWLBufferRect.width, + mWLBufferRect.height)); + if (mWaylandBuffer->Resize(mWLBufferRect.width, mWLBufferRect.height)) { + return mWaylandBuffer; + } + // OOM here, just return null to skip this frame. + return nullptr; + } + + LOGWAYLAND( + (" Buffer size does not match, requested %d x %d got %d x%d, return " + "null.\n", + mWaylandBuffer->GetWidth(), mWaylandBuffer->GetHeight(), + mWLBufferRect.width, mWLBufferRect.height)); + return nullptr; +} + +already_AddRefed<gfx::DrawTarget> WindowSurfaceWayland::LockWaylandBuffer() { + // Allocated wayland buffer can't be bigger than mozilla widget size. + LayoutDeviceIntRegion region; + region.And(mLockedScreenRect, mWindow->GetMozContainerSize()); + mWLBufferRect = LayoutDeviceIntRect(region.GetBounds()); + + LOGWAYLAND( + ("WindowSurfaceWayland::LockWaylandBuffer [%p] Requesting buffer %d x " + "%d\n", + (void*)this, mWLBufferRect.width, mWLBufferRect.height)); + + WindowBackBuffer* buffer = GetWaylandBuffer(); + LOGWAYLAND(("WindowSurfaceWayland::LockWaylandBuffer [%p] Got buffer %p\n", + (void*)this, (void*)buffer)); + + if (!buffer) { + if (mLastCommitTime && (g_get_monotonic_time() / 1000) - mLastCommitTime > + COMPOSITING_TIMEOUT) { + NS_WARNING( + "Slow response from Wayland compositor, visual glitches ahead."); + } + return nullptr; + } + + mCanSwitchWaylandBuffer = false; + + if (mBufferNeedsClear) { + buffer->Clear(); + mBufferNeedsClear = false; + } + + return buffer->Lock(); +} + +void WindowSurfaceWayland::UnlockWaylandBuffer() { + LOGWAYLAND(("WindowSurfaceWayland::UnlockWaylandBuffer [%p]\n", (void*)this)); + mWaylandBuffer->Unlock(); +} + +already_AddRefed<gfx::DrawTarget> WindowSurfaceWayland::LockImageSurface( + const gfx::IntSize& aLockSize) { + if (!mImageSurface || !(aLockSize <= mImageSurface->GetSize())) { + mImageSurface = gfx::Factory::CreateDataSourceSurface( + aLockSize, WindowBackBuffer::GetSurfaceFormat()); + } + gfx::DataSourceSurface::MappedSurface map = {nullptr, 0}; + if (!mImageSurface->Map(gfx::DataSourceSurface::READ_WRITE, &map)) { + return nullptr; + } + return gfxPlatform::CreateDrawTargetForData( + map.mData, mImageSurface->GetSize(), map.mStride, + WindowBackBuffer::GetSurfaceFormat()); +} + +static bool IsWindowFullScreenUpdate( + LayoutDeviceIntRect& aScreenRect, + const LayoutDeviceIntRegion& aUpdatedRegion) { + if (aUpdatedRegion.GetNumRects() > 1) return false; + + gfx::IntRect rect = aUpdatedRegion.RectIter().Get().ToUnknownRect(); + return (rect.x == 0 && rect.y == 0 && aScreenRect.width == rect.width && + aScreenRect.height == rect.height); +} + +static bool IsPopupFullScreenUpdate( + LayoutDeviceIntRect& aScreenRect, + const LayoutDeviceIntRegion& aUpdatedRegion) { + // We know that popups can be drawn from two parts; a panel and an arrow. + // Assume we redraw whole popups when we have two rects and bounding + // box is equal to window borders. + if (aUpdatedRegion.GetNumRects() > 2) return false; + + gfx::IntRect lockSize = aUpdatedRegion.GetBounds().ToUnknownRect(); + return (lockSize.x == 0 && lockSize.y == 0 && + aScreenRect.width == lockSize.width && + aScreenRect.height == lockSize.height); +} + +already_AddRefed<gfx::DrawTarget> WindowSurfaceWayland::Lock( + const LayoutDeviceIntRegion& aRegion) { + if (mWindow->WindowType() == eWindowType_invisible) { + return nullptr; + } + + // Wait until all pending events are processed. There may be queued + // wl_buffer release event which releases our wl_buffer for further rendering. + mWaylandDisplay->WaitForSyncEnd(); + + // Lock the surface *after* WaitForSyncEnd() call as is can fire + // FlushPendingCommits(). + MutexAutoLock lock(mSurfaceLock); + + // Disable all commits (from potential frame callback/delayed handlers) + // until next WindowSurfaceWayland::Commit() call. + mBufferCommitAllowed = false; + + LayoutDeviceIntRect lockedScreenRect = mWindow->GetBounds(); + // The window bounds of popup windows contains relative position to + // the transient window. We need to remove that effect because by changing + // position of the popup window the buffer has not changed its size. + lockedScreenRect.x = lockedScreenRect.y = 0; + gfx::IntRect lockSize = aRegion.GetBounds().ToUnknownRect(); + + bool isTransparentPopup = + mWindow->IsWaylandPopup() && + (eTransparencyTransparent == mWindow->GetTransparencyMode()); + + bool windowRedraw = isTransparentPopup + ? IsPopupFullScreenUpdate(lockedScreenRect, aRegion) + : IsWindowFullScreenUpdate(lockedScreenRect, aRegion); + if (windowRedraw) { + // Clear buffer when we (re)draw new transparent popup window, + // otherwise leave it as-is, mBufferNeedsClear can be set from previous + // (already pending) commits which are cached now. + mBufferNeedsClear = + mWindow->WaylandSurfaceNeedsClear() || isTransparentPopup; + + // Store info that we can switch WaylandBuffer when we flush + // mImageSurface / mDelayedImageCommits. Don't clear it - it's cleared + // at LockWaylandBuffer() when we actualy switch the buffer. + mCanSwitchWaylandBuffer = true; + + // We do full buffer repaint so clear our cached drawings. + mDelayedImageCommits.Clear(); + mWaylandBufferDamage.SetEmpty(); + + // Store info that we can safely invalidate whole screen. + mWaylandFullscreenDamage = true; + } else { + // We can switch buffer if there isn't any content committed + // to active buffer. + mCanSwitchWaylandBuffer = !mBufferPendingCommit; + } + + LOGWAYLAND( + ("WindowSurfaceWayland::Lock [%p] [%d,%d] -> [%d x %d] rects %d " + "windowSize [%d x %d]\n", + (void*)this, lockSize.x, lockSize.y, lockSize.width, lockSize.height, + aRegion.GetNumRects(), lockedScreenRect.width, lockedScreenRect.height)); + LOGWAYLAND((" nsWindow = %p\n", mWindow)); + LOGWAYLAND((" isPopup = %d\n", mWindow->IsWaylandPopup())); + LOGWAYLAND((" isTransparentPopup = %d\n", isTransparentPopup)); + LOGWAYLAND((" IsPopupFullScreenUpdate = %d\n", + IsPopupFullScreenUpdate(lockedScreenRect, aRegion))); + LOGWAYLAND((" IsWindowFullScreenUpdate = %d\n", + IsWindowFullScreenUpdate(lockedScreenRect, aRegion))); + LOGWAYLAND((" mBufferNeedsClear = %d\n", mBufferNeedsClear)); + LOGWAYLAND((" mBufferPendingCommit = %d\n", mBufferPendingCommit)); + LOGWAYLAND((" mCanSwitchWaylandBuffer = %d\n", mCanSwitchWaylandBuffer)); + LOGWAYLAND((" windowRedraw = %d\n", windowRedraw)); + + if (!(mLockedScreenRect == lockedScreenRect)) { + LOGWAYLAND((" screen size changed\n")); + + // Screen (window) size changed and we still have some painting pending + // for the last window size. That can happen when window is resized. + // We can't commit them any more as they're for former window size, so + // scratch them. + mDelayedImageCommits.Clear(); + mWaylandBufferDamage.SetEmpty(); + + if (!windowRedraw) { + NS_WARNING("Partial screen update when window is resized!"); + // This should not happen. Screen size changed but we got only + // partal screen update instead of whole screen. Discard this painting + // as it produces artifacts. + return nullptr; + } + mLockedScreenRect = lockedScreenRect; + } + + // We can draw directly only when widget has the same size as wl_buffer + LayoutDeviceIntRect size = mWindow->GetMozContainerSize(); + mDrawToWaylandBufferDirectly = (size.width >= mLockedScreenRect.width && + size.height >= mLockedScreenRect.height); + + // We can draw directly only when we redraw significant part of the window + // to avoid flickering or do only fullscreen updates in smooth mode. + if (mDrawToWaylandBufferDirectly) { + mDrawToWaylandBufferDirectly = + mSmoothRendering + ? windowRedraw + : (windowRedraw || (lockSize.width * 2 > lockedScreenRect.width && + lockSize.height * 2 > lockedScreenRect.height)); + } + if (!mDrawToWaylandBufferDirectly) { + // Don't switch wl_buffers when we cache drawings. + mCanSwitchWaylandBuffer = false; + LOGWAYLAND((" Indirect drawing, mCanSwitchWaylandBuffer = %d\n", + mCanSwitchWaylandBuffer)); + } + + if (mDrawToWaylandBufferDirectly) { + LOGWAYLAND((" Direct drawing\n")); + RefPtr<gfx::DrawTarget> dt = LockWaylandBuffer(); + if (dt) { +#if MOZ_LOGGING + mWaylandBuffer->DumpToFile("Lock"); +#endif + if (!windowRedraw) { + DrawDelayedImageCommits(dt, mWaylandBufferDamage); +#if MOZ_LOGGING + mWaylandBuffer->DumpToFile("Lock-after-commit"); +#endif + } + mBufferPendingCommit = true; + return dt.forget(); + } + } + + // We do indirect drawing because there isn't any front buffer available. + // Do indirect drawing to mImageSurface which is commited to wayland + // wl_buffer by DrawDelayedImageCommits() later. + mDrawToWaylandBufferDirectly = false; + + LOGWAYLAND((" Indirect drawing.\n")); + return LockImageSurface(gfx::IntSize(lockSize.XMost(), lockSize.YMost())); +} + +bool WindowImageSurface::OverlapsSurface( + class WindowImageSurface& aBottomSurface) { + return mUpdateRegion.Contains(aBottomSurface.mUpdateRegion); +} + +void WindowImageSurface::DrawToTarget( + gfx::DrawTarget* aDest, LayoutDeviceIntRegion& aWaylandBufferDamage) { +#ifdef MOZ_LOGGING + gfx::IntRect bounds = mUpdateRegion.GetBounds().ToUnknownRect(); + LOGWAYLAND(("WindowImageSurface::DrawToTarget\n")); + LOGWAYLAND((" rects num %d\n", mUpdateRegion.GetNumRects())); + LOGWAYLAND((" bounds [ %d, %d] -> [%d x %d]\n", bounds.x, bounds.y, + bounds.width, bounds.height)); +#endif + for (auto iter = mUpdateRegion.RectIter(); !iter.Done(); iter.Next()) { + gfx::IntRect r(iter.Get().ToUnknownRect()); + LOGWAYLAND( + (" draw rect [%d,%d] -> [%d x %d]\n", r.x, r.y, r.width, r.height)); + aDest->CopySurface(mImageSurface, r, gfx::IntPoint(r.x, r.y)); + } + aWaylandBufferDamage.OrWith(mUpdateRegion); +} + +WindowImageSurface::WindowImageSurface( + gfx::DataSourceSurface* aImageSurface, + const LayoutDeviceIntRegion& aUpdateRegion) + : mImageSurface(aImageSurface), mUpdateRegion(aUpdateRegion) {} + +void WindowSurfaceWayland::DrawDelayedImageCommits( + gfx::DrawTarget* aDrawTarget, LayoutDeviceIntRegion& aWaylandBufferDamage) { + unsigned int imagesNum = mDelayedImageCommits.Length(); + LOGWAYLAND(("WindowSurfaceWayland::DrawDelayedImageCommits [%p] len %d\n", + (void*)this, imagesNum)); + for (unsigned int i = 0; i < imagesNum; i++) { + mDelayedImageCommits[i].DrawToTarget(aDrawTarget, aWaylandBufferDamage); + } + mDelayedImageCommits.Clear(); +} + +void WindowSurfaceWayland::CacheImageSurface( + const LayoutDeviceIntRegion& aRegion) { +#ifdef MOZ_LOGGING + gfx::IntRect bounds = aRegion.GetBounds().ToUnknownRect(); + LOGWAYLAND(("WindowSurfaceWayland::CacheImageSurface [%p]\n", (void*)this)); + LOGWAYLAND((" rects num %d\n", aRegion.GetNumRects())); + LOGWAYLAND((" bounds [ %d, %d] -> [%d x %d]\n", bounds.x, bounds.y, + bounds.width, bounds.height)); +#endif + + mImageSurface->Unmap(); + WindowImageSurface surf = WindowImageSurface(mImageSurface, aRegion); + + if (mDelayedImageCommits.Length()) { + auto lastSurf = mDelayedImageCommits.PopLastElement(); + if (surf.OverlapsSurface(lastSurf)) { +#ifdef MOZ_LOGGING + { + gfx::IntRect size = + lastSurf.GetUpdateRegion()->GetBounds().ToUnknownRect(); + LOGWAYLAND((" removing [ %d, %d] -> [%d x %d]\n", size.x, size.y, + size.width, size.height)); + } +#endif + } else { + mDelayedImageCommits.AppendElement(lastSurf); + } + } + + mDelayedImageCommits.AppendElement(surf); + // mImageSurface is owned by mDelayedImageCommits + mImageSurface = nullptr; + + LOGWAYLAND( + (" There's %d cached images\n", int(mDelayedImageCommits.Length()))); +} + +bool WindowSurfaceWayland::CommitImageCacheToWaylandBuffer() { + if (!mDelayedImageCommits.Length()) { + return false; + } + + MOZ_ASSERT(!mDrawToWaylandBufferDirectly); + + RefPtr<gfx::DrawTarget> dt = LockWaylandBuffer(); + if (!dt) { + return false; + } + + LOGWAYLAND((" Flushing %ld cached WindowImageSurfaces to Wayland buffer\n", + long(mDelayedImageCommits.Length()))); + + DrawDelayedImageCommits(dt, mWaylandBufferDamage); + UnlockWaylandBuffer(); + + return true; +} + +void WindowSurfaceWayland::FlushPendingCommits() { + MutexAutoLock lock(mSurfaceLock); + if (FlushPendingCommitsLocked()) { + mWaylandDisplay->QueueSyncBegin(); + } +} + +// When a new window is created we may not have a valid wl_surface +// for drawing (Gtk haven't created it yet). All commits are queued +// and FlushPendingCommitsLocked() is called by timer when wl_surface is ready +// for drawing. +static int WaylandBufferFlushPendingCommits(void* data) { + WindowSurfaceWayland* aSurface = static_cast<WindowSurfaceWayland*>(data); + aSurface->FlushPendingCommits(); + return true; +} + +bool WindowSurfaceWayland::FlushPendingCommitsLocked() { + LOGWAYLAND( + ("WindowSurfaceWayland::FlushPendingCommitsLocked [%p]\n", (void*)this)); + LOGWAYLAND( + (" mDrawToWaylandBufferDirectly = %d\n", mDrawToWaylandBufferDirectly)); + LOGWAYLAND((" mCanSwitchWaylandBuffer = %d\n", mCanSwitchWaylandBuffer)); + LOGWAYLAND((" mFrameCallback = %p\n", mFrameCallback)); + LOGWAYLAND((" mLastCommittedSurface = %p\n", mLastCommittedSurface)); + LOGWAYLAND((" mBufferPendingCommit = %d\n", mBufferPendingCommit)); + LOGWAYLAND((" mBufferCommitAllowed = %d\n", mBufferCommitAllowed)); + + if (!mBufferCommitAllowed) { + return false; + } + + if (CommitImageCacheToWaylandBuffer()) { + mBufferPendingCommit = true; + } + + // There's nothing to do here + if (!mBufferPendingCommit) { + return false; + } + + MOZ_ASSERT(!mWaylandBuffer->IsAttached(), + "We can't draw to attached wayland buffer!"); + + MozContainer* container = mWindow->GetMozContainer(); + wl_surface* waylandSurface = moz_container_wayland_surface_lock(container); + if (!waylandSurface) { + LOGWAYLAND((" [%p] mWindow->GetWaylandSurface() failed, delay commit.\n", + (void*)this)); + + // Target window is not created yet - delay the commit. This can happen only + // when the window is newly created and there's no active + // frame callback pending. + MOZ_ASSERT(!mFrameCallback || waylandSurface != mLastCommittedSurface, + "Missing wayland surface at frame callback!"); + + if (!mSurfaceReadyTimerID) { + mSurfaceReadyTimerID = g_timeout_add( + EVENT_LOOP_DELAY, &WaylandBufferFlushPendingCommits, this); + } + return true; + } + if (mSurfaceReadyTimerID) { + g_source_remove(mSurfaceReadyTimerID); + mSurfaceReadyTimerID = 0; + } + + auto unlockContainer = MakeScopeExit([&] { + moz_container_wayland_surface_unlock(container, &waylandSurface); + }); + + wl_proxy_set_queue((struct wl_proxy*)waylandSurface, + mWaylandDisplay->GetEventQueue()); + + // We have an active frame callback request so handle it. + if (mFrameCallback) { + if (waylandSurface == mLastCommittedSurface) { + LOGWAYLAND((" [%p] wait for frame callback.\n", (void*)this)); + // We have an active frame callback pending from our recent surface. + // It means we should defer the commit to FrameCallbackHandler(). + return true; + } + // If our stored wl_surface does not match the actual one it means the frame + // callback is no longer active and we should release it. + wl_callback_destroy(mFrameCallback); + mFrameCallback = nullptr; + mLastCommittedSurface = nullptr; + } + + if (mWaylandFullscreenDamage) { + LOGWAYLAND((" wl_surface_damage full screen\n")); + wl_surface_damage(waylandSurface, 0, 0, INT_MAX, INT_MAX); + } else { + for (auto iter = mWaylandBufferDamage.RectIter(); !iter.Done(); + iter.Next()) { + mozilla::LayoutDeviceIntRect r = iter.Get(); + LOGWAYLAND((" wl_surface_damage_buffer [%d, %d] -> [%d, %d]\n", r.x, + r.y, r.width, r.height)); + wl_surface_damage_buffer(waylandSurface, r.x, r.y, r.width, r.height); + } + } + +#if MOZ_LOGGING + mWaylandBuffer->DumpToFile("Commit"); +#endif + + // Clear all back buffer damage as we're committing + // all requested regions. + mWaylandFullscreenDamage = false; + mWaylandBufferDamage.SetEmpty(); + + mFrameCallback = wl_surface_frame(waylandSurface); + wl_callback_add_listener(mFrameCallback, &frame_listener, this); + + mWaylandBuffer->Attach(waylandSurface); + mLastCommittedSurface = waylandSurface; + mLastCommitTime = g_get_monotonic_time() / 1000; + + // There's no pending commit, all changes are sent to compositor. + mBufferPendingCommit = false; + + return true; +} + +void WindowSurfaceWayland::Commit(const LayoutDeviceIntRegion& aInvalidRegion) { +#ifdef MOZ_LOGGING + { + gfx::IntRect lockSize = aInvalidRegion.GetBounds().ToUnknownRect(); + LOGWAYLAND( + ("WindowSurfaceWayland::Commit [%p] damage size [%d, %d] -> [%d x %d]" + "screenSize [%d x %d]\n", + (void*)this, lockSize.x, lockSize.y, lockSize.width, lockSize.height, + mLockedScreenRect.width, mLockedScreenRect.height)); + LOGWAYLAND((" mDrawToWaylandBufferDirectly = %d\n", + mDrawToWaylandBufferDirectly)); + } +#endif + + MutexAutoLock lock(mSurfaceLock); + + if (mDrawToWaylandBufferDirectly) { + MOZ_ASSERT(mWaylandBuffer->IsLocked()); + mWaylandBufferDamage.OrWith(aInvalidRegion); + UnlockWaylandBuffer(); + } else { + CacheImageSurface(aInvalidRegion); + } + + mBufferCommitAllowed = true; + if (FlushPendingCommitsLocked()) { + mWaylandDisplay->QueueSyncBegin(); + } +} + +void WindowSurfaceWayland::FrameCallbackHandler() { + MOZ_ASSERT(mFrameCallback != nullptr, + "FrameCallbackHandler() called without valid frame callback!"); + MOZ_ASSERT(mLastCommittedSurface != nullptr, + "FrameCallbackHandler() called without valid wl_surface!"); + LOGWAYLAND( + ("WindowSurfaceWayland::FrameCallbackHandler [%p]\n", (void*)this)); + + MutexAutoLock lock(mSurfaceLock); + + wl_callback_destroy(mFrameCallback); + mFrameCallback = nullptr; + + if (FlushPendingCommitsLocked()) { + mWaylandDisplay->QueueSyncBegin(); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/WindowSurfaceWayland.h b/widget/gtk/WindowSurfaceWayland.h new file mode 100644 index 0000000000..b34a9a7036 --- /dev/null +++ b/widget/gtk/WindowSurfaceWayland.h @@ -0,0 +1,269 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_H +#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_H + +#include <prthread.h> +#include "gfxImageSurface.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Types.h" +#include "nsWaylandDisplay.h" +#include "nsWindow.h" +#include "WindowSurface.h" +#include "mozilla/Mutex.h" + +#define BACK_BUFFER_NUM 3 + +namespace mozilla { +namespace widget { + +class WindowSurfaceWayland; + +// Allocates and owns shared memory for Wayland drawing surface +class WaylandShmPool { + public: + bool Create(RefPtr<nsWaylandDisplay> aWaylandDisplay, int aSize); + void Release(); + wl_shm_pool* GetShmPool() { return mShmPool; }; + void* GetImageData() { return mImageData; }; + void SetImageDataFromPool(class WaylandShmPool* aSourcePool, + int aImageDataSize); + WaylandShmPool(); + ~WaylandShmPool(); + + private: + wl_shm_pool* mShmPool; + int mShmPoolFd; + int mAllocatedSize; + void* mImageData; +}; + +// Holds actual graphics data for wl_surface +class WindowBackBuffer { + public: + explicit WindowBackBuffer(WindowSurfaceWayland* aWindowSurfaceWayland); + ~WindowBackBuffer(); + + already_AddRefed<gfx::DrawTarget> Lock(); + bool IsLocked() { return mIsLocked; }; + void Unlock() { mIsLocked = false; }; + + void Attach(wl_surface* aSurface); + void Detach(wl_buffer* aBuffer); + bool IsAttached() { return mAttached; } + + void Clear(); + bool Create(int aWidth, int aHeight); + bool Resize(int aWidth, int aHeight); + bool SetImageDataFromBuffer(class WindowBackBuffer* aSourceBuffer); + + int GetWidth() { return mWidth; }; + int GetHeight() { return mHeight; }; + + wl_buffer* GetWlBuffer() { return mWLBuffer; }; + + bool IsMatchingSize(int aWidth, int aHeight) { + return aWidth == GetWidth() && aHeight == GetHeight(); + } + bool IsMatchingSize(class WindowBackBuffer* aBuffer) { + return aBuffer->IsMatchingSize(GetWidth(), GetHeight()); + } + static gfx::SurfaceFormat GetSurfaceFormat() { return mFormat; } + +#ifdef MOZ_LOGGING + void DumpToFile(const char* aHint); +#endif + + RefPtr<nsWaylandDisplay> GetWaylandDisplay(); + + private: + void ReleaseWLBuffer(); + + static gfx::SurfaceFormat mFormat; + WindowSurfaceWayland* mWindowSurfaceWayland; + + // WaylandShmPool provides actual shared memory we draw into + WaylandShmPool mShmPool; + +#ifdef MOZ_LOGGING + static int mDumpSerial; + static char* mDumpDir; +#endif + + // wl_buffer is a wayland object that encapsulates the shared memory + // and passes it to wayland compositor by wl_surface object. + wl_buffer* mWLBuffer; + int mWidth; + int mHeight; + bool mAttached; + bool mIsLocked; +}; + +class WindowImageSurface { + public: + void DrawToTarget(gfx::DrawTarget* aDest, + LayoutDeviceIntRegion& aWaylandBufferDamage); + WindowImageSurface(gfx::DataSourceSurface* aImageSurface, + const LayoutDeviceIntRegion& aUpdateRegion); + bool OverlapsSurface(class WindowImageSurface& aBottomSurface); + + const LayoutDeviceIntRegion* GetUpdateRegion() { return &mUpdateRegion; }; + + private: + RefPtr<gfx::DataSourceSurface> mImageSurface; + const LayoutDeviceIntRegion mUpdateRegion; +}; + +// WindowSurfaceWayland is an abstraction for wl_surface +// and related management +class WindowSurfaceWayland : public WindowSurface { + public: + explicit WindowSurfaceWayland(nsWindow* aWindow); + ~WindowSurfaceWayland(); + + // Lock() / Commit() are called by gecko when Firefox + // wants to display something. Lock() returns a DrawTarget + // where gecko paints. When gecko is done it calls Commit() + // and we try to send the DrawTarget (backed by wl_buffer) + // to wayland compositor. + // + // If we fail (wayland compositor is busy, + // wl_surface is not created yet) we queue the painting + // and we send it to wayland compositor in FrameCallbackHandler()/ + // FlushPendingCommits(). + already_AddRefed<gfx::DrawTarget> Lock( + const LayoutDeviceIntRegion& aRegion) override; + void Commit(const LayoutDeviceIntRegion& aInvalidRegion) final; + + // It's called from wayland compositor when there's the right + // time to send wl_buffer to display. It's no-op if there's no + // queued commits. + void FrameCallbackHandler(); + + // Try to commit all queued drawings to Wayland compositor. This is usually + // called from other routines but can be used to explicitly flush + // all drawings as we do when wl_buffer is released + // (see WindowBackBufferShm::Detach() for instance). + void FlushPendingCommits(); + + RefPtr<nsWaylandDisplay> GetWaylandDisplay() { return mWaylandDisplay; }; + + // Image cache mode can be set by widget.wayland_cache_mode + typedef enum { + // Cache and clip all drawings, default. It's slowest + // but also without any rendered artifacts. + CACHE_ALL = 0, + // Cache drawing only when back buffer is missing. May produce + // some rendering artifacts and flickering when partial screen update + // is rendered. + CACHE_MISSING = 1, + // Don't cache anything, draw only when back buffer is available. + CACHE_NONE = 2 + } RenderingCacheMode; + + private: + WindowBackBuffer* GetWaylandBuffer(); + WindowBackBuffer* SetNewWaylandBuffer(); + WindowBackBuffer* CreateWaylandBuffer(int aWidth, int aHeight); + WindowBackBuffer* WaylandBufferFindAvailable(int aWidth, int aHeight); + + already_AddRefed<gfx::DrawTarget> LockWaylandBuffer(); + void UnlockWaylandBuffer(); + + already_AddRefed<gfx::DrawTarget> LockImageSurface( + const gfx::IntSize& aLockSize); + + void CacheImageSurface(const LayoutDeviceIntRegion& aRegion); + bool CommitImageCacheToWaylandBuffer(); + + void DrawDelayedImageCommits(gfx::DrawTarget* aDrawTarget, + LayoutDeviceIntRegion& aWaylandBufferDamage); + // Return true if we need to sync Wayland events after this call. + bool FlushPendingCommitsLocked(); + + // TODO: Do we need to hold a reference to nsWindow object? + nsWindow* mWindow; + // Buffer screen rects helps us understand if we operate on + // the same window size as we're called on WindowSurfaceWayland::Lock(). + // mLockedScreenRect is window size when our wayland buffer was allocated. + LayoutDeviceIntRect mLockedScreenRect; + + // mWLBufferRect is an intersection of mozcontainer widgetsize and + // mLockedScreenRect size. It can be different than mLockedScreenRect + // during resize when mBounds are updated immediately but actual + // GtkWidget size is updated asynchronously (see Bug 1489463). + LayoutDeviceIntRect mWLBufferRect; + RefPtr<nsWaylandDisplay> mWaylandDisplay; + + // Actual buffer (backed by wl_buffer) where all drawings go into. + // Drawn areas are stored at mWaylandBufferDamage and if there's + // any uncommited drawings which needs to be send to wayland compositor + // the mBufferPendingCommit is set. + WindowBackBuffer* mWaylandBuffer; + WindowBackBuffer* mShmBackupBuffer[BACK_BUFFER_NUM]; + + // When mWaylandFullscreenDamage we invalidate whole surface, + // otherwise partial screen updates (mWaylandBufferDamage) are used. + bool mWaylandFullscreenDamage; + LayoutDeviceIntRegion mWaylandBufferDamage; + + // After every commit to wayland compositor a frame callback is requested. + // Any next commit to wayland compositor will happen when frame callback + // comes from wayland compositor back as it's the best time to do the commit. + wl_callback* mFrameCallback; + wl_surface* mLastCommittedSurface; + + // Cached drawings. If we can't get WaylandBuffer (wl_buffer) at + // WindowSurfaceWayland::Lock() we direct gecko rendering to + // mImageSurface. + // If we can't get WaylandBuffer at WindowSurfaceWayland::Commit() + // time, mImageSurface is moved to mDelayedImageCommits which + // holds all cached drawings. + // mDelayedImageCommits can be drawn by FrameCallbackHandler() + // or when WaylandBuffer is detached. + RefPtr<gfx::DataSourceSurface> mImageSurface; + AutoTArray<WindowImageSurface, 30> mDelayedImageCommits; + + int64_t mLastCommitTime; + + // Indicates that we don't have any cached drawings at mDelayedImageCommits + // and WindowSurfaceWayland::Lock() returned WaylandBuffer to gecko + // to draw into. + bool mDrawToWaylandBufferDirectly; + + // Set when our cached drawings (mDelayedImageCommits) contains + // full screen damage. That means we can safely switch WaylandBuffer + // at LockWaylandBuffer(). + bool mCanSwitchWaylandBuffer; + + // Set when actual WaylandBuffer contains drawings which are not send to + // wayland compositor yet. + bool mBufferPendingCommit; + + // We can't send WaylandBuffer (wl_buffer) to compositor when gecko + // is rendering into it (i.e. between WindowSurfaceWayland::Lock() / + // WindowSurfaceWayland::Commit()). + // Thus we use mBufferCommitAllowed to disable commit by + // FlushPendingCommits(). + bool mBufferCommitAllowed; + + // We need to clear WaylandBuffer when entire transparent window is repainted. + // This typically apply to popup windows. + bool mBufferNeedsClear; + + // Cache all drawings except fullscreen updates. + // Avoid any rendering artifacts for significant performance penality. + bool mSmoothRendering; + + gint mSurfaceReadyTimerID; + mozilla::Mutex mSurfaceLock; +}; + +} // namespace widget +} // namespace mozilla + +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_H diff --git a/widget/gtk/WindowSurfaceX11.cpp b/widget/gtk/WindowSurfaceX11.cpp new file mode 100644 index 0000000000..a32cc12e18 --- /dev/null +++ b/widget/gtk/WindowSurfaceX11.cpp @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WindowSurfaceX11.h" +#include "gfxPlatform.h" +#include "X11UndefineNone.h" + +namespace mozilla { +namespace widget { + +WindowSurfaceX11::WindowSurfaceX11(Display* aDisplay, Window aWindow, + Visual* aVisual, unsigned int aDepth) + : mDisplay(aDisplay), + mWindow(aWindow), + mVisual(aVisual), + mDepth(aDepth), + mFormat(GetVisualFormat(aVisual, aDepth)) {} + +/* static */ +gfx::SurfaceFormat WindowSurfaceX11::GetVisualFormat(const Visual* aVisual, + unsigned int aDepth) { + switch (aDepth) { + case 32: + if (aVisual->red_mask == 0xff0000 && aVisual->green_mask == 0xff00 && + aVisual->blue_mask == 0xff) { + return gfx::SurfaceFormat::B8G8R8A8; + } + break; + case 24: + if (aVisual->red_mask == 0xff0000 && aVisual->green_mask == 0xff00 && + aVisual->blue_mask == 0xff) { + return gfx::SurfaceFormat::B8G8R8X8; + } + break; + case 16: + if (aVisual->red_mask == 0xf800 && aVisual->green_mask == 0x07e0 && + aVisual->blue_mask == 0x1f) { + return gfx::SurfaceFormat::R5G6B5_UINT16; + } + break; + } + + return gfx::SurfaceFormat::UNKNOWN; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/WindowSurfaceX11.h b/widget/gtk/WindowSurfaceX11.h new file mode 100644 index 0000000000..d297ec6b66 --- /dev/null +++ b/widget/gtk/WindowSurfaceX11.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_H +#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_H + +#ifdef MOZ_X11 + +# include "mozilla/widget/WindowSurface.h" +# include "mozilla/gfx/Types.h" + +# include <X11/Xlib.h> + +namespace mozilla { +namespace widget { + +class WindowSurfaceX11 : public WindowSurface { + public: + WindowSurfaceX11(Display* aDisplay, Window aWindow, Visual* aVisual, + unsigned int aDepth); + + protected: + static gfx::SurfaceFormat GetVisualFormat(const Visual* aVisual, + unsigned int aDepth); + + Display* const mDisplay; + const Window mWindow; + Visual* const mVisual; + const unsigned int mDepth; + const gfx::SurfaceFormat mFormat; +}; + +} // namespace widget +} // namespace mozilla + +#endif // MOZ_X11 +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_H diff --git a/widget/gtk/WindowSurfaceX11Image.cpp b/widget/gtk/WindowSurfaceX11Image.cpp new file mode 100644 index 0000000000..1e4d28915f --- /dev/null +++ b/widget/gtk/WindowSurfaceX11Image.cpp @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WindowSurfaceX11Image.h" + +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Tools.h" +#include "mozilla/gfx/gfxVars.h" +#include "gfxPlatform.h" +#include "gfx2DGlue.h" + +#include <X11/extensions/shape.h> + +namespace mozilla { +namespace widget { + +using namespace mozilla::gfx; + +// gfxImageSurface pixel format configuration. +#define SHAPED_IMAGE_SURFACE_BPP 4 +#ifdef IS_BIG_ENDIAN +# define SHAPED_IMAGE_SURFACE_ALPHA_INDEX 0 +#else +# define SHAPED_IMAGE_SURFACE_ALPHA_INDEX 3 +#endif + +WindowSurfaceX11Image::WindowSurfaceX11Image(Display* aDisplay, Window aWindow, + Visual* aVisual, + unsigned int aDepth, + bool aIsShaped) + : WindowSurfaceX11(aDisplay, aWindow, aVisual, aDepth), + mTransparencyBitmap(nullptr), + mTransparencyBitmapWidth(0), + mTransparencyBitmapHeight(0), + mIsShaped(aIsShaped) {} + +WindowSurfaceX11Image::~WindowSurfaceX11Image() { + if (mTransparencyBitmap) { + delete[] mTransparencyBitmap; + + Display* xDisplay = mWindowSurface->XDisplay(); + Window xDrawable = mWindowSurface->XDrawable(); + + XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, X11None, + ShapeSet); + } +} + +already_AddRefed<gfx::DrawTarget> WindowSurfaceX11Image::Lock( + const LayoutDeviceIntRegion& aRegion) { + gfx::IntRect bounds = aRegion.GetBounds().ToUnknownRect(); + gfx::IntSize size(bounds.XMost(), bounds.YMost()); + + if (!mWindowSurface || mWindowSurface->CairoStatus() || + !(size <= mWindowSurface->GetSize())) { + mWindowSurface = new gfxXlibSurface(mDisplay, mWindow, mVisual, size); + } + if (mWindowSurface->CairoStatus()) { + return nullptr; + } + + if (!mImageSurface || mImageSurface->CairoStatus() || + !(size <= mImageSurface->GetSize())) { + gfxImageFormat format = SurfaceFormatToImageFormat(mFormat); + if (format == gfx::SurfaceFormat::UNKNOWN) { + format = mDepth == 32 ? gfx::SurfaceFormat::A8R8G8B8_UINT32 + : gfx::SurfaceFormat::X8R8G8B8_UINT32; + } + + // Use alpha image format for shaped window as we derive + // the shape bitmap from alpha channel. Must match SHAPED_IMAGE_SURFACE_BPP + // and SHAPED_IMAGE_SURFACE_ALPHA_INDEX. + if (mIsShaped) { + format = gfx::SurfaceFormat::A8R8G8B8_UINT32; + } + + mImageSurface = new gfxImageSurface(size, format); + if (mImageSurface->CairoStatus()) { + return nullptr; + } + } + + gfxImageFormat format = mImageSurface->Format(); + // Cairo prefers compositing to BGRX instead of BGRA where possible. + // Cairo/pixman lacks some fast paths for compositing BGRX onto BGRA, so + // just report it as BGRX directly in that case. + // Otherwise, for Skia, report it as BGRA to the compositor. The alpha + // channel will be discarded when we put the image. + if (format == gfx::SurfaceFormat::X8R8G8B8_UINT32) { + gfx::BackendType backend = gfxVars::ContentBackend(); + if (!gfx::Factory::DoesBackendSupportDataDrawtarget(backend)) { +#ifdef USE_SKIA + backend = gfx::BackendType::SKIA; +#else + backend = gfx::BackendType::CAIRO; +#endif + } + if (backend != gfx::BackendType::CAIRO) { + format = gfx::SurfaceFormat::A8R8G8B8_UINT32; + } + } + + return gfxPlatform::CreateDrawTargetForData( + mImageSurface->Data(), mImageSurface->GetSize(), mImageSurface->Stride(), + ImageFormatToSurfaceFormat(format)); +} + +// The transparency bitmap routines are derived form the ones at nsWindow.cpp. +// The difference here is that we compose to RGBA image and then create +// the shape mask from final image alpha channel. +static inline int32_t GetBitmapStride(int32_t width) { return (width + 7) / 8; } + +static bool ChangedMaskBits(gchar* aMaskBits, int32_t aMaskWidth, + int32_t aMaskHeight, const nsIntRect& aRect, + uint8_t* aImageData) { + int32_t stride = aMaskWidth * SHAPED_IMAGE_SURFACE_BPP; + int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost(); + int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth); + for (y = aRect.y; y < yMax; y++) { + gchar* maskBytes = aMaskBits + y * maskBytesPerRow; + uint8_t* alphas = aImageData; + for (x = aRect.x; x < xMax; x++) { + bool newBit = *(alphas + SHAPED_IMAGE_SURFACE_ALPHA_INDEX) > 0x7f; + alphas += SHAPED_IMAGE_SURFACE_BPP; + + gchar maskByte = maskBytes[x >> 3]; + bool maskBit = (maskByte & (1 << (x & 7))) != 0; + + if (maskBit != newBit) { + return true; + } + } + aImageData += stride; + } + + return false; +} + +static void UpdateMaskBits(gchar* aMaskBits, int32_t aMaskWidth, + int32_t aMaskHeight, const nsIntRect& aRect, + uint8_t* aImageData) { + int32_t stride = aMaskWidth * SHAPED_IMAGE_SURFACE_BPP; + int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost(); + int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth); + for (y = aRect.y; y < yMax; y++) { + gchar* maskBytes = aMaskBits + y * maskBytesPerRow; + uint8_t* alphas = aImageData; + for (x = aRect.x; x < xMax; x++) { + bool newBit = *(alphas + SHAPED_IMAGE_SURFACE_ALPHA_INDEX) > 0x7f; + alphas += SHAPED_IMAGE_SURFACE_BPP; + + gchar mask = 1 << (x & 7); + gchar maskByte = maskBytes[x >> 3]; + // Note: '-newBit' turns 0 into 00...00 and 1 into 11...11 + maskBytes[x >> 3] = (maskByte & ~mask) | (-newBit & mask); + } + aImageData += stride; + } +} + +void WindowSurfaceX11Image::ResizeTransparencyBitmap(int aWidth, int aHeight) { + int32_t actualSize = + GetBitmapStride(mTransparencyBitmapWidth) * mTransparencyBitmapHeight; + int32_t newSize = GetBitmapStride(aWidth) * aHeight; + + if (actualSize < newSize) { + delete[] mTransparencyBitmap; + mTransparencyBitmap = new gchar[newSize]; + } + + mTransparencyBitmapWidth = aWidth; + mTransparencyBitmapHeight = aHeight; +} + +void WindowSurfaceX11Image::ApplyTransparencyBitmap() { + gfx::IntSize size = mWindowSurface->GetSize(); + bool maskChanged = true; + + if (!mTransparencyBitmap) { + mTransparencyBitmapWidth = size.width; + mTransparencyBitmapHeight = size.height; + + int32_t byteSize = + GetBitmapStride(mTransparencyBitmapWidth) * mTransparencyBitmapHeight; + mTransparencyBitmap = new gchar[byteSize]; + } else { + bool sizeChanged = (size.width != mTransparencyBitmapWidth || + size.height != mTransparencyBitmapHeight); + + if (sizeChanged) { + ResizeTransparencyBitmap(size.width, size.height); + } else { + maskChanged = ChangedMaskBits( + mTransparencyBitmap, mTransparencyBitmapWidth, + mTransparencyBitmapHeight, nsIntRect(0, 0, size.width, size.height), + (uint8_t*)mImageSurface->Data()); + } + } + + if (maskChanged) { + UpdateMaskBits(mTransparencyBitmap, mTransparencyBitmapWidth, + mTransparencyBitmapHeight, + nsIntRect(0, 0, size.width, size.height), + (uint8_t*)mImageSurface->Data()); + + // We use X11 calls where possible, because GDK handles expose events + // for shaped windows in a way that's incompatible with us (Bug 635903). + // It doesn't occur when the shapes are set through X. + Display* xDisplay = mWindowSurface->XDisplay(); + Window xDrawable = mWindowSurface->XDrawable(); + Pixmap maskPixmap = XCreateBitmapFromData( + xDisplay, xDrawable, mTransparencyBitmap, mTransparencyBitmapWidth, + mTransparencyBitmapHeight); + XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap, + ShapeSet); + XFreePixmap(xDisplay, maskPixmap); + } +} + +void WindowSurfaceX11Image::Commit( + const LayoutDeviceIntRegion& aInvalidRegion) { + RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateDrawTargetForCairoSurface( + mWindowSurface->CairoSurface(), mWindowSurface->GetSize()); + RefPtr<gfx::SourceSurface> surf = + gfx::Factory::CreateSourceSurfaceForCairoSurface( + mImageSurface->CairoSurface(), mImageSurface->GetSize(), + mImageSurface->Format()); + if (!dt || !surf) { + return; + } + + gfx::IntRect bounds = aInvalidRegion.GetBounds().ToUnknownRect(); + gfx::Rect rect(bounds); + if (rect.IsEmpty()) { + return; + } + + uint32_t numRects = aInvalidRegion.GetNumRects(); + if (numRects != 1) { + AutoTArray<IntRect, 32> rects; + rects.SetCapacity(numRects); + for (auto iter = aInvalidRegion.RectIter(); !iter.Done(); iter.Next()) { + rects.AppendElement(iter.Get().ToUnknownRect()); + } + dt->PushDeviceSpaceClipRects(rects.Elements(), rects.Length()); + } + + if (mIsShaped) { + ApplyTransparencyBitmap(); + } + + dt->DrawSurface(surf, rect, rect); + + if (numRects != 1) { + dt->PopClip(); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/WindowSurfaceX11Image.h b/widget/gtk/WindowSurfaceX11Image.h new file mode 100644 index 0000000000..b8b2a33f0e --- /dev/null +++ b/widget/gtk/WindowSurfaceX11Image.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_IMAGE_H +#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_IMAGE_H + +#ifdef MOZ_X11 + +# include <glib.h> +# include "WindowSurfaceX11.h" +# include "gfxXlibSurface.h" +# include "gfxImageSurface.h" + +namespace mozilla { +namespace widget { + +class WindowSurfaceX11Image : public WindowSurfaceX11 { + public: + WindowSurfaceX11Image(Display* aDisplay, Window aWindow, Visual* aVisual, + unsigned int aDepth, bool aIsShaped); + ~WindowSurfaceX11Image(); + + already_AddRefed<gfx::DrawTarget> Lock( + const LayoutDeviceIntRegion& aRegion) override; + void Commit(const LayoutDeviceIntRegion& aInvalidRegion) override; + bool IsFallback() const override { return true; } + + private: + void ResizeTransparencyBitmap(int aWidth, int aHeight); + void ApplyTransparencyBitmap(); + + RefPtr<gfxXlibSurface> mWindowSurface; + RefPtr<gfxImageSurface> mImageSurface; + + gchar* mTransparencyBitmap; + int32_t mTransparencyBitmapWidth; + int32_t mTransparencyBitmapHeight; + bool mIsShaped; +}; + +} // namespace widget +} // namespace mozilla + +#endif // MOZ_X11 +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_IMAGE_H diff --git a/widget/gtk/WindowSurfaceXRender.cpp b/widget/gtk/WindowSurfaceXRender.cpp new file mode 100644 index 0000000000..9f040d9ce3 --- /dev/null +++ b/widget/gtk/WindowSurfaceXRender.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WindowSurfaceXRender.h" + +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Types.h" +#include "gfxPlatform.h" + +namespace mozilla { +namespace widget { + +WindowSurfaceXRender::WindowSurfaceXRender(Display* aDisplay, Window aWindow, + Visual* aVisual, unsigned int aDepth) + : WindowSurfaceX11(aDisplay, aWindow, aVisual, aDepth), + mXlibSurface(nullptr), + mGC(X11None) {} + +WindowSurfaceXRender::~WindowSurfaceXRender() { + if (mGC != X11None) { + XFreeGC(mDisplay, mGC); + } +} + +already_AddRefed<gfx::DrawTarget> WindowSurfaceXRender::Lock( + const LayoutDeviceIntRegion& aRegion) { + gfx::IntRect bounds = aRegion.GetBounds().ToUnknownRect(); + gfx::IntSize size(bounds.XMost(), bounds.YMost()); + if (!mXlibSurface || mXlibSurface->CairoStatus() || + !(size <= mXlibSurface->GetSize())) { + mXlibSurface = gfxXlibSurface::Create(DefaultScreenOfDisplay(mDisplay), + mVisual, size, mWindow); + } + if (!mXlibSurface || mXlibSurface->CairoStatus()) { + return nullptr; + } + + return gfxPlatform::CreateDrawTargetForSurface(mXlibSurface, size); +} + +void WindowSurfaceXRender::Commit(const LayoutDeviceIntRegion& aInvalidRegion) { + AutoTArray<XRectangle, 32> xrects; + xrects.SetCapacity(aInvalidRegion.GetNumRects()); + + for (auto iter = aInvalidRegion.RectIter(); !iter.Done(); iter.Next()) { + const LayoutDeviceIntRect& r = iter.Get(); + XRectangle xrect = {(short)r.x, (short)r.y, (unsigned short)r.width, + (unsigned short)r.height}; + xrects.AppendElement(xrect); + } + + if (!mGC) { + mGC = XCreateGC(mDisplay, mWindow, 0, nullptr); + if (!mGC) { + NS_WARNING("Couldn't create X11 graphics context for window!"); + return; + } + } + + XSetClipRectangles(mDisplay, mGC, 0, 0, xrects.Elements(), xrects.Length(), + YXBanded); + + MOZ_ASSERT(mXlibSurface && mXlibSurface->CairoStatus() == 0, + "Attempted to commit invalid surface!"); + gfx::IntRect bounds = aInvalidRegion.GetBounds().ToUnknownRect(); + gfx::IntSize size(bounds.XMost(), bounds.YMost()); + XCopyArea(mDisplay, mXlibSurface->XDrawable(), mWindow, mGC, bounds.x, + bounds.y, size.width, size.height, bounds.x, bounds.y); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/WindowSurfaceXRender.h b/widget/gtk/WindowSurfaceXRender.h new file mode 100644 index 0000000000..8c8e2745eb --- /dev/null +++ b/widget/gtk/WindowSurfaceXRender.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_XRENDER_H +#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_XRENDER_H + +#ifdef MOZ_X11 + +# include "WindowSurfaceX11.h" +# include "gfxXlibSurface.h" + +namespace mozilla { +namespace widget { + +class WindowSurfaceXRender : public WindowSurfaceX11 { + public: + WindowSurfaceXRender(Display* aDisplay, Window aWindow, Visual* aVisual, + unsigned int aDepth); + ~WindowSurfaceXRender(); + + already_AddRefed<gfx::DrawTarget> Lock( + const LayoutDeviceIntRegion& aRegion) override; + void Commit(const LayoutDeviceIntRegion& aInvalidRegion) override; + + private: + RefPtr<gfxXlibSurface> mXlibSurface; + GC mGC; +}; + +} // namespace widget +} // namespace mozilla + +#endif // MOZ_X11 +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_XRENDER_H diff --git a/widget/gtk/compat-gtk3/gdk/gdkversionmacros.h b/widget/gtk/compat-gtk3/gdk/gdkversionmacros.h new file mode 100644 index 0000000000..4cf3b68f62 --- /dev/null +++ b/widget/gtk/compat-gtk3/gdk/gdkversionmacros.h @@ -0,0 +1,32 @@ +/* -*- 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 GDKVERSIONMACROS_WRAPPER_H +#define GDKVERSIONMACROS_WRAPPER_H + +/** + * Suppress all GTK3 deprecated warnings as deprecated functions are often + * used for GTK2 compatibility. + * + * GDK_VERSION_MIN_REQUIRED cannot be used to suppress warnings for functions + * deprecated in 3.0, but still needs to be set because gdkversionmacros.h + * asserts that GDK_VERSION_MAX_ALLOWED >= GDK_VERSION_MIN_REQUIRED and + * GDK_VERSION_MIN_REQUIRED >= GDK_VERSION_3_0. + * + * Setting GDK_DISABLE_DEPRECATION_WARNINGS would also disable + * GDK_UNAVAILABLE() warnings, which are useful. + */ + +#define GDK_VERSION_MIN_REQUIRED GDK_VERSION_3_14 + +#include_next <gdk/gdkversionmacros.h> + +#undef GDK_DEPRECATED +#define GDK_DEPRECATED GDK_AVAILABLE_IN_ALL +#undef GDK_DEPRECATED_FOR +#define GDK_DEPRECATED_FOR(f) GDK_AVAILABLE_IN_ALL + +#endif /* GDKVERSIONMACROS_WRAPPER_H */ diff --git a/widget/gtk/compat/gdk/gdkdnd.h b/widget/gtk/compat/gdk/gdkdnd.h new file mode 100644 index 0000000000..bf9888d84f --- /dev/null +++ b/widget/gtk/compat/gdk/gdkdnd.h @@ -0,0 +1,29 @@ +/* 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 GDKDND_WRAPPER_H +#define GDKDND_WRAPPER_H + +#define gdk_drag_context_get_actions gdk_drag_context_get_actions_ +#define gdk_drag_context_list_targets gdk_drag_context_list_targets_ +#define gdk_drag_context_get_dest_window gdk_drag_context_get_dest_window_ +#include_next <gdk/gdkdnd.h> +#undef gdk_drag_context_get_actions +#undef gdk_drag_context_list_targets +#undef gdk_drag_context_get_dest_window + +static inline GdkDragAction gdk_drag_context_get_actions( + GdkDragContext* context) { + return context->actions; +} + +static inline GList* gdk_drag_context_list_targets(GdkDragContext* context) { + return context->targets; +} + +static inline GdkWindow* gdk_drag_context_get_dest_window( + GdkDragContext* context) { + return context->dest_window; +} +#endif /* GDKDND_WRAPPER_H */ diff --git a/widget/gtk/compat/gdk/gdkkeysyms.h b/widget/gtk/compat/gdk/gdkkeysyms.h new file mode 100644 index 0000000000..2f48d61fcc --- /dev/null +++ b/widget/gtk/compat/gdk/gdkkeysyms.h @@ -0,0 +1,266 @@ +/* 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 GDKKEYSYMS_WRAPPER_H +#define GDKKEYSYMS_WRAPPER_H + +#include_next <gdk/gdkkeysyms.h> + +#ifndef GDK_ISO_Level5_Shift +# define GDK_ISO_Level5_Shift 0xFE11 +#endif + +#ifndef GDK_ISO_Level5_Latch +# define GDK_ISO_Level5_Latch 0xFE12 +#endif + +#ifndef GDK_ISO_Level5_Lock +# define GDK_ISO_Level5_Lock 0xFE13 +#endif + +#ifndef GDK_dead_greek +# define GDK_dead_greek 0xFE8C +#endif + +#ifndef GDK_ch +# define GDK_ch 0xFEA0 +#endif + +#ifndef GDK_Ch +# define GDK_Ch 0xFEA1 +#endif + +#ifndef GDK_CH +# define GDK_CH 0xFEA2 +#endif + +#ifndef GDK_c_h +# define GDK_c_h 0xFEA3 +#endif + +#ifndef GDK_C_h +# define GDK_C_h 0xFEA4 +#endif + +#ifndef GDK_C_H +# define GDK_C_H 0xFEA5 +#endif + +#ifndef GDK_MonBrightnessUp +# define GDK_MonBrightnessUp 0x1008FF02 +#endif + +#ifndef GDK_MonBrightnessDown +# define GDK_MonBrightnessDown 0x1008FF03 +#endif + +#ifndef GDK_AudioLowerVolume +# define GDK_AudioLowerVolume 0x1008FF11 +#endif + +#ifndef GDK_AudioMute +# define GDK_AudioMute 0x1008FF12 +#endif + +#ifndef GDK_AudioRaiseVolume +# define GDK_AudioRaiseVolume 0x1008FF13 +#endif + +#ifndef GDK_AudioPlay +# define GDK_AudioPlay 0x1008FF14 +#endif + +#ifndef GDK_AudioStop +# define GDK_AudioStop 0x1008FF15 +#endif + +#ifndef GDK_AudioPrev +# define GDK_AudioPrev 0x1008FF16 +#endif + +#ifndef GDK_AudioNext +# define GDK_AudioNext 0x1008FF17 +#endif + +#ifndef GDK_HomePage +# define GDK_HomePage 0x1008FF18 +#endif + +#ifndef GDK_Mail +# define GDK_Mail 0x1008FF19 +#endif + +#ifndef GDK_Search +# define GDK_Search 0x1008FF1B +#endif + +#ifndef GDK_AudioRecord +# define GDK_AudioRecord 0x1008FF1C +#endif + +#ifndef GDK_Back +# define GDK_Back 0x1008FF26 +#endif + +#ifndef GDK_Forward +# define GDK_Forward 0x1008FF27 +#endif + +#ifndef GDK_Stop +# define GDK_Stop 0x1008FF28 +#endif + +#ifndef GDK_Refresh +# define GDK_Refresh 0x1008FF29 +#endif + +#ifndef GDK_PowerOff +# define GDK_PowerOff 0x1008FF2A +#endif + +#ifndef GDK_Eject +# define GDK_Eject 0x1008FF2C +#endif + +#ifndef GDK_AudioPause +# define GDK_AudioPause 0x1008FF31 +#endif + +#ifndef GDK_BrightnessAdjust +# define GDK_BrightnessAdjust 0x1008FF3B +#endif + +#ifndef GDK_AudioRewind +# define GDK_AudioRewind 0x1008FF3E +#endif + +#ifndef GDK_Launch0 +# define GDK_Launch0 0x1008FF40 +#endif + +#ifndef GDK_Launch1 +# define GDK_Launch1 0x1008FF41 +#endif + +#ifndef GDK_Launch2 +# define GDK_Launch2 0x1008FF42 +#endif + +#ifndef GDK_Launch3 +# define GDK_Launch3 0x1008FF43 +#endif + +#ifndef GDK_Launch4 +# define GDK_Launch4 0x1008FF44 +#endif + +#ifndef GDK_Launch5 +# define GDK_Launch5 0x1008FF45 +#endif + +#ifndef GDK_Launch6 +# define GDK_Launch6 0x1008FF46 +#endif + +#ifndef GDK_Launch7 +# define GDK_Launch7 0x1008FF47 +#endif + +#ifndef GDK_Launch8 +# define GDK_Launch8 0x1008FF48 +#endif + +#ifndef GDK_Launch9 +# define GDK_Launch9 0x1008FF49 +#endif + +#ifndef GDK_LaunchA +# define GDK_LaunchA 0x1008FF4A +#endif + +#ifndef GDK_LaunchB +# define GDK_LaunchB 0x1008FF4B +#endif + +#ifndef GDK_LaunchC +# define GDK_LaunchC 0x1008FF4C +#endif + +#ifndef GDK_LaunchD +# define GDK_LaunchD 0x1008FF4D +#endif + +#ifndef GDK_LaunchE +# define GDK_LaunchE 0x1008FF4E +#endif + +#ifndef GDK_LaunchF +# define GDK_LaunchF 0x1008FF4F +#endif + +#ifndef GDK_Copy +# define GDK_Copy 0x1008FF57 +#endif + +#ifndef GDK_Cut +# define GDK_Cut 0x1008FF58 +#endif + +#ifndef GDK_Paste +# define GDK_Paste 0x1008FF6D +#endif + +#ifndef GDK_Reload +# define GDK_Reload 0x1008FF73 +#endif + +#ifndef GDK_AudioRandomPlay +# define GDK_AudioRandomPlay 0x1008FF99 +#endif + +#ifndef GDK_Subtitle +# define GDK_Subtitle 0x1008FF9A +#endif + +#ifndef GDK_Red +# define GDK_Red 0x1008FFA3 +#endif + +#ifndef GDK_Green +# define GDK_Green 0x1008FFA4 +#endif + +#ifndef GDK_Yellow +# define GDK_Yellow 0x1008FFA5 +#endif + +#ifndef GDK_Blue +# define GDK_Blue 0x1008FFA6 +#endif + +#ifndef GDK_TouchpadToggle +# define GDK_TouchpadToggle 0x1008FFA9 +#endif + +#ifndef GDK_TouchpadOn +# define GDK_TouchpadOn 0x1008FFB0 +#endif + +#ifndef GDK_TouchpadOff +# define GDK_TouchpadOff 0x1008ffb1 +#endif + +#ifndef GDK_LogWindowTree +# define GDK_LogWindowTree 0x1008FE24 +#endif + +#ifndef GDK_LogGrabInfo +# define GDK_LogGrabInfo 0x1008FE25 +#endif + +#ifndef GDK_Sleep +# define GDK_Sleep 0x1008FF2F +#endif + +#endif /* GDKKEYSYMS_WRAPPER_H */ diff --git a/widget/gtk/compat/gdk/gdkvisual.h b/widget/gtk/compat/gdk/gdkvisual.h new file mode 100644 index 0000000000..e5187a78da --- /dev/null +++ b/widget/gtk/compat/gdk/gdkvisual.h @@ -0,0 +1,15 @@ +/* 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 GDKVISUAL_WRAPPER_H +#define GDKVISUAL_WRAPPER_H + +#define gdk_visual_get_depth gdk_visual_get_depth_ +#include_next <gdk/gdkvisual.h> +#undef gdk_visual_get_depth + +static inline gint gdk_visual_get_depth(GdkVisual* visual) { + return visual->depth; +} +#endif /* GDKVISUAL_WRAPPER_H */ diff --git a/widget/gtk/compat/gdk/gdkwindow.h b/widget/gtk/compat/gdk/gdkwindow.h new file mode 100644 index 0000000000..a4d2efbc89 --- /dev/null +++ b/widget/gtk/compat/gdk/gdkwindow.h @@ -0,0 +1,27 @@ +/* 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 GDKWINDOW_WRAPPER_H +#define GDKWINDOW_WRAPPER_H + +#define gdk_window_get_display gdk_window_get_display_ +#define gdk_window_get_screen gdk_window_get_screen_ +#include_next <gdk/gdkwindow.h> +#undef gdk_window_get_display +#undef gdk_window_get_screen + +static inline GdkDisplay* gdk_window_get_display(GdkWindow* window) { + return gdk_drawable_get_display(GDK_DRAWABLE(window)); +} + +static inline GdkScreen* gdk_window_get_screen(GdkWindow* window) { + return gdk_drawable_get_screen(window); +} + +#if GDK_PIXBUF_MAJOR == 2 && GDK_PIXBUF_MINOR < 18 +static inline gboolean gdk_window_is_destroyed(GdkWindow* window) { + return GDK_WINDOW_OBJECT(window)->destroyed; +} +#endif +#endif /* GDKWINDOW_WRAPPER_H */ diff --git a/widget/gtk/compat/gdk/gdkx.h b/widget/gtk/compat/gdk/gdkx.h new file mode 100644 index 0000000000..3d13c88b4d --- /dev/null +++ b/widget/gtk/compat/gdk/gdkx.h @@ -0,0 +1,46 @@ +/* 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 GDKX_WRAPPER_H +#define GDKX_WRAPPER_H + +#include <gtk/gtkversion.h> + +#define gdk_x11_window_foreign_new_for_display \ + gdk_x11_window_foreign_new_for_display_ +#define gdk_x11_window_lookup_for_display gdk_x11_window_lookup_for_display_ +#define gdk_x11_window_get_xid gdk_x11_window_get_xid_ +#if !GTK_CHECK_VERSION(2, 24, 0) +# define gdk_x11_set_sm_client_id gdk_x11_set_sm_client_id_ +#endif +#include_next <gdk/gdkx.h> +#undef gdk_x11_window_foreign_new_for_display +#undef gdk_x11_window_lookup_for_display +#undef gdk_x11_window_get_xid + +static inline GdkWindow* gdk_x11_window_foreign_new_for_display( + GdkDisplay* display, Window window) { + return gdk_window_foreign_new_for_display(display, window); +} + +static inline GdkWindow* gdk_x11_window_lookup_for_display(GdkDisplay* display, + Window window) { + return gdk_window_lookup_for_display(display, window); +} + +static inline Window gdk_x11_window_get_xid(GdkWindow* window) { + return (GDK_WINDOW_XWINDOW(window)); +} + +#ifndef GDK_IS_X11_DISPLAY +# define GDK_IS_X11_DISPLAY(a) (true) +#endif + +#if !GTK_CHECK_VERSION(2, 24, 0) +# undef gdk_x11_set_sm_client_id +static inline void gdk_x11_set_sm_client_id(const gchar* sm_client_id) { + gdk_set_sm_client_id(sm_client_id); +} +#endif +#endif /* GDKX_WRAPPER_H */ diff --git a/widget/gtk/compat/glib/gmem.h b/widget/gtk/compat/glib/gmem.h new file mode 100644 index 0000000000..78f24bbd8d --- /dev/null +++ b/widget/gtk/compat/glib/gmem.h @@ -0,0 +1,48 @@ +/* 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 GMEM_WRAPPER_H +#define GMEM_WRAPPER_H + +#define g_malloc_n g_malloc_n_ +#define g_malloc0_n g_malloc0_n_ +#define g_realloc_n g_realloc_n_ +#include_next <glib/gmem.h> +#undef g_malloc_n +#undef g_malloc0_n +#undef g_realloc_n + +#include <glib/gmessages.h> + +#undef g_new +#define g_new(type, num) ((type*)g_malloc_n((num), sizeof(type))) + +#undef g_new0 +#define g_new0(type, num) ((type*)g_malloc0_n((num), sizeof(type))) + +#undef g_renew +#define g_renew(type, ptr, num) ((type*)g_realloc_n(ptr, (num), sizeof(type))) + +#define _CHECK_OVERFLOW(num, type_size) \ + if (G_UNLIKELY(type_size > 0 && num > G_MAXSIZE / type_size)) { \ + g_error("%s: overflow allocating %" G_GSIZE_FORMAT "*%" G_GSIZE_FORMAT \ + " bytes", \ + G_STRLOC, num, type_size); \ + } + +static inline gpointer g_malloc_n(gsize num, gsize type_size) { + _CHECK_OVERFLOW(num, type_size) + return g_malloc(num * type_size); +} + +static inline gpointer g_malloc0_n(gsize num, gsize type_size) { + _CHECK_OVERFLOW(num, type_size) + return g_malloc0(num * type_size); +} + +static inline gpointer g_realloc_n(gpointer ptr, gsize num, gsize type_size) { + _CHECK_OVERFLOW(num, type_size) + return g_realloc(ptr, num * type_size); +} +#endif /* GMEM_WRAPPER_H */ diff --git a/widget/gtk/compat/gtk/gtkwidget.h b/widget/gtk/compat/gtk/gtkwidget.h new file mode 100644 index 0000000000..21165d61fa --- /dev/null +++ b/widget/gtk/compat/gtk/gtkwidget.h @@ -0,0 +1,43 @@ +/* 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 GTKWIDGET_WRAPPER_H +#define GTKWIDGET_WRAPPER_H + +#define gtk_widget_set_mapped gtk_widget_set_mapped_ +#define gtk_widget_get_mapped gtk_widget_get_mapped_ +#define gtk_widget_set_realized gtk_widget_set_realized_ +#define gtk_widget_get_realized gtk_widget_get_realized_ +#include_next <gtk/gtkwidget.h> +#undef gtk_widget_set_mapped +#undef gtk_widget_get_mapped +#undef gtk_widget_set_realized +#undef gtk_widget_get_realized + +#include <gtk/gtkversion.h> + +static inline void gtk_widget_set_mapped(GtkWidget* widget, gboolean mapped) { + if (mapped) + GTK_WIDGET_SET_FLAGS(widget, GTK_MAPPED); + else + GTK_WIDGET_UNSET_FLAGS(widget, GTK_MAPPED); +} + +static inline gboolean gtk_widget_get_mapped(GtkWidget* widget) { + return GTK_WIDGET_MAPPED(widget); +} + +static inline void gtk_widget_set_realized(GtkWidget* widget, + gboolean realized) { + if (realized) + GTK_WIDGET_SET_FLAGS(widget, GTK_REALIZED); + else + GTK_WIDGET_UNSET_FLAGS(widget, GTK_REALIZED); +} + +static inline gboolean gtk_widget_get_realized(GtkWidget* widget) { + return GTK_WIDGET_REALIZED(widget); +} + +#endif /* GTKWIDGET_WRAPPER_H */ diff --git a/widget/gtk/compat/gtk/gtkwindow.h b/widget/gtk/compat/gtk/gtkwindow.h new file mode 100644 index 0000000000..7c3d5873bd --- /dev/null +++ b/widget/gtk/compat/gtk/gtkwindow.h @@ -0,0 +1,26 @@ +/* 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 GTKWINDOW_WRAPPER_H +#define GTKWINDOW_WRAPPER_H + +#define gtk_window_group_get_current_grab gtk_window_group_get_current_grab_ +#define gtk_window_get_window_type gtk_window_get_window_type_ +#include_next <gtk/gtkwindow.h> +#undef gtk_window_group_get_current_grab +#undef gtk_window_get_window_type + +static inline GtkWidget* gtk_window_group_get_current_grab( + GtkWindowGroup* window_group) { + if (!window_group->grabs) return NULL; + + return GTK_WIDGET(window_group->grabs->data); +} + +static inline GtkWindowType gtk_window_get_window_type(GtkWindow* window) { + gint type; + g_object_get(window, "type", &type, (void*)NULL); + return (GtkWindowType)type; +} +#endif /* GTKWINDOW_WRAPPER_H */ diff --git a/widget/gtk/components.conf b/widget/gtk/components.conf new file mode 100644 index 0000000000..a65c8e02b0 --- /dev/null +++ b/widget/gtk/components.conf @@ -0,0 +1,166 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Headers = [ + '/widget/gtk/nsWidgetFactory.h', +] + +InitFunc = 'nsWidgetGtk2ModuleCtor' +UnloadFunc = 'nsWidgetGtk2ModuleDtor' + +Classes = [ + { + 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}', + 'contract_ids': ['@mozilla.org/widget/appshell/gtk;1'], + 'legacy_constructor': 'nsAppShellConstructor', + 'headers': ['/widget/gtk/nsWidgetFactory.h'], + 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_AND_SOCKET_PROCESS, + }, + { + 'cid': '{c401eb80-f9ea-11d3-bb6f-e732b73ebe7c}', + 'contract_ids': ['@mozilla.org/gfx/screenmanager;1'], + 'singleton': True, + 'type': 'mozilla::widget::ScreenManager', + 'headers': ['mozilla/StaticPtr.h', 'mozilla/widget/ScreenManager.h'], + 'constructor': 'mozilla::widget::ScreenManager::GetAddRefedSingleton', + 'processes': ProcessSelector.ALLOW_IN_GPU_AND_MAIN_PROCESS, + }, + { + 'cid': '{a9339876-0027-430f-b953-84c9c11c2da3}', + 'contract_ids': ['@mozilla.org/widget/taskbarprogress/gtk;1'], + 'type': 'TaskbarProgress', + 'headers': ['/widget/gtk/TaskbarProgress.h'], + }, + { + 'cid': '{0f872c8c-3ee6-46bd-92a2-69652c6b474e}', + 'contract_ids': ['@mozilla.org/colorpicker;1'], + 'type': 'nsColorPicker', + 'headers': ['/widget/gtk/nsColorPicker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{bd57cee8-1dd1-11b2-9fe7-95cf4709aea3}', + 'contract_ids': ['@mozilla.org/filepicker;1'], + 'type': 'nsFilePicker', + 'headers': ['/widget/gtk/nsFilePicker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}', + 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'], + 'type': 'nsHTMLFormatConverter', + 'headers': ['/widget/nsHTMLFormatConverter.h'], + }, + { + 'cid': '{b148eed2-236d-11d3-b35c-00a0cc3c1cde}', + 'contract_ids': ['@mozilla.org/sound;1'], + 'singleton': True, + 'type': 'nsISound', + 'constructor': 'nsSound::GetInstance', + 'headers': ['/widget/gtk/nsSound.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{fc2389b8-c650-4093-9e42-b05e5f0685b7}', + 'contract_ids': ['@mozilla.org/widget/image-to-gdk-pixbuf;1'], + 'type': 'nsImageToPixbuf', + 'headers': ['/widget/gtk/nsImageToPixbuf.h'], + }, + { + 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}', + 'contract_ids': ['@mozilla.org/widget/transferable;1'], + 'type': 'nsTransferable', + 'headers': ['/widget/nsTransferable.h'], + }, + { + 'cid': '{e221df9b-3d66-4045-9a66-5720949f8d10}', + 'contract_ids': ['@mozilla.org/applicationchooser;1'], + 'type': 'nsApplicationChooser', + 'headers': ['/widget/gtk/nsApplicationChooser.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] + +if defined('MOZ_X11'): + Classes += [ + { + 'js_name': 'clipboard', + 'cid': '{8b5314ba-db01-11d2-96ce-0060b0fb9956}', + 'contract_ids': ['@mozilla.org/widget/clipboard;1'], + 'interfaces': ['nsIClipboard'], + 'type': 'nsIClipboard', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + 'overridable': True, + }, + { + 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}', + 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'], + 'type': 'nsClipboardHelper', + 'headers': ['/widget/nsClipboardHelper.h'], + }, + { + 'cid': '{8b5314bb-db01-11d2-96ce-0060b0fb9956}', + 'contract_ids': ['@mozilla.org/widget/dragservice;1'], + 'singleton': True, + 'type': 'nsDragService', + 'headers': ['/widget/gtk/nsDragService.h'], + 'constructor': 'nsDragService::GetInstance', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{d755a760-9f27-11df-0800-200c9a664242}', + 'contract_ids': ['@mozilla.org/gfx/info;1'], + 'type': 'mozilla::widget::GfxInfo', + 'headers': ['/widget/GfxInfoX11.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.ALLOW_IN_GPU_PROCESS, + }, + { + 'cid': '{6987230e-0098-4e78-bc5f-1493ee7519fa}', + 'contract_ids': ['@mozilla.org/widget/useridleservice;1'], + 'singleton': True, + 'type': 'nsUserIdleService', + 'headers': ['/widget/gtk/nsUserIdleServiceGTK.h'], + 'constructor': 'nsUserIdleServiceGTK::GetInstance', + }, + ] + +if defined('NS_PRINTING'): + Classes += [ + { + 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}', + 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'], + 'type': 'nsDeviceContextSpecGTK', + 'headers': ['/widget/gtk/nsDeviceContextSpecG.h'], + }, + { + 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}', + 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'], + 'type': 'nsPrintDialogServiceGTK', + 'headers': ['/widget/gtk/nsPrintDialogGTK.h'], + 'init_method': 'Init', + }, + { + 'cid': '{2f977d53-5485-11d4-87e2-0010a4e75ef2}', + 'contract_ids': ['@mozilla.org/gfx/printsession;1'], + 'type': 'nsPrintSession', + 'headers': ['/widget/nsPrintSession.h'], + 'init_method': 'Init', + }, + { + 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}', + 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'], + 'type': 'nsPrintSettingsServiceGTK', + 'headers': ['/widget/gtk/nsPrintSettingsServiceGTK.h'], + 'init_method': 'Init', + }, + { + 'cid': '{a6cf9129-15b3-11d2-932e-00805f8add32}', + 'contract_ids': ['@mozilla.org/gfx/printerlist;1'], + 'type': 'nsPrinterListCUPS', + 'headers': ['/widget/nsPrinterListCUPS.h'], + }, + ] diff --git a/widget/gtk/crashtests/540078-1.xhtml b/widget/gtk/crashtests/540078-1.xhtml new file mode 100644 index 0000000000..f5de1b9aee --- /dev/null +++ b/widget/gtk/crashtests/540078-1.xhtml @@ -0,0 +1 @@ +<html xmlns="http://www.w3.org/1999/xhtml"><hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><scrollcorner class="zebra"/></hbox><style style="display: none;">.zebra { -moz-appearance: checkbox; }</style></html> diff --git a/widget/gtk/crashtests/673390-1.html b/widget/gtk/crashtests/673390-1.html new file mode 100644 index 0000000000..8463f67f05 --- /dev/null +++ b/widget/gtk/crashtests/673390-1.html @@ -0,0 +1 @@ +<div style="-moz-appearance: progresschunk; position: fixed"></div> diff --git a/widget/gtk/crashtests/crashtests.list b/widget/gtk/crashtests/crashtests.list new file mode 100644 index 0000000000..9ae47c2233 --- /dev/null +++ b/widget/gtk/crashtests/crashtests.list @@ -0,0 +1,2 @@ +load 540078-1.xhtml +load 673390-1.html diff --git a/widget/gtk/gtk3drawing.cpp b/widget/gtk/gtk3drawing.cpp new file mode 100644 index 0000000000..c8a39bfd5c --- /dev/null +++ b/widget/gtk/gtk3drawing.cpp @@ -0,0 +1,3214 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file contains painting functions for each of the gtk2 widgets. + * Adapted from the gtkdrawing.c, and gtk+2.0 source. + */ + +#include <gtk/gtk.h> +#include <gdk/gdkprivate.h> +#include <string.h> +#include "gtkdrawing.h" +#include "mozilla/Assertions.h" +#include "mozilla/ScopeExit.h" +#include "prinrval.h" +#include "WidgetStyleCache.h" +#include "nsString.h" +#include "nsDebug.h" + +#include <math.h> +#include <dlfcn.h> + +static gboolean checkbox_check_state; +static gboolean notebook_has_tab_gap; + +static ScrollbarGTKMetrics sScrollbarMetrics[2]; +static ScrollbarGTKMetrics sActiveScrollbarMetrics[2]; +static ToggleGTKMetrics sCheckboxMetrics; +static ToggleGTKMetrics sRadioMetrics; +static ToggleGTKMetrics sMenuRadioMetrics; +static ToggleGTKMetrics sMenuCheckboxMetrics; +static ToolbarGTKMetrics sToolbarMetrics; +static CSDWindowDecorationSize sToplevelWindowDecorationSize; +static CSDWindowDecorationSize sPopupWindowDecorationSize; + +using mozilla::Span; + +#define ARROW_UP 0 +#define ARROW_DOWN G_PI +#define ARROW_RIGHT G_PI_2 +#define ARROW_LEFT (G_PI + G_PI_2) + +#if 0 +// It's used for debugging only to compare Gecko widget style with +// the ones used by Gtk+ applications. +static void +style_path_print(GtkStyleContext *context) +{ + const GtkWidgetPath* path = gtk_style_context_get_path(context); + + static auto sGtkWidgetPathToStringPtr = + (char * (*)(const GtkWidgetPath *)) + dlsym(RTLD_DEFAULT, "gtk_widget_path_to_string"); + + fprintf(stderr, "Style path:\n%s\n\n", sGtkWidgetPathToStringPtr(path)); +} +#endif + +static GtkBorder operator-(const GtkBorder& first, const GtkBorder& second) { + GtkBorder result; + result.left = first.left - second.left; + result.right = first.right - second.right; + result.top = first.top - second.top; + result.bottom = first.bottom - second.bottom; + return result; +} + +static GtkBorder operator+(const GtkBorder& first, const GtkBorder& second) { + GtkBorder result; + result.left = first.left + second.left; + result.right = first.right + second.right; + result.top = first.top + second.top; + result.bottom = first.bottom + second.bottom; + return result; +} + +static GtkBorder operator+=(GtkBorder& first, const GtkBorder& second) { + first.left += second.left; + first.right += second.right; + first.top += second.top; + first.bottom += second.bottom; + return first; +} + +static gint moz_gtk_get_tab_thickness(GtkStyleContext* style); + +static gint moz_gtk_menu_item_paint(WidgetNodeType widget, cairo_t* cr, + GdkRectangle* rect, GtkWidgetState* state, + GtkTextDirection direction); + +static GtkBorder GetMarginBorderPadding(GtkStyleContext* aStyle); + +static void Inset(GdkRectangle*, const GtkBorder&); + +static void InsetByMargin(GdkRectangle*, GtkStyleContext* style); + +static void moz_gtk_add_style_margin(GtkStyleContext* style, gint* left, + gint* top, gint* right, gint* bottom) { + GtkBorder margin; + + gtk_style_context_get_margin(style, gtk_style_context_get_state(style), + &margin); + *left += margin.left; + *right += margin.right; + *top += margin.top; + *bottom += margin.bottom; +} + +static void moz_gtk_add_style_border(GtkStyleContext* style, gint* left, + gint* top, gint* right, gint* bottom) { + GtkBorder border; + + gtk_style_context_get_border(style, gtk_style_context_get_state(style), + &border); + + *left += border.left; + *right += border.right; + *top += border.top; + *bottom += border.bottom; +} + +static void moz_gtk_add_style_padding(GtkStyleContext* style, gint* left, + gint* top, gint* right, gint* bottom) { + GtkBorder padding; + + gtk_style_context_get_padding(style, gtk_style_context_get_state(style), + &padding); + + *left += padding.left; + *right += padding.right; + *top += padding.top; + *bottom += padding.bottom; +} + +static void moz_gtk_add_margin_border_padding(GtkStyleContext* style, + gint* left, gint* top, + gint* right, gint* bottom) { + moz_gtk_add_style_margin(style, left, top, right, bottom); + moz_gtk_add_style_border(style, left, top, right, bottom); + moz_gtk_add_style_padding(style, left, top, right, bottom); +} + +static void moz_gtk_add_border_padding(GtkStyleContext* style, gint* left, + gint* top, gint* right, gint* bottom) { + moz_gtk_add_style_border(style, left, top, right, bottom); + moz_gtk_add_style_padding(style, left, top, right, bottom); +} + +// In case there's an error in Gtk theme and preferred size is zero, +// return some sane values to pass mozilla automation tests. +// It should not happen in real-life. +#define MIN_WIDGET_SIZE 10 +static void moz_gtk_sanity_preferred_size(GtkRequisition* requisition) { + if (requisition->width <= 0) { + requisition->width = MIN_WIDGET_SIZE; + } + if (requisition->height <= 0) { + requisition->height = MIN_WIDGET_SIZE; + } +} + +// GetStateFlagsFromGtkWidgetState() can be safely used for the specific +// GtkWidgets that set both prelight and active flags. For other widgets, +// either the GtkStateFlags or Gecko's GtkWidgetState need to be carefully +// adjusted to match GTK behavior. Although GTK sets insensitive and focus +// flags in the generic GtkWidget base class, GTK adds prelight and active +// flags only to widgets that are expected to demonstrate prelight or active +// states. This contrasts with HTML where any element may have :active and +// :hover states, and so Gecko's GtkStateFlags do not necessarily map to GTK +// flags. Failure to restrict the flags in the same way as GTK can cause +// generic CSS selectors from some themes to unintentionally match elements +// that are not expected to change appearance on hover or mouse-down. +static GtkStateFlags GetStateFlagsFromGtkWidgetState(GtkWidgetState* state) { + GtkStateFlags stateFlags = GTK_STATE_FLAG_NORMAL; + + if (state->disabled) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + else { + if (state->depressed || state->active) + stateFlags = + static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_ACTIVE); + if (state->inHover) + stateFlags = + static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_PRELIGHT); + if (state->focused) + stateFlags = + static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_FOCUSED); + if (state->backdrop) + stateFlags = + static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_BACKDROP); + } + + return stateFlags; +} + +static GtkStateFlags GetStateFlagsFromGtkTabFlags(GtkTabFlags flags) { + return ((flags & MOZ_GTK_TAB_SELECTED) == 0) ? GTK_STATE_FLAG_NORMAL + : GTK_STATE_FLAG_ACTIVE; +} + +gint moz_gtk_init() { + if (gtk_major_version > 3 || + (gtk_major_version == 3 && gtk_minor_version >= 14)) + checkbox_check_state = GTK_STATE_FLAG_CHECKED; + else + checkbox_check_state = GTK_STATE_FLAG_ACTIVE; + + moz_gtk_refresh(); + + return MOZ_GTK_SUCCESS; +} + +void moz_gtk_refresh() { + if (gtk_check_version(3, 20, 0) != nullptr) { + // Deprecated for Gtk >= 3.20+ + GtkStyleContext* style = GetStyleContext(MOZ_GTK_TAB_TOP); + gtk_style_context_get_style(style, "has-tab-gap", ¬ebook_has_tab_gap, + NULL); + } else { + notebook_has_tab_gap = true; + } + + sScrollbarMetrics[GTK_ORIENTATION_HORIZONTAL].initialized = false; + sScrollbarMetrics[GTK_ORIENTATION_VERTICAL].initialized = false; + sActiveScrollbarMetrics[GTK_ORIENTATION_HORIZONTAL].initialized = false; + sActiveScrollbarMetrics[GTK_ORIENTATION_VERTICAL].initialized = false; + sCheckboxMetrics.initialized = false; + sRadioMetrics.initialized = false; + sMenuCheckboxMetrics.initialized = false; + sMenuRadioMetrics.initialized = false; + sToolbarMetrics.initialized = false; + sToplevelWindowDecorationSize.initialized = false; + sPopupWindowDecorationSize.initialized = false; + + /* This will destroy all of our widgets */ + ResetWidgetCache(); +} + +static gint moz_gtk_get_focus_outline_size(GtkStyleContext* style, + gint* focus_h_width, + gint* focus_v_width) { + GtkBorder border; + gtk_style_context_get_border(style, gtk_style_context_get_state(style), + &border); + *focus_h_width = border.left; + *focus_v_width = border.top; + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_get_focus_outline_size(gint* focus_h_width, gint* focus_v_width) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_ENTRY); + moz_gtk_get_focus_outline_size(style, focus_h_width, focus_v_width); + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_menuitem_get_horizontal_padding(gint* horizontal_padding) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_MENUITEM); + gtk_style_context_get_style(style, "horizontal-padding", horizontal_padding, + nullptr); + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_checkmenuitem_get_horizontal_padding(gint* horizontal_padding) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_CHECKMENUITEM); + gtk_style_context_get_style(style, "horizontal-padding", horizontal_padding, + nullptr); + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_button_get_default_overflow(gint* border_top, gint* border_left, + gint* border_bottom, + gint* border_right) { + GtkBorder* default_outside_border; + + GtkStyleContext* style = GetStyleContext(MOZ_GTK_BUTTON); + gtk_style_context_get_style(style, "default-outside-border", + &default_outside_border, NULL); + + if (default_outside_border) { + *border_top = default_outside_border->top; + *border_left = default_outside_border->left; + *border_bottom = default_outside_border->bottom; + *border_right = default_outside_border->right; + gtk_border_free(default_outside_border); + } else { + *border_top = *border_left = *border_bottom = *border_right = 0; + } + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_button_get_default_border(gint* border_top, + gint* border_left, + gint* border_bottom, + gint* border_right) { + GtkBorder* default_border; + + GtkStyleContext* style = GetStyleContext(MOZ_GTK_BUTTON); + gtk_style_context_get_style(style, "default-border", &default_border, NULL); + + if (default_border) { + *border_top = default_border->top; + *border_left = default_border->left; + *border_bottom = default_border->bottom; + *border_right = default_border->right; + gtk_border_free(default_border); + } else { + /* see gtkbutton.c */ + *border_top = *border_left = *border_bottom = *border_right = 1; + } + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_splitter_get_metrics(gint orientation, gint* size) { + GtkStyleContext* style; + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + style = GetStyleContext(MOZ_GTK_SPLITTER_HORIZONTAL); + } else { + style = GetStyleContext(MOZ_GTK_SPLITTER_VERTICAL); + } + gtk_style_context_get_style(style, "handle_size", size, NULL); + return MOZ_GTK_SUCCESS; +} + +static void CalculateToolbarButtonMetrics(WidgetNodeType aAppearance, + ToolbarButtonGTKMetrics* aMetrics) { + gint iconWidth, iconHeight; + if (!gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &iconWidth, &iconHeight)) { + NS_WARNING("Failed to get Gtk+ icon size for titlebar button!"); + + // Use some reasonable fallback size + iconWidth = 16; + iconHeight = 16; + } + + GtkStyleContext* style = GetStyleContext(aAppearance); + gint width = 0, height = 0; + if (gtk_check_version(3, 20, 0) == nullptr) { + gtk_style_context_get(style, gtk_style_context_get_state(style), + "min-width", &width, "min-height", &height, NULL); + } + + // Cover cases when min-width/min-height is not set, it's invalid + // or we're running on Gtk+ < 3.20. + if (width < iconWidth) width = iconWidth; + if (height < iconHeight) height = iconHeight; + + gint left = 0, top = 0, right = 0, bottom = 0; + moz_gtk_add_border_padding(style, &left, &top, &right, &bottom); + + // Button size is calculated as min-width/height + border/padding. + width += left + right; + height += top + bottom; + + // Place icon at button center. + aMetrics->iconXPosition = (width - iconWidth) / 2; + aMetrics->iconYPosition = (height - iconHeight) / 2; + + aMetrics->minSizeWithBorderMargin.width = width; + aMetrics->minSizeWithBorderMargin.height = height; +} + +// We support LTR layout only here for now. +static void CalculateToolbarButtonSpacing(WidgetNodeType aAppearance, + ToolbarButtonGTKMetrics* aMetrics) { + GtkStyleContext* style = GetStyleContext(aAppearance); + gtk_style_context_get_margin(style, gtk_style_context_get_state(style), + &aMetrics->buttonMargin); + + // Get titlebar spacing, a default one is 6 pixels (gtk/gtkheaderbar.c) + gint buttonSpacing = 6; + g_object_get(GetWidget(MOZ_GTK_HEADER_BAR), "spacing", &buttonSpacing, + nullptr); + + // We apply spacing as a margin equaly to both adjacent buttons. + buttonSpacing /= 2; + + if (!aMetrics->firstButton) { + aMetrics->buttonMargin.left += buttonSpacing; + } + if (!aMetrics->lastButton) { + aMetrics->buttonMargin.right += buttonSpacing; + } + + aMetrics->minSizeWithBorderMargin.width += + aMetrics->buttonMargin.right + aMetrics->buttonMargin.left; + aMetrics->minSizeWithBorderMargin.height += + aMetrics->buttonMargin.top + aMetrics->buttonMargin.bottom; +} + +size_t GetGtkHeaderBarButtonLayout(Span<ButtonLayout> aButtonLayout, + bool* aReversedButtonsPlacement) { + gchar* decorationLayoutSetting = nullptr; + GtkSettings* settings = gtk_settings_get_default(); + g_object_get(settings, "gtk-decoration-layout", &decorationLayoutSetting, + nullptr); + auto free = mozilla::MakeScopeExit([&] { g_free(decorationLayoutSetting); }); + + // Use a default layout + const gchar* decorationLayout = "menu:minimize,maximize,close"; + if (decorationLayoutSetting) { + decorationLayout = decorationLayoutSetting; + } + + // "minimize,maximize,close:" layout means buttons are on the opposite + // titlebar side. close button is always there. + if (aReversedButtonsPlacement) { + const char* closeButton = strstr(decorationLayout, "close"); + const char* separator = strchr(decorationLayout, ':'); + *aReversedButtonsPlacement = + closeButton && separator && closeButton < separator; + } + + // We check what position a button string is stored in decorationLayout. + // + // decorationLayout gets its value from the GNOME preference: + // org.gnome.desktop.vm.preferences.button-layout via the + // gtk-decoration-layout property. + // + // Documentation of the gtk-decoration-layout property can be found here: + // https://developer.gnome.org/gtk3/stable/GtkSettings.html#GtkSettings--gtk-decoration-layout + if (aButtonLayout.IsEmpty()) { + return 0; + } + + nsDependentCSubstring layout(decorationLayout, strlen(decorationLayout)); + + bool right = false; + size_t activeButtons = 0; + for (const auto& part : layout.Split(':')) { + for (const auto& button : part.Split(',')) { + if (button.EqualsLiteral("close")) { + aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_CLOSE, + right}; + } else if (button.EqualsLiteral("minimize")) { + aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE, + right}; + } else if (button.EqualsLiteral("maximize")) { + aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE, + right}; + } + if (activeButtons == aButtonLayout.Length()) { + return activeButtons; + } + } + right = true; + } + return activeButtons; +} + +static void EnsureToolbarMetrics(void) { + if (!sToolbarMetrics.initialized) { + // Make sure we have clean cache after theme reset, etc. + memset(&sToolbarMetrics, 0, sizeof(sToolbarMetrics)); + + // Calculate titlebar button visibility and positions. + ButtonLayout aButtonLayout[TOOLBAR_BUTTONS]; + size_t activeButtonNums = + GetGtkHeaderBarButtonLayout(mozilla::Span(aButtonLayout), nullptr); + + for (size_t i = 0; i < activeButtonNums; i++) { + int buttonIndex = + (aButtonLayout[i].mType - MOZ_GTK_HEADER_BAR_BUTTON_CLOSE); + ToolbarButtonGTKMetrics* metrics = sToolbarMetrics.button + buttonIndex; + metrics->visible = true; + // Mark first button + if (!i) { + metrics->firstButton = true; + } + // Mark last button. + if (i == (activeButtonNums - 1)) { + metrics->lastButton = true; + } + + CalculateToolbarButtonMetrics(aButtonLayout[i].mType, metrics); + CalculateToolbarButtonSpacing(aButtonLayout[i].mType, metrics); + } + + sToolbarMetrics.initialized = true; + } +} + +const ToolbarButtonGTKMetrics* GetToolbarButtonMetrics( + WidgetNodeType aAppearance) { + EnsureToolbarMetrics(); + + int buttonIndex = (aAppearance - MOZ_GTK_HEADER_BAR_BUTTON_CLOSE); + NS_ASSERTION(buttonIndex >= 0 && buttonIndex <= TOOLBAR_BUTTONS, + "GetToolbarButtonMetrics(): Wrong titlebar button!"); + return sToolbarMetrics.button + buttonIndex; +} + +static gint moz_gtk_window_paint(cairo_t* cr, GdkRectangle* rect, + GtkTextDirection direction) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_WINDOW, direction); + + gtk_style_context_save(style); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_BACKGROUND); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_style_context_restore(style); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_button_paint(cairo_t* cr, const GdkRectangle* rect, + GtkWidgetState* state, GtkReliefStyle relief, + GtkWidget* widget, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = gtk_widget_get_style_context(widget); + gint x = rect->x, y = rect->y, width = rect->width, height = rect->height; + + gtk_widget_set_direction(widget, direction); + + gtk_style_context_save(style); + StyleContextSetScale(style, state->scale); + gtk_style_context_set_state(style, state_flags); + + if (state->isDefault && relief == GTK_RELIEF_NORMAL) { + /* handle default borders both outside and inside the button */ + gint default_top, default_left, default_bottom, default_right; + moz_gtk_button_get_default_overflow(&default_top, &default_left, + &default_bottom, &default_right); + x -= default_left; + y -= default_top; + width += default_left + default_right; + height += default_top + default_bottom; + gtk_render_background(style, cr, x, y, width, height); + gtk_render_frame(style, cr, x, y, width, height); + moz_gtk_button_get_default_border(&default_top, &default_left, + &default_bottom, &default_right); + x += default_left; + y += default_top; + width -= (default_left + default_right); + height -= (default_top + default_bottom); + } else if (relief != GTK_RELIEF_NONE || state->depressed || + (state_flags & GTK_STATE_FLAG_PRELIGHT)) { + /* the following line can trigger an assertion (Crux theme) + file ../../gdk/gdkwindow.c: line 1846 (gdk_window_clear_area): + assertion `GDK_IS_WINDOW (window)' failed */ + gtk_render_background(style, cr, x, y, width, height); + gtk_render_frame(style, cr, x, y, width, height); + } + + if (state->focused) { + GtkBorder border; + gtk_style_context_get_border(style, state_flags, &border); + x += border.left; + y += border.top; + width -= (border.left + border.right); + height -= (border.top + border.bottom); + gtk_render_focus(style, cr, x, y, width, height); + } + gtk_style_context_restore(style); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_header_bar_button_paint(cairo_t* cr, + const GdkRectangle* aRect, + GtkWidgetState* state, + GtkReliefStyle relief, + WidgetNodeType aIconWidgetType, + GtkTextDirection direction) { + WidgetNodeType buttonWidgetType = + (aIconWidgetType == MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE) + ? MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE + : aIconWidgetType; + + GdkRectangle rect = *aRect; + // We need to inset our calculated margin because it also + // contains titlebar button spacing. + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(buttonWidgetType); + Inset(&rect, metrics->buttonMargin); + + GtkWidget* buttonWidget = GetWidget(buttonWidgetType); + moz_gtk_button_paint(cr, &rect, state, relief, buttonWidget, direction); + + GtkWidget* iconWidget = + gtk_bin_get_child(GTK_BIN(GetWidget(aIconWidgetType))); + cairo_surface_t* surface = GetWidgetIconSurface(iconWidget, state->scale); + + if (surface) { + GtkStyleContext* style = gtk_widget_get_style_context(buttonWidget); + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + + gtk_style_context_save(style); + StyleContextSetScale(style, state->scale); + gtk_style_context_set_state(style, state_flags); + + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(buttonWidgetType); + + /* This is available since Gtk+ 3.10 as well as GtkHeaderBar */ + static auto sGtkRenderIconSurfacePtr = + (void (*)(GtkStyleContext*, cairo_t*, cairo_surface_t*, gdouble, + gdouble))dlsym(RTLD_DEFAULT, "gtk_render_icon_surface"); + + sGtkRenderIconSurfacePtr(style, cr, surface, + rect.x + metrics->iconXPosition, + rect.y + metrics->iconYPosition); + gtk_style_context_restore(style); + } + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_toggle_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, gboolean selected, + gboolean inconsistent, gboolean isradio, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + gint x, y, width, height; + GtkStyleContext* style; + + // We need to call this before GetStyleContext, because otherwise we would + // reset state flags + const ToggleGTKMetrics* metrics = + GetToggleMetrics(isradio ? MOZ_GTK_RADIOBUTTON : MOZ_GTK_CHECKBUTTON); + // Clamp the rect and paint it center aligned in the rect. + x = rect->x; + y = rect->y; + width = rect->width; + height = rect->height; + + if (rect->width < rect->height) { + y = rect->y + (rect->height - rect->width) / 2; + height = rect->width; + } + + if (rect->height < rect->width) { + x = rect->x + (rect->width - rect->height) / 2; + width = rect->height; + } + + if (selected) + state_flags = + static_cast<GtkStateFlags>(state_flags | checkbox_check_state); + + if (inconsistent) + state_flags = + static_cast<GtkStateFlags>(state_flags | GTK_STATE_FLAG_INCONSISTENT); + + style = GetStyleContext(isradio ? MOZ_GTK_RADIOBUTTON : MOZ_GTK_CHECKBUTTON, + state->scale, direction, state_flags); + + if (gtk_check_version(3, 20, 0) == nullptr) { + gtk_render_background(style, cr, x, y, width, height); + gtk_render_frame(style, cr, x, y, width, height); + // Indicator is inset by the toggle's padding and border. + gint indicator_x = x + metrics->borderAndPadding.left; + gint indicator_y = y + metrics->borderAndPadding.top; + gint indicator_width = metrics->minSizeWithBorder.width - + metrics->borderAndPadding.left - + metrics->borderAndPadding.right; + gint indicator_height = metrics->minSizeWithBorder.height - + metrics->borderAndPadding.top - + metrics->borderAndPadding.bottom; + if (isradio) { + gtk_render_option(style, cr, indicator_x, indicator_y, indicator_width, + indicator_height); + } else { + gtk_render_check(style, cr, indicator_x, indicator_y, indicator_width, + indicator_height); + } + } else { + if (isradio) { + gtk_render_option(style, cr, x, y, width, height); + } else { + gtk_render_check(style, cr, x, y, width, height); + } + } + + return MOZ_GTK_SUCCESS; +} + +static gint calculate_button_inner_rect(GtkWidget* button, + const GdkRectangle* rect, + GdkRectangle* inner_rect, + GtkTextDirection direction) { + GtkStyleContext* style; + GtkBorder border; + GtkBorder padding = {0, 0, 0, 0}; + + style = gtk_widget_get_style_context(button); + + /* This mirrors gtkbutton's child positioning */ + gtk_style_context_get_border(style, gtk_style_context_get_state(style), + &border); + gtk_style_context_get_padding(style, gtk_style_context_get_state(style), + &padding); + + inner_rect->x = rect->x + border.left + padding.left; + inner_rect->y = rect->y + padding.top + border.top; + inner_rect->width = + MAX(1, rect->width - padding.left - padding.right - border.left * 2); + inner_rect->height = + MAX(1, rect->height - padding.top - padding.bottom - border.top * 2); + + return MOZ_GTK_SUCCESS; +} + +static gint calculate_arrow_rect(GtkWidget* arrow, GdkRectangle* rect, + GdkRectangle* arrow_rect, + GtkTextDirection direction) { + /* defined in gtkarrow.c */ + gfloat arrow_scaling = 0.7; + gfloat xalign, xpad; + gint extent; + gint mxpad, mypad; + gfloat mxalign, myalign; + GtkMisc* misc = GTK_MISC(arrow); + + gtk_style_context_get_style(gtk_widget_get_style_context(arrow), + "arrow_scaling", &arrow_scaling, NULL); + + gtk_misc_get_padding(misc, &mxpad, &mypad); + extent = MIN((rect->width - mxpad * 2), (rect->height - mypad * 2)) * + arrow_scaling; + + gtk_misc_get_alignment(misc, &mxalign, &myalign); + + xalign = direction == GTK_TEXT_DIR_LTR ? mxalign : 1.0 - mxalign; + xpad = mxpad + (rect->width - extent) * xalign; + + arrow_rect->x = direction == GTK_TEXT_DIR_LTR ? floor(rect->x + xpad) + : ceil(rect->x + xpad); + arrow_rect->y = floor(rect->y + mypad + ((rect->height - extent) * myalign)); + + arrow_rect->width = arrow_rect->height = extent; + + return MOZ_GTK_SUCCESS; +} + +static MozGtkSize GetMinContentBox(GtkStyleContext* style) { + GtkStateFlags state_flags = gtk_style_context_get_state(style); + gint width, height; + gtk_style_context_get(style, state_flags, "min-width", &width, "min-height", + &height, nullptr); + return {width, height}; +} + +/** + * Get minimum widget size as sum of margin, padding, border and + * min-width/min-height. + */ +static void moz_gtk_get_widget_min_size(GtkStyleContext* style, int* width, + int* height) { + GtkStateFlags state_flags = gtk_style_context_get_state(style); + gtk_style_context_get(style, state_flags, "min-height", height, "min-width", + width, nullptr); + + GtkBorder border, padding, margin; + gtk_style_context_get_border(style, state_flags, &border); + gtk_style_context_get_padding(style, state_flags, &padding); + gtk_style_context_get_margin(style, state_flags, &margin); + + *width += border.left + border.right + margin.left + margin.right + + padding.left + padding.right; + *height += border.top + border.bottom + margin.top + margin.bottom + + padding.top + padding.bottom; +} + +static MozGtkSize GetMinMarginBox(GtkStyleContext* style) { + gint width, height; + moz_gtk_get_widget_min_size(style, &width, &height); + return {width, height}; +} + +static void Inset(GdkRectangle* rect, const GtkBorder& aBorder) { + rect->x += aBorder.left; + rect->y += aBorder.top; + rect->width -= aBorder.left + aBorder.right; + rect->height -= aBorder.top + aBorder.bottom; +} + +// Inset a rectangle by the margins specified in a style context. +static void InsetByMargin(GdkRectangle* rect, GtkStyleContext* style) { + GtkBorder margin; + gtk_style_context_get_margin(style, gtk_style_context_get_state(style), + &margin); + Inset(rect, margin); +} + +// Inset a rectangle by the border and padding specified in a style context. +static void InsetByBorderPadding(GdkRectangle* rect, GtkStyleContext* style) { + GtkStateFlags state = gtk_style_context_get_state(style); + GtkBorder padding, border; + + gtk_style_context_get_padding(style, state, &padding); + Inset(rect, padding); + gtk_style_context_get_border(style, state, &border); + Inset(rect, border); +} + +static gint moz_gtk_scrollbar_button_paint(cairo_t* cr, + const GdkRectangle* aRect, + GtkWidgetState* state, + GtkScrollbarButtonFlags flags, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GdkRectangle arrow_rect; + gdouble arrow_angle; + GtkStyleContext* style; + gint arrow_displacement_x, arrow_displacement_y; + + GtkWidget* scrollbar = GetWidget(flags & MOZ_GTK_STEPPER_VERTICAL + ? MOZ_GTK_SCROLLBAR_VERTICAL + : MOZ_GTK_SCROLLBAR_HORIZONTAL); + + gtk_widget_set_direction(scrollbar, direction); + + if (flags & MOZ_GTK_STEPPER_VERTICAL) { + arrow_angle = (flags & MOZ_GTK_STEPPER_DOWN) ? ARROW_DOWN : ARROW_UP; + } else { + arrow_angle = (flags & MOZ_GTK_STEPPER_DOWN) ? ARROW_RIGHT : ARROW_LEFT; + } + + style = gtk_widget_get_style_context(scrollbar); + + gtk_style_context_save(style); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_BUTTON); + StyleContextSetScale(style, state->scale); + gtk_style_context_set_state(style, state_flags); + if (arrow_angle == ARROW_RIGHT) { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_RIGHT); + } else if (arrow_angle == ARROW_DOWN) { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_BOTTOM); + } else if (arrow_angle == ARROW_LEFT) { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_LEFT); + } else { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_TOP); + } + + GdkRectangle rect = *aRect; + if (gtk_check_version(3, 20, 0) == nullptr) { + // The "trough-border" is not used since GTK 3.20. The stepper margin + // box occupies the full width of the "contents" gadget content box. + InsetByMargin(&rect, style); + } else { + // Scrollbar button has to be inset by trough_border because its DOM + // element is filling width of vertical scrollbar's track (or height + // in case of horizontal scrollbars). + GtkOrientation orientation = flags & MOZ_GTK_STEPPER_VERTICAL + ? GTK_ORIENTATION_VERTICAL + : GTK_ORIENTATION_HORIZONTAL; + + const ScrollbarGTKMetrics* metrics = GetScrollbarMetrics(orientation); + if (flags & MOZ_GTK_STEPPER_VERTICAL) { + rect.x += metrics->border.track.left; + rect.width = metrics->size.thumb.width; + } else { + rect.y += metrics->border.track.top; + rect.height = metrics->size.thumb.height; + } + } + + gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height); + gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height); + + arrow_rect.width = rect.width / 2; + arrow_rect.height = rect.height / 2; + + gfloat arrow_scaling; + gtk_style_context_get_style(style, "arrow-scaling", &arrow_scaling, NULL); + + gdouble arrow_size = MIN(rect.width, rect.height) * arrow_scaling; + arrow_rect.x = rect.x + (rect.width - arrow_size) / 2; + arrow_rect.y = rect.y + (rect.height - arrow_size) / 2; + + if (state_flags & GTK_STATE_FLAG_ACTIVE) { + gtk_style_context_get_style(style, "arrow-displacement-x", + &arrow_displacement_x, "arrow-displacement-y", + &arrow_displacement_y, NULL); + + arrow_rect.x += arrow_displacement_x; + arrow_rect.y += arrow_displacement_y; + } + + gtk_render_arrow(style, cr, arrow_angle, arrow_rect.x, arrow_rect.y, + arrow_size); + + gtk_style_context_restore(style); + + return MOZ_GTK_SUCCESS; +} + +static void moz_gtk_update_scrollbar_style(GtkStyleContext* style, + WidgetNodeType widget, + GtkTextDirection direction) { + if (widget == MOZ_GTK_SCROLLBAR_HORIZONTAL) { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_BOTTOM); + } else { + if (direction == GTK_TEXT_DIR_LTR) { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_RIGHT); + gtk_style_context_remove_class(style, GTK_STYLE_CLASS_LEFT); + } else { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_LEFT); + gtk_style_context_remove_class(style, GTK_STYLE_CLASS_RIGHT); + } + } +} + +static void moz_gtk_draw_styled_frame(GtkStyleContext* style, cairo_t* cr, + const GdkRectangle* aRect, + bool drawFocus) { + GdkRectangle rect = *aRect; + + InsetByMargin(&rect, style); + + gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height); + gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height); + if (drawFocus) { + gtk_render_focus(style, cr, rect.x, rect.y, rect.width, rect.height); + } +} + +static gint moz_gtk_scrollbar_trough_paint(WidgetNodeType widget, cairo_t* cr, + const GdkRectangle* aRect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GdkRectangle rect = *aRect; + GtkStyleContext* style; + + if (gtk_get_minor_version() >= 20) { + WidgetNodeType thumb = widget == MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL + ? MOZ_GTK_SCROLLBAR_THUMB_VERTICAL + : MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL; + MozGtkSize thumbSize = GetMinMarginBox(GetStyleContext(thumb)); + style = GetStyleContext(widget, state->scale, direction, state_flags); + MozGtkSize trackSize = GetMinContentBox(style); + trackSize.Include(thumbSize); + trackSize += GetMarginBorderPadding(style); + // Gecko's trough |aRect| fills available breadth, but GTK's trough is + // centered in the contents_gadget. The centering here round left + // and up, like gtk_box_gadget_allocate_child(). + if (widget == MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL) { + rect.x += (rect.width - trackSize.width) / 2; + rect.width = trackSize.width; + } else { + rect.y += (rect.height - trackSize.height) / 2; + rect.height = trackSize.height; + } + } else { + style = GetStyleContext(widget, state->scale, direction, state_flags); + } + + moz_gtk_draw_styled_frame(style, cr, &rect, state->focused); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_scrollbar_paint(WidgetNodeType widget, cairo_t* cr, + const GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = + GetStyleContext(widget, state->scale, direction, state_flags); + + moz_gtk_update_scrollbar_style(style, widget, direction); + + moz_gtk_draw_styled_frame(style, cr, rect, state->focused); + + style = GetStyleContext((widget == MOZ_GTK_SCROLLBAR_HORIZONTAL) + ? MOZ_GTK_SCROLLBAR_CONTENTS_HORIZONTAL + : MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL, + state->scale, direction, state_flags); + moz_gtk_draw_styled_frame(style, cr, rect, state->focused); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_scrollbar_thumb_paint(WidgetNodeType widget, cairo_t* cr, + const GdkRectangle* aRect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = + GetStyleContext(widget, state->scale, direction, state_flags); + + GtkOrientation orientation = (widget == MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL) + ? GTK_ORIENTATION_HORIZONTAL + : GTK_ORIENTATION_VERTICAL; + + GdkRectangle rect = *aRect; + + const ScrollbarGTKMetrics* metrics = + (state->depressed || state->active || state->inHover) + ? GetActiveScrollbarMetrics(orientation) + : GetScrollbarMetrics(orientation); + Inset(&rect, metrics->margin.thumb); + + gtk_render_slider(style, cr, rect.x, rect.y, rect.width, rect.height, + orientation); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_inner_spin_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPINBUTTON, state->scale, direction, + GetStateFlagsFromGtkWidgetState(state)); + + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + + /* hard code these values */ + GdkRectangle arrow_rect; + arrow_rect.width = 6; + arrow_rect.height = 6; + + // align spin to the left + arrow_rect.x = rect->x; + + // up button + arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2 - 3; + gtk_render_arrow(style, cr, ARROW_UP, arrow_rect.x, arrow_rect.y, + arrow_rect.width); + + // down button + arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2 + 3; + gtk_render_arrow(style, cr, ARROW_DOWN, arrow_rect.x, arrow_rect.y, + arrow_rect.width); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_spin_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPINBUTTON, state->scale, direction); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_spin_updown_paint(cairo_t* cr, GdkRectangle* rect, + gboolean isDown, GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPINBUTTON, state->scale, direction, + GetStateFlagsFromGtkWidgetState(state)); + + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + + /* hard code these values */ + GdkRectangle arrow_rect; + arrow_rect.width = 6; + arrow_rect.height = 6; + arrow_rect.x = rect->x + (rect->width - arrow_rect.width) / 2; + arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2; + arrow_rect.y += isDown ? -1 : 1; + + gtk_render_arrow(style, cr, isDown ? ARROW_DOWN : ARROW_UP, arrow_rect.x, + arrow_rect.y, arrow_rect.width); + + return MOZ_GTK_SUCCESS; +} + +/* See gtk_range_draw() for reference. + */ +static gint moz_gtk_scale_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, GtkOrientation flags, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + gint x, y, width, height, min_width, min_height; + GtkStyleContext* style; + GtkBorder margin; + + moz_gtk_get_scale_metrics(flags, &min_width, &min_height); + + WidgetNodeType widget = (flags == GTK_ORIENTATION_HORIZONTAL) + ? MOZ_GTK_SCALE_TROUGH_HORIZONTAL + : MOZ_GTK_SCALE_TROUGH_VERTICAL; + style = GetStyleContext(widget, state->scale, direction, state_flags); + gtk_style_context_get_margin(style, state_flags, &margin); + + // Clamp the dimension perpendicular to the direction that the slider crosses + // to the minimum size. + if (flags == GTK_ORIENTATION_HORIZONTAL) { + width = rect->width - (margin.left + margin.right); + height = min_height - (margin.top + margin.bottom); + x = rect->x + margin.left; + y = rect->y + (rect->height - height) / 2; + } else { + width = min_width - (margin.left + margin.right); + height = rect->height - (margin.top + margin.bottom); + x = rect->x + (rect->width - width) / 2; + y = rect->y + margin.top; + } + + gtk_render_background(style, cr, x, y, width, height); + gtk_render_frame(style, cr, x, y, width, height); + + if (state->focused) + gtk_render_focus(style, cr, rect->x, rect->y, rect->width, rect->height); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_scale_thumb_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkOrientation flags, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style; + gint thumb_width, thumb_height, x, y; + + /* determine the thumb size, and position the thumb in the center in the + * opposite axis + */ + if (flags == GTK_ORIENTATION_HORIZONTAL) { + moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_HORIZONTAL, &thumb_width, + &thumb_height); + x = rect->x; + y = rect->y + (rect->height - thumb_height) / 2; + } else { + moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_VERTICAL, &thumb_height, + &thumb_width); + x = rect->x + (rect->width - thumb_width) / 2; + y = rect->y; + } + + WidgetNodeType widget = (flags == GTK_ORIENTATION_HORIZONTAL) + ? MOZ_GTK_SCALE_THUMB_HORIZONTAL + : MOZ_GTK_SCALE_THUMB_VERTICAL; + style = GetStyleContext(widget, state->scale, direction, state_flags); + gtk_render_slider(style, cr, x, y, thumb_width, thumb_height, flags); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_gripper_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_GRIPPER, state->scale, direction, + GetStateFlagsFromGtkWidgetState(state)); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_hpaned_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL, state->scale, + GTK_TEXT_DIR_LTR, GetStateFlagsFromGtkWidgetState(state)); + gtk_render_handle(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_vpaned_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL, state->scale, + GTK_TEXT_DIR_LTR, GetStateFlagsFromGtkWidgetState(state)); + gtk_render_handle(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +// See gtk_entry_draw() for reference. +static gint moz_gtk_entry_paint(cairo_t* cr, const GdkRectangle* aRect, + GtkWidgetState* state, GtkStyleContext* style, + WidgetNodeType widget) { + // StyleAppearance::FocusOutline + int draw_focus_outline_only = state->depressed; + GdkRectangle rect = *aRect; + + if (draw_focus_outline_only) { + // Inflate the given 'rect' with the focus outline size. + gint h, v; + moz_gtk_get_focus_outline_size(style, &h, &v); + rect.x -= h; + rect.width += 2 * h; + rect.y -= v; + rect.height += 2 * v; + } else { + gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height); + } + + // Paint the border, except for 'menulist-textfield' that isn't focused: + if (widget != MOZ_GTK_DROPDOWN_ENTRY || state->focused) { + gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height); + } + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_text_view_paint(cairo_t* cr, GdkRectangle* aRect, + GtkWidgetState* state, + GtkTextDirection direction) { + // GtkTextView and GtkScrolledWindow do not set active and prelight flags. + // The use of focus with MOZ_GTK_SCROLLED_WINDOW here is questionable + // because a parent widget will not have focus when its child GtkTextView + // has focus, but perhaps this may help identify a focused textarea with + // some themes as GtkTextView backgrounds do not typically render + // differently with focus. + GtkStateFlags state_flags = state->disabled ? GTK_STATE_FLAG_INSENSITIVE + : state->focused ? GTK_STATE_FLAG_FOCUSED + : GTK_STATE_FLAG_NORMAL; + + GtkStyleContext* style_frame = GetStyleContext( + MOZ_GTK_SCROLLED_WINDOW, state->scale, direction, state_flags); + gtk_render_frame(style_frame, cr, aRect->x, aRect->y, aRect->width, + aRect->height); + + GdkRectangle rect = *aRect; + InsetByBorderPadding(&rect, style_frame); + + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_TEXT_VIEW, state->scale, direction, state_flags); + gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height); + // There is a separate "text" window, which usually provides the + // background behind the text. However, this is transparent in Ambiance + // for GTK 3.20, in which case the MOZ_GTK_TEXT_VIEW background is + // visible. + style = GetStyleContext(MOZ_GTK_TEXT_VIEW_TEXT, state->scale, direction, + state_flags); + gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_treeview_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + gint xthickness, ythickness; + GtkStyleContext* style; + GtkStyleContext* style_tree; + GtkStateFlags state_flags; + GtkBorder border; + + /* only handle disabled and normal states, otherwise the whole background + * area will be painted differently with other states */ + state_flags = + state->disabled ? GTK_STATE_FLAG_INSENSITIVE : GTK_STATE_FLAG_NORMAL; + + style = GetStyleContext(MOZ_GTK_SCROLLED_WINDOW, state->scale, direction); + gtk_style_context_get_border(style, state_flags, &border); + xthickness = border.left; + ythickness = border.top; + + style_tree = GetStyleContext(MOZ_GTK_TREEVIEW_VIEW, state->scale, direction); + gtk_render_background(style_tree, cr, rect->x + xthickness, + rect->y + ythickness, rect->width - 2 * xthickness, + rect->height - 2 * ythickness); + + style = GetStyleContext(MOZ_GTK_SCROLLED_WINDOW, state->scale, direction); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_tree_header_cell_paint(cairo_t* cr, + const GdkRectangle* aRect, + GtkWidgetState* state, + gboolean isSorted, + GtkTextDirection direction) { + moz_gtk_button_paint(cr, aRect, state, GTK_RELIEF_NORMAL, + GetWidget(MOZ_GTK_TREE_HEADER_CELL), direction); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_tree_header_sort_arrow_paint(cairo_t* cr, + GdkRectangle* rect, + GtkWidgetState* state, + GtkArrowType arrow_type, + GtkTextDirection direction) { + GdkRectangle arrow_rect; + gdouble arrow_angle; + GtkStyleContext* style; + + /* hard code these values */ + arrow_rect.width = 11; + arrow_rect.height = 11; + arrow_rect.x = rect->x + (rect->width - arrow_rect.width) / 2; + arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2; + style = GetStyleContext(MOZ_GTK_TREE_HEADER_SORTARROW, state->scale, + direction, GetStateFlagsFromGtkWidgetState(state)); + switch (arrow_type) { + case GTK_ARROW_LEFT: + arrow_angle = ARROW_LEFT; + break; + case GTK_ARROW_RIGHT: + arrow_angle = ARROW_RIGHT; + break; + case GTK_ARROW_DOWN: + arrow_angle = ARROW_DOWN; + break; + default: + arrow_angle = ARROW_UP; + break; + } + if (arrow_type != GTK_ARROW_NONE) + gtk_render_arrow(style, cr, arrow_angle, arrow_rect.x, arrow_rect.y, + arrow_rect.width); + return MOZ_GTK_SUCCESS; +} + +/* See gtk_expander_paint() for reference. + */ +static gint moz_gtk_treeview_expander_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkExpanderStyle expander_state, + GtkTextDirection direction) { + /* Because the frame we get is of the entire treeview, we can't get the + * precise event state of one expander, thus rendering hover and active + * feedback useless. */ + GtkStateFlags state_flags = + state->disabled ? GTK_STATE_FLAG_INSENSITIVE : GTK_STATE_FLAG_NORMAL; + + if (state->inHover) + state_flags = + static_cast<GtkStateFlags>(state_flags | GTK_STATE_FLAG_PRELIGHT); + if (state->selected) + state_flags = + static_cast<GtkStateFlags>(state_flags | GTK_STATE_FLAG_SELECTED); + + /* GTK_STATE_FLAG_ACTIVE controls expanded/colapsed state rendering + * in gtk_render_expander() + */ + if (expander_state == GTK_EXPANDER_EXPANDED) + state_flags = + static_cast<GtkStateFlags>(state_flags | checkbox_check_state); + else + state_flags = + static_cast<GtkStateFlags>(state_flags & ~(checkbox_check_state)); + + GtkStyleContext* style = GetStyleContext( + MOZ_GTK_TREEVIEW_EXPANDER, state->scale, direction, state_flags); + gtk_render_expander(style, cr, rect->x, rect->y, rect->width, rect->height); + + return MOZ_GTK_SUCCESS; +} + +/* See gtk_separator_draw() for reference. + */ +static gint moz_gtk_combo_box_paint(cairo_t* cr, const GdkRectangle* aRect, + GtkWidgetState* state, + GtkTextDirection direction) { + GdkRectangle arrow_rect, real_arrow_rect; + gint separator_width; + gboolean wide_separators; + GtkStyleContext* style; + GtkRequisition arrow_req; + + GtkWidget* comboBoxButton = GetWidget(MOZ_GTK_COMBOBOX_BUTTON); + GtkWidget* comboBoxArrow = GetWidget(MOZ_GTK_COMBOBOX_ARROW); + + /* Also sets the direction on gComboBoxButtonWidget, which is then + * inherited by the separator and arrow */ + moz_gtk_button_paint(cr, aRect, state, GTK_RELIEF_NORMAL, comboBoxButton, + direction); + + calculate_button_inner_rect(comboBoxButton, aRect, &arrow_rect, direction); + /* Now arrow_rect contains the inner rect ; we want to correct the width + * to what the arrow needs (see gtk_combo_box_size_allocate) */ + gtk_widget_get_preferred_size(comboBoxArrow, NULL, &arrow_req); + moz_gtk_sanity_preferred_size(&arrow_req); + + if (direction == GTK_TEXT_DIR_LTR) + arrow_rect.x += arrow_rect.width - arrow_req.width; + arrow_rect.width = arrow_req.width; + + calculate_arrow_rect(comboBoxArrow, &arrow_rect, &real_arrow_rect, direction); + + style = GetStyleContext(MOZ_GTK_COMBOBOX_ARROW, state->scale); + gtk_render_arrow(style, cr, ARROW_DOWN, real_arrow_rect.x, real_arrow_rect.y, + real_arrow_rect.width); + + /* If there is no separator in the theme, there's nothing left to do. */ + GtkWidget* widget = GetWidget(MOZ_GTK_COMBOBOX_SEPARATOR); + if (!widget) return MOZ_GTK_SUCCESS; + style = gtk_widget_get_style_context(widget); + StyleContextSetScale(style, state->scale); + gtk_style_context_get_style(style, "wide-separators", &wide_separators, + "separator-width", &separator_width, NULL); + + if (wide_separators) { + if (direction == GTK_TEXT_DIR_LTR) + arrow_rect.x -= separator_width; + else + arrow_rect.x += arrow_rect.width; + + gtk_render_frame(style, cr, arrow_rect.x, arrow_rect.y, separator_width, + arrow_rect.height); + } else { + if (direction == GTK_TEXT_DIR_LTR) { + GtkBorder padding; + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + gtk_style_context_get_padding(style, state_flags, &padding); + arrow_rect.x -= padding.left; + } else + arrow_rect.x += arrow_rect.width; + + gtk_render_line(style, cr, arrow_rect.x, arrow_rect.y, arrow_rect.x, + arrow_rect.y + arrow_rect.height); + } + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_arrow_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, GtkArrowType arrow_type, + GtkTextDirection direction) { + GdkRectangle arrow_rect; + gdouble arrow_angle; + + if (direction == GTK_TEXT_DIR_RTL) { + if (arrow_type == GTK_ARROW_LEFT) { + arrow_type = GTK_ARROW_RIGHT; + } else if (arrow_type == GTK_ARROW_RIGHT) { + arrow_type = GTK_ARROW_LEFT; + } + } + switch (arrow_type) { + case GTK_ARROW_LEFT: + arrow_angle = ARROW_LEFT; + break; + case GTK_ARROW_RIGHT: + arrow_angle = ARROW_RIGHT; + break; + case GTK_ARROW_DOWN: + arrow_angle = ARROW_DOWN; + break; + default: + arrow_angle = ARROW_UP; + break; + } + if (arrow_type == GTK_ARROW_NONE) return MOZ_GTK_SUCCESS; + + calculate_arrow_rect(GetWidget(MOZ_GTK_BUTTON_ARROW), rect, &arrow_rect, + direction); + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = GetStyleContext(MOZ_GTK_BUTTON_ARROW, state->scale, + direction, state_flags); + gtk_render_arrow(style, cr, arrow_angle, arrow_rect.x, arrow_rect.y, + arrow_rect.width); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_combo_box_entry_button_paint(cairo_t* cr, + GdkRectangle* rect, + GtkWidgetState* state, + gboolean input_focus, + GtkTextDirection direction) { + gint x_displacement, y_displacement; + GdkRectangle arrow_rect, real_arrow_rect; + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style; + + GtkWidget* comboBoxEntry = GetWidget(MOZ_GTK_COMBOBOX_ENTRY_BUTTON); + moz_gtk_button_paint(cr, rect, state, GTK_RELIEF_NORMAL, comboBoxEntry, + direction); + calculate_button_inner_rect(comboBoxEntry, rect, &arrow_rect, direction); + + if (state_flags & GTK_STATE_FLAG_ACTIVE) { + style = gtk_widget_get_style_context(comboBoxEntry); + StyleContextSetScale(style, state->scale); + gtk_style_context_get_style(style, "child-displacement-x", &x_displacement, + "child-displacement-y", &y_displacement, NULL); + arrow_rect.x += x_displacement; + arrow_rect.y += y_displacement; + } + + calculate_arrow_rect(GetWidget(MOZ_GTK_COMBOBOX_ENTRY_ARROW), &arrow_rect, + &real_arrow_rect, direction); + + style = GetStyleContext(MOZ_GTK_COMBOBOX_ENTRY_ARROW, state->scale); + gtk_render_arrow(style, cr, ARROW_DOWN, real_arrow_rect.x, real_arrow_rect.y, + real_arrow_rect.width); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_container_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + WidgetNodeType widget_type, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = + GetStyleContext(widget_type, state->scale, direction, state_flags); + /* this is for drawing a prelight box */ + if (state_flags & GTK_STATE_FLAG_PRELIGHT) { + gtk_render_background(style, cr, rect->x, rect->y, rect->width, + rect->height); + } + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_toggle_label_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, gboolean isradio, + GtkTextDirection direction) { + if (!state->focused) return MOZ_GTK_SUCCESS; + + GtkStyleContext* style = GetStyleContext( + isradio ? MOZ_GTK_RADIOBUTTON_CONTAINER : MOZ_GTK_CHECKBUTTON_CONTAINER, + state->scale, direction, GetStateFlagsFromGtkWidgetState(state)); + gtk_render_focus(style, cr, rect->x, rect->y, rect->width, rect->height); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_toolbar_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_TOOLBAR, state->scale, direction); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +/* See _gtk_toolbar_paint_space_line() for reference. + */ +static gint moz_gtk_toolbar_separator_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + gint separator_width; + gint paint_width; + gboolean wide_separators; + + /* Defined as constants in GTK+ 2.10.14 */ + const double start_fraction = 0.2; + const double end_fraction = 0.8; + + GtkStyleContext* style = GetStyleContext(MOZ_GTK_TOOLBAR, state->scale); + gtk_style_context_get_style(style, "wide-separators", &wide_separators, + "separator-width", &separator_width, NULL); + + style = GetStyleContext(MOZ_GTK_TOOLBAR_SEPARATOR, state->scale, direction); + if (wide_separators) { + if (separator_width > rect->width) separator_width = rect->width; + + gtk_render_frame(style, cr, rect->x + (rect->width - separator_width) / 2, + rect->y + rect->height * start_fraction, separator_width, + rect->height * (end_fraction - start_fraction)); + } else { + GtkBorder padding; + gtk_style_context_get_padding(style, gtk_style_context_get_state(style), + &padding); + + paint_width = padding.left; + if (paint_width > rect->width) paint_width = rect->width; + + gtk_render_line(style, cr, rect->x + (rect->width - paint_width) / 2, + rect->y + rect->height * start_fraction, + rect->x + (rect->width - paint_width) / 2, + rect->y + rect->height * end_fraction); + } + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_tooltip_paint(cairo_t* cr, const GdkRectangle* aRect, + GtkWidgetState* state, + GtkTextDirection direction) { + // Tooltip widget is made in GTK3 as following tree: + // Tooltip window + // Horizontal Box + // Icon (not supported by Firefox) + // Label + // Each element can be fully styled by CSS of GTK theme. + // We have to draw all elements with appropriate offset and right dimensions. + + // Tooltip drawing + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_TOOLTIP, state->scale, direction); + GdkRectangle rect = *aRect; + gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height); + gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height); + + // Horizontal Box drawing + // + // The box element has hard-coded 6px margin-* GtkWidget properties, which + // are added between the window dimensions and the CSS margin box of the + // horizontal box. The frame of the tooltip window is drawn in the + // 6px margin. + // For drawing Horizontal Box we have to inset drawing area by that 6px + // plus its CSS margin. + GtkStyleContext* boxStyle = + GetStyleContext(MOZ_GTK_TOOLTIP_BOX, state->scale, direction); + + rect.x += 6; + rect.y += 6; + rect.width -= 12; + rect.height -= 12; + + InsetByMargin(&rect, boxStyle); + gtk_render_background(boxStyle, cr, rect.x, rect.y, rect.width, rect.height); + gtk_render_frame(boxStyle, cr, rect.x, rect.y, rect.width, rect.height); + + // Label drawing + InsetByBorderPadding(&rect, boxStyle); + + GtkStyleContext* labelStyle = + GetStyleContext(MOZ_GTK_TOOLTIP_BOX_LABEL, state->scale, direction); + moz_gtk_draw_styled_frame(labelStyle, cr, &rect, false); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_resizer_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_RESIZER, state->scale, GTK_TEXT_DIR_LTR, + GetStateFlagsFromGtkWidgetState(state)); + + // Workaround unico not respecting the text direction for resizers. + // See bug 1174248. + cairo_save(cr); + if (direction == GTK_TEXT_DIR_RTL) { + cairo_matrix_t mat; + cairo_matrix_init_translate(&mat, 2 * rect->x + rect->width, 0); + cairo_matrix_scale(&mat, -1, 1); + cairo_transform(cr, &mat); + } + + gtk_render_handle(style, cr, rect->x, rect->y, rect->width, rect->height); + cairo_restore(cr); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_frame_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_FRAME, state->scale, direction); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_progressbar_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_PROGRESS_TROUGH, state->scale, direction); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_progress_chunk_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction, + WidgetNodeType widget) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_PROGRESS_CHUNK, state->scale, direction); + + if (widget == MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE || + widget == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE) { + /** + * The bar's size and the bar speed are set depending of the progress' + * size. These could also be constant for all progress bars easily. + */ + gboolean vertical = + (widget == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE); + + /* The size of the dimension we are going to use for the animation. */ + const gint progressSize = vertical ? rect->height : rect->width; + + /* The bar is using a fifth of the element size, based on GtkProgressBar + * activity-blocks property. */ + const gint barSize = MAX(1, progressSize / 5); + + /* Represents the travel that has to be done for a complete cycle. */ + const gint travel = 2 * (progressSize - barSize); + + /* period equals to travel / pixelsPerMillisecond + * where pixelsPerMillisecond equals progressSize / 1000.0. + * This is equivalent to 1600. */ + static const guint period = 1600; + const gint t = PR_IntervalToMilliseconds(PR_IntervalNow()) % period; + const gint dx = travel * t / period; + + if (vertical) { + rect->y += (dx < travel / 2) ? dx : travel - dx; + rect->height = barSize; + } else { + rect->x += (dx < travel / 2) ? dx : travel - dx; + rect->width = barSize; + } + } + + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_get_tab_thickness(GtkStyleContext* style) { + if (!notebook_has_tab_gap) + return 0; /* tabs do not overdraw the tabpanel border with "no gap" style */ + + GtkBorder border; + gtk_style_context_get_border(style, gtk_style_context_get_state(style), + &border); + if (border.top < 2) return 2; /* some themes don't set ythickness correctly */ + + return border.top; +} + +gint moz_gtk_get_tab_thickness(WidgetNodeType aNodeType) { + GtkStyleContext* style = GetStyleContext(aNodeType); + int thickness = moz_gtk_get_tab_thickness(style); + return thickness; +} + +/* actual small tabs */ +static gint moz_gtk_tab_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, GtkTabFlags flags, + GtkTextDirection direction, + WidgetNodeType widget) { + /* When the tab isn't selected, we just draw a notebook extension. + * When it is selected, we overwrite the adjacent border of the tabpanel + * touching the tab with a pierced border (called "the gap") to make the + * tab appear physically attached to the tabpanel; see details below. */ + + GtkStyleContext* style; + GdkRectangle tabRect; + GdkRectangle focusRect; + GdkRectangle backRect; + int initial_gap = 0; + bool isBottomTab = (widget == MOZ_GTK_TAB_BOTTOM); + + style = GetStyleContext(widget, state->scale, direction, + GetStateFlagsFromGtkTabFlags(flags)); + tabRect = *rect; + + if (flags & MOZ_GTK_TAB_FIRST) { + gtk_style_context_get_style(style, "initial-gap", &initial_gap, NULL); + tabRect.width -= initial_gap; + + if (direction != GTK_TEXT_DIR_RTL) { + tabRect.x += initial_gap; + } + } + + focusRect = backRect = tabRect; + + if (notebook_has_tab_gap) { + if ((flags & MOZ_GTK_TAB_SELECTED) == 0) { + /* Only draw the tab */ + gtk_render_extension(style, cr, tabRect.x, tabRect.y, tabRect.width, + tabRect.height, + isBottomTab ? GTK_POS_TOP : GTK_POS_BOTTOM); + } else { + /* Draw the tab and the gap + * We want the gap to be positioned exactly on the tabpanel top + * border; since tabbox.css may set a negative margin so that the tab + * frame rect already overlaps the tabpanel frame rect, we need to take + * that into account when drawing. To that effect, nsNativeThemeGTK + * passes us this negative margin (bmargin in the graphic below) in the + * lowest bits of |flags|. We use it to set gap_voffset, the distance + * between the top of the gap and the bottom of the tab (resp. the + * bottom of the gap and the top of the tab when we draw a bottom tab), + * while ensuring that the gap always touches the border of the tab, + * i.e. 0 <= gap_voffset <= gap_height, to avoid surprinsing results + * with big negative or positive margins. + * Here is a graphical explanation in the case of top tabs: + * ___________________________ + * / \ + * | T A B | + * ----------|. . . . . . . . . . . . . . .|----- top of tabpanel + * : ^ bmargin : ^ + * : | (-negative margin, : | + * bottom : v passed in flags) : | gap_height + * of -> :.............................: | (the size of the + * the tab . part of the gap . | tabpanel top border) + * . outside of the tab . v + * ---------------------------------------------- + * + * To draw the gap, we use gtk_render_frame_gap(), see comment in + * moz_gtk_tabpanels_paint(). This gap is made 3 * gap_height tall, + * which should suffice to ensure that the only visible border is the + * pierced one. If the tab is in the middle, we make the box_gap begin + * a bit to the left of the tab and end a bit to the right, adjusting + * the gap position so it still is under the tab, because we want the + * rendering of a gap in the middle of a tabpanel. This is the role of + * the gints gap_{l,r}_offset. On the contrary, if the tab is the + * first, we align the start border of the box_gap with the start + * border of the tab (left if LTR, right if RTL), by setting the + * appropriate offset to 0.*/ + gint gap_loffset, gap_roffset, gap_voffset, gap_height; + + /* Get height needed by the gap */ + gap_height = moz_gtk_get_tab_thickness(style); + + /* Extract gap_voffset from the first bits of flags */ + gap_voffset = flags & MOZ_GTK_TAB_MARGIN_MASK; + if (gap_voffset > gap_height) gap_voffset = gap_height; + + /* Set gap_{l,r}_offset to appropriate values */ + gap_loffset = gap_roffset = 20; /* should be enough */ + if (flags & MOZ_GTK_TAB_FIRST) { + if (direction == GTK_TEXT_DIR_RTL) + gap_roffset = initial_gap; + else + gap_loffset = initial_gap; + } + + GtkStyleContext* panelStyle = + GetStyleContext(MOZ_GTK_TABPANELS, state->scale, direction); + + if (isBottomTab) { + /* Draw the tab on bottom */ + focusRect.y += gap_voffset; + focusRect.height -= gap_voffset; + + gtk_render_extension(style, cr, tabRect.x, tabRect.y + gap_voffset, + tabRect.width, tabRect.height - gap_voffset, + GTK_POS_TOP); + + backRect.y += (gap_voffset - gap_height); + backRect.height = gap_height; + + /* Draw the gap; erase with background color before painting in + * case theme does not */ + gtk_render_background(panelStyle, cr, backRect.x, backRect.y, + backRect.width, backRect.height); + cairo_save(cr); + cairo_rectangle(cr, backRect.x, backRect.y, backRect.width, + backRect.height); + cairo_clip(cr); + + gtk_render_frame_gap(panelStyle, cr, tabRect.x - gap_loffset, + tabRect.y + gap_voffset - 3 * gap_height, + tabRect.width + gap_loffset + gap_roffset, + 3 * gap_height, GTK_POS_BOTTOM, gap_loffset, + gap_loffset + tabRect.width); + cairo_restore(cr); + } else { + /* Draw the tab on top */ + focusRect.height -= gap_voffset; + gtk_render_extension(style, cr, tabRect.x, tabRect.y, tabRect.width, + tabRect.height - gap_voffset, GTK_POS_BOTTOM); + + backRect.y += (tabRect.height - gap_voffset); + backRect.height = gap_height; + + /* Draw the gap; erase with background color before painting in + * case theme does not */ + gtk_render_background(panelStyle, cr, backRect.x, backRect.y, + backRect.width, backRect.height); + + cairo_save(cr); + cairo_rectangle(cr, backRect.x, backRect.y, backRect.width, + backRect.height); + cairo_clip(cr); + + gtk_render_frame_gap(panelStyle, cr, tabRect.x - gap_loffset, + tabRect.y + tabRect.height - gap_voffset, + tabRect.width + gap_loffset + gap_roffset, + 3 * gap_height, GTK_POS_TOP, gap_loffset, + gap_loffset + tabRect.width); + cairo_restore(cr); + } + } + } else { + gtk_render_background(style, cr, tabRect.x, tabRect.y, tabRect.width, + tabRect.height); + gtk_render_frame(style, cr, tabRect.x, tabRect.y, tabRect.width, + tabRect.height); + } + + if (state->focused) { + /* Paint the focus ring */ + GtkBorder padding; + gtk_style_context_get_padding(style, GetStateFlagsFromGtkWidgetState(state), + &padding); + + focusRect.x += padding.left; + focusRect.width -= (padding.left + padding.right); + focusRect.y += padding.top; + focusRect.height -= (padding.top + padding.bottom); + + gtk_render_focus(style, cr, focusRect.x, focusRect.y, focusRect.width, + focusRect.height); + } + + return MOZ_GTK_SUCCESS; +} + +/* tab area*/ +static gint moz_gtk_tabpanels_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_TABPANELS, state->scale, direction); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + /* + * The gap size is not needed in moz_gtk_tabpanels_paint because + * the gap will be painted with the foreground tab in moz_gtk_tab_paint. + * + * However, if moz_gtk_tabpanels_paint just uses gtk_render_frame(), + * the theme will think that there are no tabs and may draw something + * different.Hence the trick of using two clip regions, and drawing the + * gap outside each clip region, to get the correct frame for + * a tabpanel with tabs. + */ + /* left side */ + cairo_save(cr); + cairo_rectangle(cr, rect->x, rect->y, rect->x + rect->width / 2, + rect->y + rect->height); + cairo_clip(cr); + gtk_render_frame_gap(style, cr, rect->x, rect->y, rect->width, rect->height, + GTK_POS_TOP, rect->width - 1, rect->width); + cairo_restore(cr); + + /* right side */ + cairo_save(cr); + cairo_rectangle(cr, rect->x + rect->width / 2, rect->y, rect->x + rect->width, + rect->y + rect->height); + cairo_clip(cr); + gtk_render_frame_gap(style, cr, rect->x, rect->y, rect->width, rect->height, + GTK_POS_TOP, 0, 1); + cairo_restore(cr); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_tab_scroll_arrow_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkArrowType arrow_type, + GtkTextDirection direction) { + GtkStyleContext* style; + gdouble arrow_angle; + gint arrow_size = MIN(rect->width, rect->height); + gint x = rect->x + (rect->width - arrow_size) / 2; + gint y = rect->y + (rect->height - arrow_size) / 2; + + if (direction == GTK_TEXT_DIR_RTL) { + arrow_type = + (arrow_type == GTK_ARROW_LEFT) ? GTK_ARROW_RIGHT : GTK_ARROW_LEFT; + } + switch (arrow_type) { + case GTK_ARROW_LEFT: + arrow_angle = ARROW_LEFT; + break; + case GTK_ARROW_RIGHT: + arrow_angle = ARROW_RIGHT; + break; + case GTK_ARROW_DOWN: + arrow_angle = ARROW_DOWN; + break; + default: + arrow_angle = ARROW_UP; + break; + } + if (arrow_type != GTK_ARROW_NONE) { + style = GetStyleContext(MOZ_GTK_TAB_SCROLLARROW, state->scale, direction, + GetStateFlagsFromGtkWidgetState(state)); + gtk_render_arrow(style, cr, arrow_angle, x, y, arrow_size); + } + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_menu_bar_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style; + + GtkWidget* widget = GetWidget(MOZ_GTK_MENUBAR); + gtk_widget_set_direction(widget, direction); + + style = gtk_widget_get_style_context(widget); + gtk_style_context_save(style); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_MENUBAR); + StyleContextSetScale(style, state->scale); + + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_style_context_restore(style); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_menu_popup_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style; + + GtkWidget* widget = GetWidget(MOZ_GTK_MENUPOPUP); + gtk_widget_set_direction(widget, direction); + + // Draw a backing toplevel. This fixes themes that don't provide a menu + // background, and depend on the GtkMenu's implementation window to provide + // it. + moz_gtk_window_paint(cr, rect, direction); + + style = gtk_widget_get_style_context(widget); + gtk_style_context_save(style); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_MENU); + StyleContextSetScale(style, state->scale); + + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_style_context_restore(style); + + return MOZ_GTK_SUCCESS; +} + +// See gtk_menu_item_draw() for reference. +static gint moz_gtk_menu_separator_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkWidgetState defaultState = {0}; + moz_gtk_menu_item_paint(MOZ_GTK_MENUSEPARATOR, cr, rect, &defaultState, + direction); + + if (gtk_get_minor_version() >= 20) return MOZ_GTK_SUCCESS; + + GtkStyleContext* style; + gboolean wide_separators; + gint separator_height; + gint x, y, w; + GtkBorder padding; + + style = GetStyleContext(MOZ_GTK_MENUSEPARATOR, state->scale, direction); + gtk_style_context_get_padding(style, gtk_style_context_get_state(style), + &padding); + + x = rect->x; + y = rect->y; + w = rect->width; + + gtk_style_context_save(style); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_SEPARATOR); + + gtk_style_context_get_style(style, "wide-separators", &wide_separators, + "separator-height", &separator_height, NULL); + + if (wide_separators) { + gtk_render_frame(style, cr, x + padding.left, y + padding.top, + w - padding.left - padding.right, separator_height); + } else { + gtk_render_line(style, cr, x + padding.left, y + padding.top, + x + w - padding.right - 1, y + padding.top); + } + + gtk_style_context_restore(style); + + return MOZ_GTK_SUCCESS; +} + +// See gtk_menu_item_draw() for reference. +static gint moz_gtk_menu_item_paint(WidgetNodeType widget, cairo_t* cr, + GdkRectangle* rect, GtkWidgetState* state, + GtkTextDirection direction) { + gint x, y, w, h; + guint minorVersion = gtk_get_minor_version(); + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + + // GTK versions prior to 3.8 render the background and frame only when not + // a separator and in hover prelight. + if (minorVersion < 8 && (widget == MOZ_GTK_MENUSEPARATOR || + !(state_flags & GTK_STATE_FLAG_PRELIGHT))) + return MOZ_GTK_SUCCESS; + + GtkStyleContext* style = + GetStyleContext(widget, state->scale, direction, state_flags); + + if (minorVersion < 6) { + // GTK+ 3.4 saves the style context and adds the menubar class to + // menubar children, but does each of these only when drawing, not + // during layout. + gtk_style_context_save(style); + if (widget == MOZ_GTK_MENUBARITEM) { + gtk_style_context_add_class(style, GTK_STYLE_CLASS_MENUBAR); + } + } + + x = rect->x; + y = rect->y; + w = rect->width; + h = rect->height; + + gtk_render_background(style, cr, x, y, w, h); + gtk_render_frame(style, cr, x, y, w, h); + + if (minorVersion < 6) { + gtk_style_context_restore(style); + } + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_menu_arrow_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_MENUITEM, state->scale, direction, state_flags); + gtk_render_arrow(style, cr, + (direction == GTK_TEXT_DIR_LTR) ? ARROW_RIGHT : ARROW_LEFT, + rect->x, rect->y, rect->width); + return MOZ_GTK_SUCCESS; +} + +// For reference, see gtk_check_menu_item_size_allocate() in GTK versions after +// 3.20 and gtk_real_check_menu_item_draw_indicator() in earlier versions. +static gint moz_gtk_check_menu_item_paint(WidgetNodeType widgetType, + cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + gboolean checked, + GtkTextDirection direction) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style; + gint indicator_size, horizontal_padding; + gint x, y; + + moz_gtk_menu_item_paint(MOZ_GTK_MENUITEM, cr, rect, state, direction); + + if (checked) { + state_flags = + static_cast<GtkStateFlags>(state_flags | checkbox_check_state); + } + + bool pre_3_20 = gtk_get_minor_version() < 20; + gint offset; + style = GetStyleContext(widgetType, state->scale, direction); + gtk_style_context_get_style(style, "indicator-size", &indicator_size, + "horizontal-padding", &horizontal_padding, NULL); + if (pre_3_20) { + GtkBorder padding; + gtk_style_context_get_padding(style, state_flags, &padding); + offset = horizontal_padding + padding.left + 2; + } else { + GdkRectangle r = {0}; + InsetByMargin(&r, style); + InsetByBorderPadding(&r, style); + offset = r.x; + } + + bool isRadio = (widgetType == MOZ_GTK_RADIOMENUITEM); + WidgetNodeType indicatorType = isRadio ? MOZ_GTK_RADIOMENUITEM_INDICATOR + : MOZ_GTK_CHECKMENUITEM_INDICATOR; + const ToggleGTKMetrics* metrics = GetToggleMetrics(indicatorType); + style = GetStyleContext(indicatorType, state->scale, direction, state_flags); + + if (direction == GTK_TEXT_DIR_RTL) { + x = rect->width - indicator_size - offset; + } else { + x = rect->x + offset; + } + y = rect->y + (rect->height - indicator_size) / 2; + + gint indicator_width, indicator_height; + indicator_width = indicator_height = indicator_size; + if (!pre_3_20) { + gtk_render_background(style, cr, x, y, indicator_size, indicator_size); + gtk_render_frame(style, cr, x, y, indicator_size, indicator_size); + x = x + metrics->borderAndPadding.left; + y = y + metrics->borderAndPadding.top; + indicator_width = metrics->minSizeWithBorder.width - + metrics->borderAndPadding.left - + metrics->borderAndPadding.right; + indicator_height = metrics->minSizeWithBorder.height - + metrics->borderAndPadding.top - + metrics->borderAndPadding.bottom; + } + + if (isRadio) { + gtk_render_option(style, cr, x, y, indicator_width, indicator_height); + } else { + gtk_render_check(style, cr, x, y, indicator_width, indicator_height); + } + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_info_bar_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_INFO_BAR, state->scale, GTK_TEXT_DIR_LTR, + GetStateFlagsFromGtkWidgetState(state)); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height); + + return MOZ_GTK_SUCCESS; +} + +static gint moz_gtk_header_bar_paint(WidgetNodeType widgetType, cairo_t* cr, + GdkRectangle* rect, + GtkWidgetState* state) { + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = + GetStyleContext(widgetType, state->scale, GTK_TEXT_DIR_NONE, state_flags); + +// Some themes (Adwaita for instance) draws bold dark line at +// titlebar bottom. It does not fit well with Firefox tabs so +// draw with some extent to make the titlebar bottom part invisible. +#define TITLEBAR_EXTENT 4 + + // We don't need to draw window decoration for MOZ_GTK_HEADER_BAR_MAXIMIZED, + // i.e. when main window is maximized. + if (widgetType == MOZ_GTK_HEADER_BAR) { + GtkStyleContext* windowStyle = + GetStyleContext(MOZ_GTK_HEADERBAR_WINDOW, state->scale); + bool solidDecorations = + gtk_style_context_has_class(windowStyle, "solid-csd"); + GtkStyleContext* decorationStyle = + GetStyleContext(solidDecorations ? MOZ_GTK_WINDOW_DECORATION_SOLID + : MOZ_GTK_WINDOW_DECORATION, + state->scale, GTK_TEXT_DIR_LTR, state_flags); + + gtk_render_background(decorationStyle, cr, rect->x, rect->y, rect->width, + rect->height + TITLEBAR_EXTENT); + gtk_render_frame(decorationStyle, cr, rect->x, rect->y, rect->width, + rect->height + TITLEBAR_EXTENT); + } + + gtk_render_background(style, cr, rect->x, rect->y, rect->width, + rect->height + TITLEBAR_EXTENT); + gtk_render_frame(style, cr, rect->x, rect->y, rect->width, + rect->height + TITLEBAR_EXTENT); + + return MOZ_GTK_SUCCESS; +} + +static GtkBorder GetMarginBorderPadding(GtkStyleContext* aStyle) { + gint left = 0, top = 0, right = 0, bottom = 0; + moz_gtk_add_margin_border_padding(aStyle, &left, &top, &right, &bottom); + // narrowing conversions to gint16: + GtkBorder result; + result.left = left; + result.right = right; + result.top = top; + result.bottom = bottom; + return result; +} + +gint moz_gtk_get_widget_border(WidgetNodeType widget, gint* left, gint* top, + gint* right, gint* bottom, + // NOTE: callers depend on direction being used + // only for MOZ_GTK_DROPDOWN widgets. + GtkTextDirection direction) { + GtkWidget* w; + GtkStyleContext* style; + *left = *top = *right = *bottom = 0; + + switch (widget) { + case MOZ_GTK_BUTTON: + case MOZ_GTK_TOOLBAR_BUTTON: { + style = GetStyleContext(MOZ_GTK_BUTTON); + + *left = *top = *right = *bottom = gtk_container_get_border_width( + GTK_CONTAINER(GetWidget(MOZ_GTK_BUTTON))); + + if (widget == MOZ_GTK_TOOLBAR_BUTTON) { + gtk_style_context_save(style); + gtk_style_context_add_class(style, "image-button"); + } + + moz_gtk_add_style_padding(style, left, top, right, bottom); + + if (widget == MOZ_GTK_TOOLBAR_BUTTON) gtk_style_context_restore(style); + + moz_gtk_add_style_border(style, left, top, right, bottom); + + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_ENTRY: + case MOZ_GTK_DROPDOWN_ENTRY: { + style = GetStyleContext(widget); + + // XXX: Subtract 1 pixel from the padding to account for the default + // padding in forms.css. See bug 1187385. + *left = *top = *right = *bottom = -1; + moz_gtk_add_border_padding(style, left, top, right, bottom); + + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_TEXT_VIEW: + case MOZ_GTK_TREEVIEW: { + style = GetStyleContext(MOZ_GTK_SCROLLED_WINDOW); + moz_gtk_add_style_border(style, left, top, right, bottom); + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_TREE_HEADER_CELL: { + /* A Tree Header in GTK is just a different styled button + * It must be placed in a TreeView for getting the correct style + * assigned. + * That is why the following code is the same as for MOZ_GTK_BUTTON. + * */ + *left = *top = *right = *bottom = gtk_container_get_border_width( + GTK_CONTAINER(GetWidget(MOZ_GTK_TREE_HEADER_CELL))); + style = GetStyleContext(MOZ_GTK_TREE_HEADER_CELL); + moz_gtk_add_border_padding(style, left, top, right, bottom); + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_TREE_HEADER_SORTARROW: + w = GetWidget(MOZ_GTK_TREE_HEADER_SORTARROW); + break; + case MOZ_GTK_DROPDOWN_ARROW: + w = GetWidget(MOZ_GTK_COMBOBOX_ENTRY_BUTTON); + break; + case MOZ_GTK_DROPDOWN: { + /* We need to account for the arrow on the dropdown, so text + * doesn't come too close to the arrow, or in some cases spill + * into the arrow. */ + gboolean wide_separators; + gint separator_width; + GtkRequisition arrow_req; + GtkBorder border; + + *left = *top = *right = *bottom = gtk_container_get_border_width( + GTK_CONTAINER(GetWidget(MOZ_GTK_COMBOBOX_BUTTON))); + style = GetStyleContext(MOZ_GTK_COMBOBOX_BUTTON); + moz_gtk_add_border_padding(style, left, top, right, bottom); + + /* If there is no separator, don't try to count its width. */ + separator_width = 0; + GtkWidget* comboBoxSeparator = GetWidget(MOZ_GTK_COMBOBOX_SEPARATOR); + if (comboBoxSeparator) { + style = gtk_widget_get_style_context(comboBoxSeparator); + gtk_style_context_get_style(style, "wide-separators", &wide_separators, + "separator-width", &separator_width, NULL); + + if (!wide_separators) { + gtk_style_context_get_border( + style, gtk_style_context_get_state(style), &border); + separator_width = border.left; + } + } + + gtk_widget_get_preferred_size(GetWidget(MOZ_GTK_COMBOBOX_ARROW), NULL, + &arrow_req); + moz_gtk_sanity_preferred_size(&arrow_req); + + if (direction == GTK_TEXT_DIR_RTL) + *left += separator_width + arrow_req.width; + else + *right += separator_width + arrow_req.width; + + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_TABPANELS: + w = GetWidget(MOZ_GTK_TABPANELS); + break; + case MOZ_GTK_PROGRESSBAR: + w = GetWidget(MOZ_GTK_PROGRESSBAR); + break; + case MOZ_GTK_SPINBUTTON_ENTRY: + case MOZ_GTK_SPINBUTTON_UP: + case MOZ_GTK_SPINBUTTON_DOWN: + w = GetWidget(MOZ_GTK_SPINBUTTON); + break; + case MOZ_GTK_SCALE_HORIZONTAL: + case MOZ_GTK_SCALE_VERTICAL: + w = GetWidget(widget); + break; + case MOZ_GTK_FRAME: + w = GetWidget(MOZ_GTK_FRAME); + break; + case MOZ_GTK_CHECKBUTTON_CONTAINER: + case MOZ_GTK_RADIOBUTTON_CONTAINER: { + w = GetWidget(widget); + style = gtk_widget_get_style_context(w); + + *left = *top = *right = *bottom = + gtk_container_get_border_width(GTK_CONTAINER(w)); + moz_gtk_add_border_padding(style, left, top, right, bottom); + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_MENUPOPUP: + w = GetWidget(MOZ_GTK_MENUPOPUP); + break; + case MOZ_GTK_MENUBARITEM: + case MOZ_GTK_MENUITEM: + case MOZ_GTK_CHECKMENUITEM: + case MOZ_GTK_RADIOMENUITEM: { + // Bug 1274143 for MOZ_GTK_MENUBARITEM + WidgetNodeType type = + widget == MOZ_GTK_MENUBARITEM ? MOZ_GTK_MENUITEM : widget; + style = GetStyleContext(type); + + if (gtk_get_minor_version() < 20) { + moz_gtk_add_style_padding(style, left, top, right, bottom); + } else { + moz_gtk_add_margin_border_padding(style, left, top, right, bottom); + } + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_INFO_BAR: + w = GetWidget(MOZ_GTK_INFO_BAR); + break; + case MOZ_GTK_TOOLTIP: { + // In GTK 3 there are 6 pixels of additional margin around the box. + // See details there: + // https://github.com/GNOME/gtk/blob/5ea69a136bd7e4970b3a800390e20314665aaed2/gtk/ui/gtktooltipwindow.ui#L11 + *left = *right = *top = *bottom = 6; + + // We also need to add margin/padding/borders from Tooltip content. + // Tooltip contains horizontal box, where icon and label is put. + // We ignore icon as long as we don't have support for it. + GtkStyleContext* boxStyle = GetStyleContext(MOZ_GTK_TOOLTIP_BOX); + moz_gtk_add_margin_border_padding(boxStyle, left, top, right, bottom); + + GtkStyleContext* labelStyle = GetStyleContext(MOZ_GTK_TOOLTIP_BOX_LABEL); + moz_gtk_add_margin_border_padding(labelStyle, left, top, right, bottom); + + return MOZ_GTK_SUCCESS; + } + case MOZ_GTK_HEADER_BAR_BUTTON_BOX: { + style = GetStyleContext(MOZ_GTK_HEADER_BAR); + moz_gtk_add_border_padding(style, left, top, right, bottom); + *top = *bottom = 0; + bool leftButtonsPlacement = false; + GetGtkHeaderBarButtonLayout({}, &leftButtonsPlacement); + if (direction == GTK_TEXT_DIR_RTL) { + leftButtonsPlacement = !leftButtonsPlacement; + } + if (leftButtonsPlacement) { + *right = 0; + } else { + *left = 0; + } + return MOZ_GTK_SUCCESS; + } + /* These widgets have no borders, since they are not containers. */ + case MOZ_GTK_CHECKBUTTON_LABEL: + case MOZ_GTK_RADIOBUTTON_LABEL: + case MOZ_GTK_SPLITTER_HORIZONTAL: + case MOZ_GTK_SPLITTER_VERTICAL: + case MOZ_GTK_CHECKBUTTON: + case MOZ_GTK_RADIOBUTTON: + case MOZ_GTK_SCROLLBAR_BUTTON: + case MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL: + case MOZ_GTK_SCROLLBAR_THUMB_VERTICAL: + case MOZ_GTK_SCALE_THUMB_HORIZONTAL: + case MOZ_GTK_SCALE_THUMB_VERTICAL: + case MOZ_GTK_GRIPPER: + case MOZ_GTK_PROGRESS_CHUNK: + case MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE: + case MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE: + case MOZ_GTK_TREEVIEW_EXPANDER: + case MOZ_GTK_TOOLBAR_SEPARATOR: + case MOZ_GTK_MENUSEPARATOR: + case MOZ_GTK_HEADER_BAR: + case MOZ_GTK_HEADER_BAR_MAXIMIZED: + case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE: + case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE: + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE: + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE: + /* These widgets have no borders.*/ + case MOZ_GTK_INNER_SPIN_BUTTON: + case MOZ_GTK_SPINBUTTON: + case MOZ_GTK_WINDOW: + case MOZ_GTK_RESIZER: + case MOZ_GTK_MENUARROW: + case MOZ_GTK_TOOLBARBUTTON_ARROW: + case MOZ_GTK_TOOLBAR: + case MOZ_GTK_MENUBAR: + case MOZ_GTK_TAB_SCROLLARROW: + return MOZ_GTK_SUCCESS; + default: + g_warning("Unsupported widget type: %d", widget); + return MOZ_GTK_UNKNOWN_WIDGET; + } + /* TODO - we're still missing some widget implementations */ + if (w) { + moz_gtk_add_style_border(gtk_widget_get_style_context(w), left, top, right, + bottom); + } + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_get_tab_border(gint* left, gint* top, gint* right, gint* bottom, + GtkTextDirection direction, GtkTabFlags flags, + WidgetNodeType widget) { + GtkStyleContext* style = GetStyleContext(widget, 1, direction, + GetStateFlagsFromGtkTabFlags(flags)); + + *left = *top = *right = *bottom = 0; + moz_gtk_add_style_padding(style, left, top, right, bottom); + + // Gtk >= 3.20 does not use those styles + if (gtk_check_version(3, 20, 0) != nullptr) { + int tab_curvature; + + gtk_style_context_get_style(style, "tab-curvature", &tab_curvature, NULL); + *left += tab_curvature; + *right += tab_curvature; + + if (flags & MOZ_GTK_TAB_FIRST) { + int initial_gap = 0; + gtk_style_context_get_style(style, "initial-gap", &initial_gap, NULL); + if (direction == GTK_TEXT_DIR_RTL) + *right += initial_gap; + else + *left += initial_gap; + } + } else { + GtkBorder margin; + + gtk_style_context_get_margin(style, gtk_style_context_get_state(style), + &margin); + *left += margin.left; + *right += margin.right; + + if (flags & MOZ_GTK_TAB_FIRST) { + style = GetStyleContext(MOZ_GTK_NOTEBOOK_HEADER, direction); + gtk_style_context_get_margin(style, gtk_style_context_get_state(style), + &margin); + *left += margin.left; + *right += margin.right; + } + } + + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_get_combo_box_entry_button_size(gint* width, gint* height) { + /* + * We get the requisition of the drop down button, which includes + * all padding, border and focus line widths the button uses, + * as well as the minimum arrow size and its padding + * */ + GtkRequisition requisition; + + gtk_widget_get_preferred_size(GetWidget(MOZ_GTK_COMBOBOX_ENTRY_BUTTON), NULL, + &requisition); + moz_gtk_sanity_preferred_size(&requisition); + + *width = requisition.width; + *height = requisition.height; + + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_get_tab_scroll_arrow_size(gint* width, gint* height) { + gint arrow_size; + + GtkStyleContext* style = GetStyleContext(MOZ_GTK_TABPANELS); + gtk_style_context_get_style(style, "scroll-arrow-hlength", &arrow_size, NULL); + + *height = *width = arrow_size; + + return MOZ_GTK_SUCCESS; +} + +void moz_gtk_get_arrow_size(WidgetNodeType widgetType, gint* width, + gint* height) { + GtkWidget* widget; + switch (widgetType) { + case MOZ_GTK_DROPDOWN: + widget = GetWidget(MOZ_GTK_COMBOBOX_ARROW); + break; + default: + widget = GetWidget(MOZ_GTK_BUTTON_ARROW); + break; + } + + GtkRequisition requisition; + gtk_widget_get_preferred_size(widget, NULL, &requisition); + moz_gtk_sanity_preferred_size(&requisition); + + *width = requisition.width; + *height = requisition.height; +} + +gint moz_gtk_get_toolbar_separator_width(gint* size) { + gboolean wide_separators; + gint separator_width; + GtkBorder border; + + GtkStyleContext* style = GetStyleContext(MOZ_GTK_TOOLBAR); + gtk_style_context_get_style(style, "space-size", size, "wide-separators", + &wide_separators, "separator-width", + &separator_width, NULL); + /* Just in case... */ + gtk_style_context_get_border(style, gtk_style_context_get_state(style), + &border); + *size = MAX(*size, (wide_separators ? separator_width : border.left)); + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_get_expander_size(gint* size) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_EXPANDER); + gtk_style_context_get_style(style, "expander-size", size, NULL); + return MOZ_GTK_SUCCESS; +} + +gint moz_gtk_get_treeview_expander_size(gint* size) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_TREEVIEW); + gtk_style_context_get_style(style, "expander-size", size, NULL); + return MOZ_GTK_SUCCESS; +} + +// See gtk_menu_item_draw() for reference. +gint moz_gtk_get_menu_separator_height(gint* size) { + gboolean wide_separators; + gint separator_height; + GtkBorder padding; + GtkStyleContext* style = GetStyleContext(MOZ_GTK_MENUSEPARATOR); + gtk_style_context_get_padding(style, gtk_style_context_get_state(style), + &padding); + + gtk_style_context_save(style); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_SEPARATOR); + + gtk_style_context_get_style(style, "wide-separators", &wide_separators, + "separator-height", &separator_height, NULL); + + gtk_style_context_restore(style); + + *size = padding.top + padding.bottom; + *size += (wide_separators) ? separator_height : 1; + + return MOZ_GTK_SUCCESS; +} + +void moz_gtk_get_entry_min_height(gint* min_content_height, + gint* border_padding_height) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_ENTRY); + if (!gtk_check_version(3, 20, 0)) { + gtk_style_context_get(style, gtk_style_context_get_state(style), + "min-height", min_content_height, nullptr); + } else { + *min_content_height = 0; + } + + GtkBorder border; + GtkBorder padding; + gtk_style_context_get_border(style, gtk_style_context_get_state(style), + &border); + gtk_style_context_get_padding(style, gtk_style_context_get_state(style), + &padding); + + *border_padding_height = + (border.top + border.bottom + padding.top + padding.bottom); +} + +void moz_gtk_get_scale_metrics(GtkOrientation orient, gint* scale_width, + gint* scale_height) { + if (gtk_check_version(3, 20, 0) != nullptr) { + WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL) + ? MOZ_GTK_SCALE_HORIZONTAL + : MOZ_GTK_SCALE_VERTICAL; + + gint thumb_length, thumb_height, trough_border; + moz_gtk_get_scalethumb_metrics(orient, &thumb_length, &thumb_height); + + GtkStyleContext* style = GetStyleContext(widget); + gtk_style_context_get_style(style, "trough-border", &trough_border, NULL); + + if (orient == GTK_ORIENTATION_HORIZONTAL) { + *scale_width = thumb_length + trough_border * 2; + *scale_height = thumb_height + trough_border * 2; + } else { + *scale_width = thumb_height + trough_border * 2; + *scale_height = thumb_length + trough_border * 2; + } + } else { + WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL) + ? MOZ_GTK_SCALE_TROUGH_HORIZONTAL + : MOZ_GTK_SCALE_TROUGH_VERTICAL; + moz_gtk_get_widget_min_size(GetStyleContext(widget), scale_width, + scale_height); + } +} + +gint moz_gtk_get_scalethumb_metrics(GtkOrientation orient, gint* thumb_length, + gint* thumb_height) { + if (gtk_check_version(3, 20, 0) != nullptr) { + WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL) + ? MOZ_GTK_SCALE_HORIZONTAL + : MOZ_GTK_SCALE_VERTICAL; + GtkStyleContext* style = GetStyleContext(widget); + gtk_style_context_get_style(style, "slider_length", thumb_length, + "slider_width", thumb_height, NULL); + } else { + WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL) + ? MOZ_GTK_SCALE_THUMB_HORIZONTAL + : MOZ_GTK_SCALE_THUMB_VERTICAL; + GtkStyleContext* style = GetStyleContext(widget); + + gint min_width, min_height; + GtkStateFlags state = gtk_style_context_get_state(style); + gtk_style_context_get(style, state, "min-width", &min_width, "min-height", + &min_height, nullptr); + GtkBorder margin; + gtk_style_context_get_margin(style, state, &margin); + gint margin_width = margin.left + margin.right; + gint margin_height = margin.top + margin.bottom; + + // Negative margin of slider element also determines its minimal size + // so use bigger of those two values. + if (min_width < -margin_width) min_width = -margin_width; + if (min_height < -margin_height) min_height = -margin_height; + + *thumb_length = min_width; + *thumb_height = min_height; + } + + return MOZ_GTK_SUCCESS; +} + +static MozGtkSize SizeFromLengthAndBreadth(GtkOrientation aOrientation, + gint aLength, gint aBreadth) { + return aOrientation == GTK_ORIENTATION_HORIZONTAL + ? MozGtkSize({aLength, aBreadth}) + : MozGtkSize({aBreadth, aLength}); +} + +const ToggleGTKMetrics* GetToggleMetrics(WidgetNodeType aWidgetType) { + ToggleGTKMetrics* metrics; + + switch (aWidgetType) { + case MOZ_GTK_RADIOBUTTON: + metrics = &sRadioMetrics; + break; + case MOZ_GTK_CHECKBUTTON: + metrics = &sCheckboxMetrics; + break; + case MOZ_GTK_RADIOMENUITEM_INDICATOR: + metrics = &sMenuRadioMetrics; + break; + case MOZ_GTK_CHECKMENUITEM_INDICATOR: + metrics = &sMenuCheckboxMetrics; + break; + default: + MOZ_CRASH("Unsupported widget type for getting metrics"); + return nullptr; + } + + metrics->initialized = true; + if (gtk_check_version(3, 20, 0) == nullptr) { + GtkStyleContext* style = GetStyleContext(aWidgetType); + GtkStateFlags state_flags = gtk_style_context_get_state(style); + gtk_style_context_get(style, state_flags, "min-height", + &(metrics->minSizeWithBorder.height), "min-width", + &(metrics->minSizeWithBorder.width), nullptr); + // Fallback to indicator size if min dimensions are zero + if (metrics->minSizeWithBorder.height == 0 || + metrics->minSizeWithBorder.width == 0) { + gint indicator_size; + gtk_widget_style_get(GetWidget(MOZ_GTK_CHECKBUTTON_CONTAINER), + "indicator_size", &indicator_size, nullptr); + if (metrics->minSizeWithBorder.height == 0) { + metrics->minSizeWithBorder.height = indicator_size; + } + if (metrics->minSizeWithBorder.width == 0) { + metrics->minSizeWithBorder.width = indicator_size; + } + } + + GtkBorder border, padding; + gtk_style_context_get_border(style, state_flags, &border); + gtk_style_context_get_padding(style, state_flags, &padding); + metrics->borderAndPadding.left = border.left + padding.left; + metrics->borderAndPadding.right = border.right + padding.right; + metrics->borderAndPadding.top = border.top + padding.top; + metrics->borderAndPadding.bottom = border.bottom + padding.bottom; + metrics->minSizeWithBorder.width += + metrics->borderAndPadding.left + metrics->borderAndPadding.right; + metrics->minSizeWithBorder.height += + metrics->borderAndPadding.top + metrics->borderAndPadding.bottom; + } else { + gint indicator_size, indicator_spacing; + gtk_widget_style_get(GetWidget(MOZ_GTK_CHECKBUTTON_CONTAINER), + "indicator_size", &indicator_size, "indicator_spacing", + &indicator_spacing, nullptr); + metrics->minSizeWithBorder.width = metrics->minSizeWithBorder.height = + indicator_size; + } + return metrics; +} + +static void InitScrollbarMetrics(ScrollbarGTKMetrics* aMetrics, + GtkOrientation aOrientation, + GtkStateFlags aStateFlags) { + WidgetNodeType scrollbar = aOrientation == GTK_ORIENTATION_HORIZONTAL + ? MOZ_GTK_SCROLLBAR_HORIZONTAL + : MOZ_GTK_SCROLLBAR_VERTICAL; + + gboolean backward, forward, secondary_backward, secondary_forward; + GtkStyleContext* style = + GetStyleContext(scrollbar, 1, GTK_TEXT_DIR_NONE, aStateFlags); + gtk_style_context_get_style( + style, "has-backward-stepper", &backward, "has-forward-stepper", &forward, + "has-secondary-backward-stepper", &secondary_backward, + "has-secondary-forward-stepper", &secondary_forward, nullptr); + bool hasButtons = + backward || forward || secondary_backward || secondary_forward; + + if (gtk_get_minor_version() < 20) { + gint slider_width, trough_border, stepper_size, min_slider_size; + + gtk_style_context_get_style(style, "slider-width", &slider_width, + "trough-border", &trough_border, "stepper-size", + &stepper_size, "min-slider-length", + &min_slider_size, nullptr); + + aMetrics->size.thumb = + SizeFromLengthAndBreadth(aOrientation, min_slider_size, slider_width); + aMetrics->size.button = + SizeFromLengthAndBreadth(aOrientation, stepper_size, slider_width); + // overall scrollbar + gint breadth = slider_width + 2 * trough_border; + // Require room for the slider in the track if we don't have buttons. + gint length = hasButtons ? 0 : min_slider_size + 2 * trough_border; + aMetrics->size.scrollbar = + SizeFromLengthAndBreadth(aOrientation, length, breadth); + + // Borders on the major axis are set on the outermost scrollbar + // element to correctly position the buttons when + // trough-under-steppers is true. + // Borders on the minor axis are set on the track element so that it + // receives mouse events, as in GTK. + // Other borders have been zero-initialized. + if (aOrientation == GTK_ORIENTATION_HORIZONTAL) { + aMetrics->border.scrollbar.left = aMetrics->border.scrollbar.right = + aMetrics->border.track.top = aMetrics->border.track.bottom = + trough_border; + } else { + aMetrics->border.scrollbar.top = aMetrics->border.scrollbar.bottom = + aMetrics->border.track.left = aMetrics->border.track.right = + trough_border; + } + + // We're done here for Gtk+ < 3.20... + return; + } + + // GTK version > 3.20 + // scrollbar + aMetrics->border.scrollbar = GetMarginBorderPadding(style); + + WidgetNodeType contents, track, thumb; + if (aOrientation == GTK_ORIENTATION_HORIZONTAL) { + contents = MOZ_GTK_SCROLLBAR_CONTENTS_HORIZONTAL; + track = MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL; + thumb = MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL; + } else { + contents = MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL; + track = MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL; + thumb = MOZ_GTK_SCROLLBAR_THUMB_VERTICAL; + } + + /* GetStyleContext() sets GtkStateFlags to the latest widget name + * in css selector string. When we call: + * + * GetStyleContext(thumb, GTK_STATE_FLAG_PRELIGHT) + * + * we get: + * + * "scrollbar contents trough slider:hover" + * + * Some themes (Ubuntu Ambiance) styles trough/thumb by scrollbar, + * the Gtk+ css rule looks like: + * + * "scrollbar:hover contents trough slider" + * + * So we need to apply GtkStateFlags to each widgets in style path. + */ + + // thumb + style = + CreateStyleContextWithStates(thumb, 1, GTK_TEXT_DIR_NONE, aStateFlags); + aMetrics->size.thumb = GetMinMarginBox(style); + gtk_style_context_get_margin(style, gtk_style_context_get_state(style), + &aMetrics->margin.thumb); + g_object_unref(style); + + // track + style = + CreateStyleContextWithStates(track, 1, GTK_TEXT_DIR_NONE, aStateFlags); + aMetrics->border.track = GetMarginBorderPadding(style); + MozGtkSize trackMinSize = GetMinContentBox(style) + aMetrics->border.track; + MozGtkSize trackSizeForThumb = aMetrics->size.thumb + aMetrics->border.track; + g_object_unref(style); + + // button + if (hasButtons) { + style = CreateStyleContextWithStates(MOZ_GTK_SCROLLBAR_BUTTON, 1, + GTK_TEXT_DIR_NONE, aStateFlags); + aMetrics->size.button = GetMinMarginBox(style); + g_object_unref(style); + } else { + aMetrics->size.button = {0, 0}; + } + if (aOrientation == GTK_ORIENTATION_HORIZONTAL) { + aMetrics->size.button.Rotate(); + // If the track is wider than necessary for the thumb, including when + // the buttons will cause Gecko to expand the track to fill + // available breadth, then add to the track border to prevent Gecko + // from expanding the thumb to fill available breadth. + gint extra = std::max(trackMinSize.height, aMetrics->size.button.height) - + trackSizeForThumb.height; + if (extra > 0) { + // If extra is odd, then the thumb is 0.5 pixels above + // center as in gtk_range_compute_slider_position(). + aMetrics->border.track.top += extra / 2; + aMetrics->border.track.bottom += extra - extra / 2; + // Update size for change in border. + trackSizeForThumb.height += extra; + } + } else { + gint extra = std::max(trackMinSize.width, aMetrics->size.button.width) - + trackSizeForThumb.width; + if (extra > 0) { + // If extra is odd, then the thumb is 0.5 pixels to the left + // of center as in gtk_range_compute_slider_position(). + aMetrics->border.track.left += extra / 2; + aMetrics->border.track.right += extra - extra / 2; + trackSizeForThumb.width += extra; + } + } + + style = + CreateStyleContextWithStates(contents, 1, GTK_TEXT_DIR_NONE, aStateFlags); + GtkBorder contentsBorder = GetMarginBorderPadding(style); + g_object_unref(style); + + aMetrics->size.scrollbar = + trackSizeForThumb + contentsBorder + aMetrics->border.scrollbar; +} + +const ScrollbarGTKMetrics* GetScrollbarMetrics(GtkOrientation aOrientation) { + auto metrics = &sScrollbarMetrics[aOrientation]; + if (!metrics->initialized) { + InitScrollbarMetrics(metrics, aOrientation, GTK_STATE_FLAG_NORMAL); + + // We calculate thumb margin here because it's composited from + // thumb class margin + difference margin between active and inactive + // scrollbars. It's a workaround which alows us to emulate + // overlay scrollbars for some Gtk+ themes (Ubuntu/Ambiance), + // when an inactive scrollbar thumb is smaller than the active one. + const ScrollbarGTKMetrics* metricsActive = + GetActiveScrollbarMetrics(aOrientation); + + if (metrics->size.thumb < metricsActive->size.thumb) { + metrics->margin.thumb += + (metrics->border.scrollbar + metrics->border.track) - + (metricsActive->border.scrollbar + metricsActive->border.track); + } + + metrics->initialized = true; + } + return metrics; +} + +const ScrollbarGTKMetrics* GetActiveScrollbarMetrics( + GtkOrientation aOrientation) { + auto metrics = &sActiveScrollbarMetrics[aOrientation]; + if (!metrics->initialized) { + InitScrollbarMetrics(metrics, aOrientation, GTK_STATE_FLAG_PRELIGHT); + metrics->initialized = true; + } + return metrics; +} + +/* + * get_shadow_width() from gtkwindow.c is not public so we need + * to implement it. + */ +void InitWindowDecorationSize(CSDWindowDecorationSize* sWindowDecorationSize, + bool aPopupWindow) { + bool solidDecorations = gtk_style_context_has_class( + GetStyleContext(MOZ_GTK_HEADERBAR_WINDOW, 1), "solid-csd"); + // solid-csd does not use frame extents, quit now. + if (solidDecorations) { + sWindowDecorationSize->decorationSize = {0, 0, 0, 0}; + return; + } + + // Scale factor is applied later when decoration size is used for actual + // gtk windows. + GtkStyleContext* context = GetStyleContext(MOZ_GTK_WINDOW_DECORATION); + + /* Always sum border + padding */ + GtkBorder padding; + GtkStateFlags state = gtk_style_context_get_state(context); + gtk_style_context_get_border(context, state, + &sWindowDecorationSize->decorationSize); + gtk_style_context_get_padding(context, state, &padding); + sWindowDecorationSize->decorationSize += padding; + + // Available on GTK 3.20+. + static auto sGtkRenderBackgroundGetClip = (void (*)( + GtkStyleContext*, gdouble, gdouble, gdouble, gdouble, + GdkRectangle*))dlsym(RTLD_DEFAULT, "gtk_render_background_get_clip"); + + if (!sGtkRenderBackgroundGetClip) { + return; + } + + GdkRectangle clip; + sGtkRenderBackgroundGetClip(context, 0, 0, 0, 0, &clip); + + GtkBorder extents; + extents.top = -clip.y; + extents.right = clip.width + clip.x; + extents.bottom = clip.height + clip.y; + extents.left = -clip.x; + + // Get shadow extents but combine with style margin; use the bigger value. + // Margin is used for resize grip size - it's not present on + // popup windows. + if (!aPopupWindow) { + GtkBorder margin; + gtk_style_context_get_margin(context, state, &margin); + + extents.top = MAX(extents.top, margin.top); + extents.right = MAX(extents.right, margin.right); + extents.bottom = MAX(extents.bottom, margin.bottom); + extents.left = MAX(extents.left, margin.left); + } + + sWindowDecorationSize->decorationSize += extents; +} + +GtkBorder GetCSDDecorationSize(bool aIsPopup) { + auto metrics = + aIsPopup ? &sPopupWindowDecorationSize : &sToplevelWindowDecorationSize; + if (!metrics->initialized) { + InitWindowDecorationSize(metrics, aIsPopup); + metrics->initialized = true; + } + return metrics->decorationSize; +} + +/* cairo_t *cr argument has to be a system-cairo. */ +gint moz_gtk_widget_paint(WidgetNodeType widget, cairo_t* cr, + GdkRectangle* rect, GtkWidgetState* state, gint flags, + GtkTextDirection direction) { + /* A workaround for https://bugzilla.gnome.org/show_bug.cgi?id=694086 + */ + cairo_new_path(cr); + + switch (widget) { + case MOZ_GTK_BUTTON: + case MOZ_GTK_TOOLBAR_BUTTON: + if (state->depressed) { + return moz_gtk_button_paint(cr, rect, state, (GtkReliefStyle)flags, + GetWidget(MOZ_GTK_TOGGLE_BUTTON), + direction); + } + return moz_gtk_button_paint(cr, rect, state, (GtkReliefStyle)flags, + GetWidget(MOZ_GTK_BUTTON), direction); + case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE: + case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE: + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE: + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE: + return moz_gtk_header_bar_button_paint( + cr, rect, state, (GtkReliefStyle)flags, widget, direction); + case MOZ_GTK_CHECKBUTTON: + case MOZ_GTK_RADIOBUTTON: + return moz_gtk_toggle_paint(cr, rect, state, + !!(flags & MOZ_GTK_WIDGET_CHECKED), + !!(flags & MOZ_GTK_WIDGET_INCONSISTENT), + (widget == MOZ_GTK_RADIOBUTTON), direction); + case MOZ_GTK_SCROLLBAR_BUTTON: + return moz_gtk_scrollbar_button_paint( + cr, rect, state, (GtkScrollbarButtonFlags)flags, direction); + case MOZ_GTK_SCROLLBAR_HORIZONTAL: + case MOZ_GTK_SCROLLBAR_VERTICAL: { + if (flags & MOZ_GTK_TRACK_OPAQUE) { + GtkStyleContext* style = GetStyleContext(MOZ_GTK_WINDOW, direction); + gtk_render_background(style, cr, rect->x, rect->y, rect->width, + rect->height); + } + if (gtk_check_version(3, 20, 0) == nullptr) { + return moz_gtk_scrollbar_paint(widget, cr, rect, state, direction); + } + WidgetNodeType trough_widget = (widget == MOZ_GTK_SCROLLBAR_HORIZONTAL) + ? MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL + : MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL; + return moz_gtk_scrollbar_trough_paint(trough_widget, cr, rect, state, + direction); + } + case MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL: + case MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL: + if (gtk_check_version(3, 20, 0) == nullptr) { + return moz_gtk_scrollbar_trough_paint(widget, cr, rect, state, + direction); + } + break; + case MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL: + case MOZ_GTK_SCROLLBAR_THUMB_VERTICAL: + return moz_gtk_scrollbar_thumb_paint(widget, cr, rect, state, direction); + case MOZ_GTK_SCALE_HORIZONTAL: + case MOZ_GTK_SCALE_VERTICAL: + return moz_gtk_scale_paint(cr, rect, state, (GtkOrientation)flags, + direction); + case MOZ_GTK_SCALE_THUMB_HORIZONTAL: + case MOZ_GTK_SCALE_THUMB_VERTICAL: + return moz_gtk_scale_thumb_paint(cr, rect, state, (GtkOrientation)flags, + direction); + case MOZ_GTK_INNER_SPIN_BUTTON: + return moz_gtk_inner_spin_paint(cr, rect, state, direction); + case MOZ_GTK_SPINBUTTON: + return moz_gtk_spin_paint(cr, rect, state, direction); + case MOZ_GTK_SPINBUTTON_UP: + case MOZ_GTK_SPINBUTTON_DOWN: + return moz_gtk_spin_updown_paint( + cr, rect, (widget == MOZ_GTK_SPINBUTTON_DOWN), state, direction); + case MOZ_GTK_SPINBUTTON_ENTRY: { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPINBUTTON_ENTRY, state->scale, direction, + GetStateFlagsFromGtkWidgetState(state)); + return moz_gtk_entry_paint(cr, rect, state, style, widget); + } + case MOZ_GTK_GRIPPER: + return moz_gtk_gripper_paint(cr, rect, state, direction); + case MOZ_GTK_TREEVIEW: + return moz_gtk_treeview_paint(cr, rect, state, direction); + case MOZ_GTK_TREE_HEADER_CELL: + return moz_gtk_tree_header_cell_paint(cr, rect, state, flags, direction); + case MOZ_GTK_TREE_HEADER_SORTARROW: + return moz_gtk_tree_header_sort_arrow_paint( + cr, rect, state, (GtkArrowType)flags, direction); + case MOZ_GTK_TREEVIEW_EXPANDER: + return moz_gtk_treeview_expander_paint( + cr, rect, state, (GtkExpanderStyle)flags, direction); + case MOZ_GTK_ENTRY: + case MOZ_GTK_DROPDOWN_ENTRY: { + GtkStyleContext* style = + GetStyleContext(widget, state->scale, direction, + GetStateFlagsFromGtkWidgetState(state)); + gint ret = moz_gtk_entry_paint(cr, rect, state, style, widget); + return ret; + } + case MOZ_GTK_TEXT_VIEW: + return moz_gtk_text_view_paint(cr, rect, state, direction); + case MOZ_GTK_DROPDOWN: + return moz_gtk_combo_box_paint(cr, rect, state, direction); + case MOZ_GTK_DROPDOWN_ARROW: + return moz_gtk_combo_box_entry_button_paint(cr, rect, state, flags, + direction); + case MOZ_GTK_CHECKBUTTON_CONTAINER: + case MOZ_GTK_RADIOBUTTON_CONTAINER: + return moz_gtk_container_paint(cr, rect, state, widget, direction); + case MOZ_GTK_CHECKBUTTON_LABEL: + case MOZ_GTK_RADIOBUTTON_LABEL: + return moz_gtk_toggle_label_paint( + cr, rect, state, (widget == MOZ_GTK_RADIOBUTTON_LABEL), direction); + case MOZ_GTK_TOOLBAR: + return moz_gtk_toolbar_paint(cr, rect, state, direction); + case MOZ_GTK_TOOLBAR_SEPARATOR: + return moz_gtk_toolbar_separator_paint(cr, rect, state, direction); + case MOZ_GTK_TOOLTIP: + return moz_gtk_tooltip_paint(cr, rect, state, direction); + case MOZ_GTK_FRAME: + return moz_gtk_frame_paint(cr, rect, state, direction); + case MOZ_GTK_RESIZER: + return moz_gtk_resizer_paint(cr, rect, state, direction); + case MOZ_GTK_PROGRESSBAR: + return moz_gtk_progressbar_paint(cr, rect, state, direction); + case MOZ_GTK_PROGRESS_CHUNK: + case MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE: + case MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE: + return moz_gtk_progress_chunk_paint(cr, rect, state, direction, widget); + case MOZ_GTK_TAB_TOP: + case MOZ_GTK_TAB_BOTTOM: + return moz_gtk_tab_paint(cr, rect, state, (GtkTabFlags)flags, direction, + widget); + case MOZ_GTK_TABPANELS: + return moz_gtk_tabpanels_paint(cr, rect, state, direction); + case MOZ_GTK_TAB_SCROLLARROW: + return moz_gtk_tab_scroll_arrow_paint(cr, rect, state, + (GtkArrowType)flags, direction); + case MOZ_GTK_MENUBAR: + return moz_gtk_menu_bar_paint(cr, rect, state, direction); + case MOZ_GTK_MENUPOPUP: + return moz_gtk_menu_popup_paint(cr, rect, state, direction); + case MOZ_GTK_MENUSEPARATOR: + return moz_gtk_menu_separator_paint(cr, rect, state, direction); + case MOZ_GTK_MENUBARITEM: + case MOZ_GTK_MENUITEM: + return moz_gtk_menu_item_paint(widget, cr, rect, state, direction); + case MOZ_GTK_MENUARROW: + return moz_gtk_menu_arrow_paint(cr, rect, state, direction); + case MOZ_GTK_TOOLBARBUTTON_ARROW: + return moz_gtk_arrow_paint(cr, rect, state, (GtkArrowType)flags, + direction); + case MOZ_GTK_CHECKMENUITEM: + case MOZ_GTK_RADIOMENUITEM: + return moz_gtk_check_menu_item_paint(widget, cr, rect, state, + (gboolean)flags, direction); + case MOZ_GTK_SPLITTER_HORIZONTAL: + return moz_gtk_vpaned_paint(cr, rect, state); + case MOZ_GTK_SPLITTER_VERTICAL: + return moz_gtk_hpaned_paint(cr, rect, state); + case MOZ_GTK_WINDOW: + return moz_gtk_window_paint(cr, rect, direction); + case MOZ_GTK_INFO_BAR: + return moz_gtk_info_bar_paint(cr, rect, state); + case MOZ_GTK_HEADER_BAR: + case MOZ_GTK_HEADER_BAR_MAXIMIZED: + return moz_gtk_header_bar_paint(widget, cr, rect, state); + default: + g_warning("Unknown widget type: %d", widget); + } + + return MOZ_GTK_UNKNOWN_WIDGET; +} + +gint moz_gtk_shutdown() { + /* This will destroy all of our widgets */ + ResetWidgetCache(); + + return MOZ_GTK_SUCCESS; +} diff --git a/widget/gtk/gtkdrawing.h b/widget/gtk/gtkdrawing.h new file mode 100644 index 0000000000..38dc1587fc --- /dev/null +++ b/widget/gtk/gtkdrawing.h @@ -0,0 +1,634 @@ +/* -*- 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/. */ + +/** + * gtkdrawing.h: GTK widget rendering utilities + * + * gtkdrawing provides an API for rendering GTK widgets in the + * current theme to a pixmap or window, without requiring an actual + * widget instantiation, similar to the Macintosh Appearance Manager + * or Windows XP's DrawThemeBackground() API. + */ + +#ifndef _GTK_DRAWING_H_ +#define _GTK_DRAWING_H_ + +#include <gdk/gdk.h> +#include <gtk/gtk.h> +#include <algorithm> +#include "mozilla/Span.h" + +/*** type definitions ***/ +typedef struct { + guint8 active; + guint8 focused; + guint8 selected; + guint8 inHover; + guint8 disabled; + guint8 isDefault; + guint8 canDefault; + /* The depressed state is for buttons which remain active for a longer period: + * activated toggle buttons or buttons showing a popup menu. */ + guint8 depressed; + guint8 backdrop; + gint32 curpos; /* curpos and maxpos are used for scrollbars */ + gint32 maxpos; + gint32 scale; /* actual widget scale */ +} GtkWidgetState; + +/** + * A size in the same GTK pixel units as GtkBorder and GdkRectangle. + */ +struct MozGtkSize { + gint width; + gint height; + + MozGtkSize& operator+=(const GtkBorder& aBorder) { + width += aBorder.left + aBorder.right; + height += aBorder.top + aBorder.bottom; + return *this; + } + MozGtkSize operator+(const GtkBorder& aBorder) const { + MozGtkSize result = *this; + return result += aBorder; + } + bool operator<(const MozGtkSize& aOther) const { + return (width < aOther.width && height <= aOther.height) || + (width <= aOther.width && height < aOther.height); + } + void Include(MozGtkSize aOther) { + width = std::max(width, aOther.width); + height = std::max(height, aOther.height); + } + void Rotate() { + gint tmp = width; + width = height; + height = tmp; + } +}; + +typedef struct { + bool initialized; + struct { + MozGtkSize scrollbar; + MozGtkSize thumb; + MozGtkSize button; + } size; + struct { + GtkBorder scrollbar; + GtkBorder track; + } border; + struct { + GtkBorder thumb; + } margin; +} ScrollbarGTKMetrics; + +typedef struct { + bool initialized; + MozGtkSize minSizeWithBorder; + GtkBorder borderAndPadding; +} ToggleGTKMetrics; + +typedef struct { + MozGtkSize minSizeWithBorderMargin; + GtkBorder buttonMargin; + gint iconXPosition; + gint iconYPosition; + bool visible; + bool firstButton; + bool lastButton; +} ToolbarButtonGTKMetrics; + +#define TOOLBAR_BUTTONS 3 +typedef struct { + bool initialized; + ToolbarButtonGTKMetrics button[TOOLBAR_BUTTONS]; +} ToolbarGTKMetrics; + +typedef struct { + bool initialized; + GtkBorder decorationSize; +} CSDWindowDecorationSize; + +typedef enum { + MOZ_GTK_STEPPER_DOWN = 1 << 0, + MOZ_GTK_STEPPER_BOTTOM = 1 << 1, + MOZ_GTK_STEPPER_VERTICAL = 1 << 2 +} GtkScrollbarButtonFlags; + +typedef enum { MOZ_GTK_TRACK_OPAQUE = 1 << 0 } GtkScrollbarTrackFlags; + +/** flags for tab state **/ +typedef enum { + /* first eight bits are used to pass a margin */ + MOZ_GTK_TAB_MARGIN_MASK = 0xFF, + /* the first tab in the group */ + MOZ_GTK_TAB_FIRST = 1 << 9, + /* the selected tab */ + MOZ_GTK_TAB_SELECTED = 1 << 10 +} GtkTabFlags; + +/*** result/error codes ***/ +#define MOZ_GTK_SUCCESS 0 +#define MOZ_GTK_UNKNOWN_WIDGET -1 +#define MOZ_GTK_UNSAFE_THEME -2 + +/*** checkbox/radio flags ***/ +#define MOZ_GTK_WIDGET_CHECKED 1 +#define MOZ_GTK_WIDGET_INCONSISTENT (1 << 1) + +/*** widget type constants ***/ +enum WidgetNodeType : int { + /* Paints a GtkButton. flags is a GtkReliefStyle. */ + MOZ_GTK_BUTTON, + /* Paints a button with image and no text */ + MOZ_GTK_TOOLBAR_BUTTON, + /* Paints a toggle button */ + MOZ_GTK_TOGGLE_BUTTON, + /* Paints a button arrow */ + MOZ_GTK_BUTTON_ARROW, + + /* Paints the container part of a GtkCheckButton. */ + MOZ_GTK_CHECKBUTTON_CONTAINER, + /* Paints a GtkCheckButton. flags is a boolean, 1=checked, 0=not checked. */ + MOZ_GTK_CHECKBUTTON, + /* Paints the label of a GtkCheckButton (focus outline) */ + MOZ_GTK_CHECKBUTTON_LABEL, + + /* Paints the container part of a GtkRadioButton. */ + MOZ_GTK_RADIOBUTTON_CONTAINER, + /* Paints a GtkRadioButton. flags is a boolean, 1=checked, 0=not checked. */ + MOZ_GTK_RADIOBUTTON, + /* Paints the label of a GtkRadioButton (focus outline) */ + MOZ_GTK_RADIOBUTTON_LABEL, + /** + * Paints the button of a GtkScrollbar. flags is a GtkArrowType giving + * the arrow direction. + */ + MOZ_GTK_SCROLLBAR_BUTTON, + + /* Horizontal GtkScrollbar counterparts */ + MOZ_GTK_SCROLLBAR_HORIZONTAL, + MOZ_GTK_SCROLLBAR_CONTENTS_HORIZONTAL, + /* Paints the trough (track) of a GtkScrollbar. */ + MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL, + /* Paints the slider (thumb) of a GtkScrollbar. */ + MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL, + + /* Vertical GtkScrollbar counterparts */ + MOZ_GTK_SCROLLBAR_VERTICAL, + MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL, + MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL, + MOZ_GTK_SCROLLBAR_THUMB_VERTICAL, + + /* Paints a GtkScale. */ + MOZ_GTK_SCALE_HORIZONTAL, + MOZ_GTK_SCALE_VERTICAL, + /* Paints a GtkScale trough. */ + MOZ_GTK_SCALE_CONTENTS_HORIZONTAL, + MOZ_GTK_SCALE_CONTENTS_VERTICAL, + MOZ_GTK_SCALE_TROUGH_HORIZONTAL, + MOZ_GTK_SCALE_TROUGH_VERTICAL, + /* Paints a GtkScale thumb. */ + MOZ_GTK_SCALE_THUMB_HORIZONTAL, + MOZ_GTK_SCALE_THUMB_VERTICAL, + /* Paints a GtkSpinButton */ + MOZ_GTK_INNER_SPIN_BUTTON, + MOZ_GTK_SPINBUTTON, + MOZ_GTK_SPINBUTTON_UP, + MOZ_GTK_SPINBUTTON_DOWN, + MOZ_GTK_SPINBUTTON_ENTRY, + /* Paints the gripper of a GtkHandleBox. */ + MOZ_GTK_GRIPPER, + /* Paints a GtkEntry. */ + MOZ_GTK_ENTRY, + /* Paints a GtkExpander. */ + MOZ_GTK_EXPANDER, + /* Paints a GtkTextView or gets the style context corresponding to the + root node of a GtkTextView. */ + MOZ_GTK_TEXT_VIEW, + /* The "text" window or node of a GtkTextView */ + MOZ_GTK_TEXT_VIEW_TEXT, + /* The "selection" node of a GtkTextView.text */ + MOZ_GTK_TEXT_VIEW_TEXT_SELECTION, + /* Paints a GtkOptionMenu. */ + MOZ_GTK_DROPDOWN, + /* Paints a dropdown arrow (a GtkButton containing a down GtkArrow). */ + MOZ_GTK_DROPDOWN_ARROW, + /* Paints an entry in an editable option menu */ + MOZ_GTK_DROPDOWN_ENTRY, + + /* Paints the background of a GtkHandleBox. */ + MOZ_GTK_TOOLBAR, + /* Paints a toolbar separator */ + MOZ_GTK_TOOLBAR_SEPARATOR, + /* Paints a GtkToolTip */ + MOZ_GTK_TOOLTIP, + /* Paints a GtkBox from GtkToolTip */ + MOZ_GTK_TOOLTIP_BOX, + /* Paints a GtkLabel of GtkToolTip */ + MOZ_GTK_TOOLTIP_BOX_LABEL, + /* Paints a GtkFrame (e.g. a status bar panel). */ + MOZ_GTK_FRAME, + /* Paints the border of a GtkFrame */ + MOZ_GTK_FRAME_BORDER, + /* Paints a resize grip for a GtkTextView */ + MOZ_GTK_RESIZER, + /* Paints a GtkProgressBar. */ + MOZ_GTK_PROGRESSBAR, + /* Paints a trough (track) of a GtkProgressBar */ + MOZ_GTK_PROGRESS_TROUGH, + /* Paints a progress chunk of a GtkProgressBar. */ + MOZ_GTK_PROGRESS_CHUNK, + /* Paints a progress chunk of an indeterminated GtkProgressBar. */ + MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE, + /* Paints a progress chunk of a vertical indeterminated GtkProgressBar. */ + MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE, + /* Used as root style of whole GtkNotebook widget */ + MOZ_GTK_NOTEBOOK, + /* Used as root style of active GtkNotebook area which contains tabs and + arrows. */ + MOZ_GTK_NOTEBOOK_HEADER, + /* Paints a tab of a GtkNotebook. flags is a GtkTabFlags, defined above. */ + MOZ_GTK_TAB_TOP, + /* Paints a tab of a GtkNotebook. flags is a GtkTabFlags, defined above. */ + MOZ_GTK_TAB_BOTTOM, + /* Paints the background and border of a GtkNotebook. */ + MOZ_GTK_TABPANELS, + /* Paints a GtkArrow for a GtkNotebook. flags is a GtkArrowType. */ + MOZ_GTK_TAB_SCROLLARROW, + /* Paints the expander and border of a GtkTreeView */ + MOZ_GTK_TREEVIEW, + /* Paints the border of a GtkTreeView */ + MOZ_GTK_TREEVIEW_VIEW, + /* Paints treeheader cells */ + MOZ_GTK_TREE_HEADER_CELL, + /* Paints sort arrows in treeheader cells */ + MOZ_GTK_TREE_HEADER_SORTARROW, + /* Paints an expander for a GtkTreeView */ + MOZ_GTK_TREEVIEW_EXPANDER, + /* Paints the background of the menu bar. */ + MOZ_GTK_MENUBAR, + /* Paints the background of menus, context menus. */ + MOZ_GTK_MENUPOPUP, + /* Paints the arrow of menuitems that contain submenus */ + MOZ_GTK_MENUARROW, + /* Paints an arrow in a toolbar button. flags is a GtkArrowType. */ + MOZ_GTK_TOOLBARBUTTON_ARROW, + /* Paints items of menubar. */ + MOZ_GTK_MENUBARITEM, + /* Paints items of popup menus. */ + MOZ_GTK_MENUITEM, + /* Paints a menuitem with check indicator, or the gets the style context for + a menuitem that contains a checkbox. */ + MOZ_GTK_CHECKMENUITEM, + /* Gets the style context for a checkbox in a check menuitem. */ + MOZ_GTK_CHECKMENUITEM_INDICATOR, + MOZ_GTK_RADIOMENUITEM, + MOZ_GTK_RADIOMENUITEM_INDICATOR, + MOZ_GTK_MENUSEPARATOR, + /* GtkVPaned base class */ + MOZ_GTK_SPLITTER_HORIZONTAL, + /* GtkHPaned base class */ + MOZ_GTK_SPLITTER_VERTICAL, + /* Paints a GtkVPaned separator */ + MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL, + /* Paints a GtkHPaned separator */ + MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL, + /* Paints the background of a window, dialog or page. */ + MOZ_GTK_WINDOW, + /* Used only as a container for MOZ_GTK_HEADER_BAR. */ + MOZ_GTK_HEADERBAR_WINDOW, + /* Used only as a container for MOZ_GTK_HEADER_BAR_MAXIMIZED. */ + MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED, + /* Window container for all widgets */ + MOZ_GTK_WINDOW_CONTAINER, + /* Paints a GtkInfoBar, for notifications. */ + MOZ_GTK_INFO_BAR, + /* Used for widget tree construction. */ + MOZ_GTK_COMBOBOX, + /* Paints a GtkComboBox button widget. */ + MOZ_GTK_COMBOBOX_BUTTON, + /* Paints a GtkComboBox arrow widget. */ + MOZ_GTK_COMBOBOX_ARROW, + /* Paints a GtkComboBox separator widget. */ + MOZ_GTK_COMBOBOX_SEPARATOR, + /* Used for widget tree construction. */ + MOZ_GTK_COMBOBOX_ENTRY, + /* Paints a GtkComboBox entry widget. */ + MOZ_GTK_COMBOBOX_ENTRY_TEXTAREA, + /* Paints a GtkComboBox entry button widget. */ + MOZ_GTK_COMBOBOX_ENTRY_BUTTON, + /* Paints a GtkComboBox entry arrow widget. */ + MOZ_GTK_COMBOBOX_ENTRY_ARROW, + /* Used for scrolled window shell. */ + MOZ_GTK_SCROLLED_WINDOW, + /* Paints a GtkHeaderBar */ + MOZ_GTK_HEADER_BAR, + /* Paints a GtkHeaderBar in maximized state */ + MOZ_GTK_HEADER_BAR_MAXIMIZED, + /* Container for GtkHeaderBar buttons */ + MOZ_GTK_HEADER_BAR_BUTTON_BOX, + /* Paints GtkHeaderBar title buttons. + * Keep the order here as MOZ_GTK_HEADER_BAR_BUTTON_* are processed + * as an array from MOZ_GTK_HEADER_BAR_BUTTON_CLOSE to the last one. + */ + MOZ_GTK_HEADER_BAR_BUTTON_CLOSE, + MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE, + MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE, + + /* MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE is a state of + * MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE button and it's used as + * an icon placeholder only. + */ + MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE, + + /* Client-side window decoration node. Available on GTK 3.20+. */ + MOZ_GTK_WINDOW_DECORATION, + MOZ_GTK_WINDOW_DECORATION_SOLID, + + MOZ_GTK_WIDGET_NODE_COUNT +}; + +/* ButtonLayout represents a GTK CSD button and whether its on the left or + * right side of the tab bar */ +struct ButtonLayout { + WidgetNodeType mType; + bool mAtRight; +}; + +/*** General library functions ***/ +/** + * Initializes the drawing library. You must call this function + * prior to using any other functionality. + * returns: MOZ_GTK_SUCCESS if there were no errors + * MOZ_GTK_UNSAFE_THEME if the current theme engine is known + * to crash with gtkdrawing. + */ +gint moz_gtk_init(); + +/** + * Updates the drawing library when the theme changes. + */ +void moz_gtk_refresh(); + +/** + * Perform cleanup of the drawing library. You should call this function + * when your program exits, or you no longer need the library. + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_shutdown(); + +/*** Widget drawing ***/ +/** + * Paint a widget in the current theme. + * widget: a constant giving the widget to paint + * drawable: the drawable to paint to; + * it's colormap must be moz_gtk_widget_get_colormap(). + * rect: the bounding rectangle for the widget + * state: the state of the widget. ignored for some widgets. + * flags: widget-dependant flags; see the WidgetNodeType definition. + * direction: the text direction, to draw the widget correctly LTR and RTL. + */ +gint moz_gtk_widget_paint(WidgetNodeType widget, cairo_t* cr, + GdkRectangle* rect, GtkWidgetState* state, gint flags, + GtkTextDirection direction); + +/*** Widget metrics ***/ +/** + * Get the border size of a widget + * left/right: [OUT] the widget's left/right border + * top/bottom: [OUT] the widget's top/bottom border + * direction: the text direction for the widget. Callers depend on this + * being used only for MOZ_GTK_DROPDOWN widgets, and cache + * results for other widget types across direction values. + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_widget_border(WidgetNodeType widget, gint* left, gint* top, + gint* right, gint* bottom, + GtkTextDirection direction); + +/** + * Get the border size of a notebook tab + * left/right: [OUT] the tab's left/right border + * top/bottom: [OUT] the tab's top/bottom border + * direction: the text direction for the widget + * flags: tab-dependant flags; see the GtkTabFlags definition. + * widget: tab widget + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_tab_border(gint* left, gint* top, gint* right, gint* bottom, + GtkTextDirection direction, GtkTabFlags flags, + WidgetNodeType widget); + +/** + * Get the desired size of a GtkCheckButton + * indicator_size: [OUT] the indicator size + * indicator_spacing: [OUT] the spacing between the indicator and its + * container + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_checkbox_get_metrics(gint* indicator_size, + gint* indicator_spacing); + +/** + * Get metrics of the toggle (radio or checkbox) + * isRadio: [IN] true when requesting metrics for the radio button + * returns: pointer to ToggleGTKMetrics struct + */ +const ToggleGTKMetrics* GetToggleMetrics(WidgetNodeType aWidgetType); + +/** + * Get the desired size of a GtkRadioButton + * indicator_size: [OUT] the indicator size + * indicator_spacing: [OUT] the spacing between the indicator and its + * container + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_radio_get_metrics(gint* indicator_size, gint* indicator_spacing); + +/** Returns the size of the focus ring for outline:auto. + * focus_h_width: [OUT] the horizontal width + * focus_v_width: [OUT] the vertical width + * + * returns: MOZ_GTK_SUCCESS + */ +gint moz_gtk_get_focus_outline_size(gint* focus_h_width, gint* focus_v_width); + +/** Get the horizontal padding for the menuitem widget or checkmenuitem widget. + * horizontal_padding: [OUT] The left and right padding of the menuitem or + * checkmenuitem + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_menuitem_get_horizontal_padding(gint* horizontal_padding); + +gint moz_gtk_checkmenuitem_get_horizontal_padding(gint* horizontal_padding); + +/** + * Some GTK themes draw their indication for the default button outside + * the button (e.g. the glow in New Wave). This gets the extra space necessary. + * + * border_top: [OUT] extra space to add above + * border_left: [OUT] extra space to add to the left + * border_bottom: [OUT] extra space to add underneath + * border_right: [OUT] extra space to add to the right + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_button_get_default_overflow(gint* border_top, gint* border_left, + gint* border_bottom, + gint* border_right); + +/** + * Gets the minimum size of a GtkScale. + * orient: [IN] the scale orientation + * scale_width: [OUT] the width of the scale + * scale_height: [OUT] the height of the scale + */ +void moz_gtk_get_scale_metrics(GtkOrientation orient, gint* scale_width, + gint* scale_height); + +/** + * Get the desired size of a GtkScale thumb + * orient: [IN] the scale orientation + * thumb_length: [OUT] the length of the thumb + * thumb_height: [OUT] the height of the thumb + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_scalethumb_metrics(GtkOrientation orient, gint* thumb_length, + gint* thumb_height); + +/** + * Get the metrics in GTK pixels for a scrollbar. + * aOrientation: [IN] the scrollbar orientation + */ +const ScrollbarGTKMetrics* GetScrollbarMetrics(GtkOrientation aOrientation); + +/** + * Get the metrics in GTK pixels for a scrollbar which is active + * (selected by mouse pointer). + * aOrientation: [IN] the scrollbar orientation + */ +const ScrollbarGTKMetrics* GetActiveScrollbarMetrics( + GtkOrientation aOrientation); + +/** + * Get the desired size of a dropdown arrow button + * width: [OUT] the desired width + * height: [OUT] the desired height + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_combo_box_entry_button_size(gint* width, gint* height); + +/** + * Get the desired size of a scroll arrow widget + * width: [OUT] the desired width + * height: [OUT] the desired height + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_tab_scroll_arrow_size(gint* width, gint* height); + +/** + * Get the desired size of an arrow in a button + * + * widgetType: [IN] the widget for which to get the arrow size + * width: [OUT] the desired width + * height: [OUT] the desired height + */ +void moz_gtk_get_arrow_size(WidgetNodeType widgetType, gint* width, + gint* height); + +/** + * Get the minimum height of a entry widget + * min_content_height: [OUT] the minimum height of the content box. + * border_padding_height: [OUT] the size of borders and paddings. + */ +void moz_gtk_get_entry_min_height(gint* min_content_height, + gint* border_padding_height); + +/** + * Get the desired size of a toolbar separator + * size: [OUT] the desired width + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_toolbar_separator_width(gint* size); + +/** + * Get the size of a regular GTK expander that shows/hides content + * size: [OUT] the size of the GTK expander, size = width = height. + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_expander_size(gint* size); + +/** + * Get the size of a treeview's expander (we call them twisties) + * size: [OUT] the size of the GTK expander, size = width = height. + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_treeview_expander_size(gint* size); + +/** + * Get the desired height of a menu separator + * size: [OUT] the desired height + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_get_menu_separator_height(gint* size); + +/** + * Get the desired size of a splitter + * orientation: [IN] GTK_ORIENTATION_HORIZONTAL or GTK_ORIENTATION_VERTICAL + * size: [OUT] width or height of the splitter handle + * + * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise + */ +gint moz_gtk_splitter_get_metrics(gint orientation, gint* size); + +/** + * Get the YTHICKNESS of a tab (notebook extension). + */ +gint moz_gtk_get_tab_thickness(WidgetNodeType aNodeType); + +/** + * Get ToolbarButtonGTKMetrics for recent theme. + */ +const ToolbarButtonGTKMetrics* GetToolbarButtonMetrics( + WidgetNodeType aAppearance); + +/** + * Get toolbar button layout. + * aButtonLayout: [OUT] An array which will be filled by ButtonLayout + * references to visible titlebar buttons. Must contain at + * least TOOLBAR_BUTTONS entries if non-empty. + * aReversedButtonsPlacement: [OUT] True if the buttons are placed in opposite + * titlebar corner. + * + * returns: Number of returned entries at aButtonLayout. + */ +size_t GetGtkHeaderBarButtonLayout(mozilla::Span<ButtonLayout>, + bool* aReversedButtonsPlacement); + +/** + * Get size of CSD window extents. + * + * aIsPopup: [IN] Get decoration size for popup or toplevel window. + * + * returns: Calculated (or estimated) decoration size of given aGtkWindow. + */ +GtkBorder GetCSDDecorationSize(bool aIsPopup); + +#endif diff --git a/widget/gtk/maiRedundantObjectFactory.c b/widget/gtk/maiRedundantObjectFactory.c new file mode 100644 index 0000000000..ce086e20af --- /dev/null +++ b/widget/gtk/maiRedundantObjectFactory.c @@ -0,0 +1,81 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 <atk/atk.h> +#include "maiRedundantObjectFactory.h" + +static void mai_redundant_object_factory_class_init( + maiRedundantObjectFactoryClass* klass); + +static AtkObject* mai_redundant_object_factory_create_accessible(GObject* obj); +static GType mai_redundant_object_factory_get_accessible_type(void); + +GType mai_redundant_object_factory_get_type(void) { + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = { + sizeof(maiRedundantObjectFactoryClass), + (GBaseInitFunc)NULL, /* base init */ + (GBaseFinalizeFunc)NULL, /* base finalize */ + (GClassInitFunc) + mai_redundant_object_factory_class_init, /* class init */ + (GClassFinalizeFunc)NULL, /* class finalize */ + NULL, /* class data */ + sizeof(maiRedundantObjectFactory), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc)NULL, /* instance init */ + NULL /* value table */ + }; + type = g_type_register_static(ATK_TYPE_OBJECT_FACTORY, + "MaiRedundantObjectFactory", &tinfo, 0); + } + + return type; +} + +static void mai_redundant_object_factory_class_init( + maiRedundantObjectFactoryClass* klass) { + AtkObjectFactoryClass* class = ATK_OBJECT_FACTORY_CLASS(klass); + + class->create_accessible = mai_redundant_object_factory_create_accessible; + class->get_accessible_type = mai_redundant_object_factory_get_accessible_type; +} + +/** + * mai_redundant_object_factory_new: + * + * Creates an instance of an #AtkObjectFactory which generates primitive + * (non-functioning) #AtkObjects. + * + * Returns: an instance of an #AtkObjectFactory + **/ +AtkObjectFactory* mai_redundant_object_factory_new() { + GObject* factory; + + factory = g_object_new(mai_redundant_object_factory_get_type(), NULL); + + g_return_val_if_fail(factory != NULL, NULL); + return ATK_OBJECT_FACTORY(factory); +} + +static AtkObject* mai_redundant_object_factory_create_accessible(GObject* obj) { + AtkObject* accessible; + + g_return_val_if_fail(obj != NULL, NULL); + + accessible = g_object_new(ATK_TYPE_OBJECT, NULL); + g_return_val_if_fail(accessible != NULL, NULL); + + accessible->role = ATK_ROLE_REDUNDANT_OBJECT; + + return accessible; +} + +static GType mai_redundant_object_factory_get_accessible_type() { + return mai_redundant_object_factory_get_type(); +} diff --git a/widget/gtk/maiRedundantObjectFactory.h b/widget/gtk/maiRedundantObjectFactory.h new file mode 100644 index 0000000000..931adffb6b --- /dev/null +++ b/widget/gtk/maiRedundantObjectFactory.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 __MAI_REDUNDANT_OBJECT_FACTORY_H__ +#define __MAI_REDUNDANT_OBJECT_FACTORY_H__ + +G_BEGIN_DECLS + +typedef struct _maiRedundantObjectFactory maiRedundantObjectFactory; +typedef struct _maiRedundantObjectFactoryClass maiRedundantObjectFactoryClass; + +struct _maiRedundantObjectFactory { + AtkObjectFactory parent; +}; + +struct _maiRedundantObjectFactoryClass { + AtkObjectFactoryClass parent_class; +}; + +GType mai_redundant_object_factory_get_type(); + +AtkObjectFactory* mai_redundant_object_factory_new(); + +G_END_DECLS + +#endif /* __NS_MAI_REDUNDANT_OBJECT_FACTORY_H__ */ diff --git a/widget/gtk/moz.build b/widget/gtk/moz.build new file mode 100644 index 0000000000..cddd45e49f --- /dev/null +++ b/widget/gtk/moz.build @@ -0,0 +1,178 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Widget: Gtk") + +with Files("*CompositorWidget*"): + BUG_COMPONENT = ("Core", "Graphics") + +with Files("*WindowSurface*"): + BUG_COMPONENT = ("Core", "Graphics") + +with Files("*IMContextWrapper*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*nsGtkKeyUtils*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + DIRS += ["mozgtk"] + +if CONFIG["MOZ_WAYLAND"]: + DIRS += ["wayland", "mozwayland"] + +EXPORTS += [ + "MozContainer.h", + "nsGTKToolkit.h", + "nsIImageToPixbuf.h", +] + +EXPORTS.mozilla += ["WidgetUtilsGtk.h"] + +UNIFIED_SOURCES += [ + "IMContextWrapper.cpp", + "MozContainer.cpp", + "MPRISServiceHandler.cpp", + "NativeKeyBindings.cpp", + "nsAppShell.cpp", + "nsBidiKeyboard.cpp", + "nsColorPicker.cpp", + "nsFilePicker.cpp", + "nsGtkKeyUtils.cpp", + "nsImageToPixbuf.cpp", + "nsLookAndFeel.cpp", + "nsNativeBasicThemeGTK.cpp", + "nsNativeThemeGTK.cpp", + "nsSound.cpp", + "nsToolkit.cpp", + "nsWidgetFactory.cpp", + "ScreenHelperGTK.cpp", + "TaskbarProgress.cpp", + "WakeLockListener.cpp", + "WidgetTraceEvent.cpp", + "WidgetUtilsGtk.cpp", +] + +SOURCES += [ + "MediaKeysEventSourceFactory.cpp", + "nsWindow.cpp", # conflicts with X11 headers + "WaylandVsyncSource.cpp", # conflicts with X11 headers +] + +if CONFIG["MOZ_X11"]: + UNIFIED_SOURCES += [ + "CompositorWidgetChild.cpp", + "CompositorWidgetParent.cpp", + "GtkCompositorWidget.cpp", + "InProcessGtkCompositorWidget.cpp", + "nsUserIdleServiceGTK.cpp", + ] + EXPORTS.mozilla.widget += [ + "CompositorWidgetChild.h", + "CompositorWidgetParent.h", + "GtkCompositorWidget.h", + "InProcessGtkCompositorWidget.h", + ] + +if CONFIG["NS_PRINTING"]: + UNIFIED_SOURCES += [ + "nsDeviceContextSpecG.cpp", + "nsPrintDialogGTK.cpp", + "nsPrintSettingsGTK.cpp", + "nsPrintSettingsServiceGTK.cpp", + ] + +if CONFIG["MOZ_X11"]: + UNIFIED_SOURCES += [ + "nsClipboard.cpp", + "nsClipboardX11.cpp", + "nsDragService.cpp", + "WindowSurfaceProvider.cpp", + "WindowSurfaceX11.cpp", + "WindowSurfaceX11Image.cpp", + "WindowSurfaceXRender.cpp", + ] + EXPORTS.mozilla.widget += [ + "WindowSurfaceProvider.h", + ] + +if CONFIG["MOZ_WAYLAND"]: + UNIFIED_SOURCES += [ + "DMABufLibWrapper.cpp", + "DMABufSurface.cpp", + "MozContainerWayland.cpp", + "nsClipboardWayland.cpp", + "nsWaylandDisplay.cpp", + "WindowSurfaceWayland.cpp", + ] + EXPORTS.mozilla.widget += [ + "DMABufLibWrapper.h", + "DMABufSurface.h", + "MozContainerWayland.h", + "nsWaylandDisplay.h", + ] + +if CONFIG["ACCESSIBILITY"]: + UNIFIED_SOURCES += [ + "maiRedundantObjectFactory.c", + ] + +UNIFIED_SOURCES += [ + "gtk3drawing.cpp", + "nsApplicationChooser.cpp", + "WidgetStyleCache.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/layout/base", + "/layout/forms", + "/layout/generic", + "/layout/xul", + "/other-licenses/atk-1.0", + "/third_party/cups/include", + "/widget", + "/widget/headless", +] + +if CONFIG["MOZ_X11"]: + LOCAL_INCLUDES += [ + "/widget/x11", + ] + +DEFINES["CAIRO_GFX"] = True + +DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] + +# When building with GTK3, the widget code always needs to use +# system Cairo headers, regardless of whether we are also linked +# against and using in-tree Cairo. By not using in-tree Cairo +# headers, we avoid picking up our renamed symbols, and instead +# use only system Cairo symbols that GTK3 uses. This allows that +# any Cairo objects created can be freely passed back and forth +# between the widget code and GTK3. +if not (CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk" and CONFIG["MOZ_TREE_CAIRO"]): + CXXFLAGS += CONFIG["MOZ_CAIRO_CFLAGS"] + +CFLAGS += CONFIG["TK_CFLAGS"] +CXXFLAGS += CONFIG["TK_CFLAGS"] + +if CONFIG["MOZ_WAYLAND"]: + CFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"] + +if CONFIG["MOZ_ENABLE_DBUS"]: + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + +CXXFLAGS += ["-Wno-error=shadow"] diff --git a/widget/gtk/mozgtk/gtk2/moz.build b/widget/gtk/mozgtk/gtk2/moz.build new file mode 100644 index 0000000000..93e43c3957 --- /dev/null +++ b/widget/gtk/mozgtk/gtk2/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "../mozgtk.c", +] + +DEFINES["GTK3_SYMBOLS"] = True + +SharedLibrary("mozgtk2") + +SHARED_LIBRARY_NAME = "mozgtk" + +FINAL_TARGET = "dist/bin/gtk2" + +# If LDFLAGS contains -Wl,--as-needed or if it's the default for the toolchain, +# we need to add -Wl,--no-as-needed before the gtk libraries, otherwise the +# linker will drop those dependencies because no symbols are used from them. +# But those dependencies need to be kept for things to work properly. +# Ideally, we'd only add -Wl,--no-as-needed if necessary, but it's just simpler +# to add it unconditionally. This library is also simple enough that forcing +# -Wl,--as-needed after the gtk libraries is not going to make a significant +# difference. +if CONFIG["GCC_USE_GNU_LD"]: + no_as_needed = ["-Wl,--no-as-needed"] + as_needed = ["-Wl,--as-needed"] +else: + no_as_needed = [] + as_needed = [] + +OS_LIBS += [f for f in CONFIG["MOZ_GTK2_LIBS"] if f.startswith("-L")] +OS_LIBS += no_as_needed +OS_LIBS += [ + "gtk-x11-2.0", + "gdk-x11-2.0", +] +OS_LIBS += as_needed diff --git a/widget/gtk/mozgtk/gtk3/moz.build b/widget/gtk/mozgtk/gtk3/moz.build new file mode 100644 index 0000000000..b4ab68ecb1 --- /dev/null +++ b/widget/gtk/mozgtk/gtk3/moz.build @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "../mozgtk.c", +] + +DEFINES["GTK2_SYMBOLS"] = True + +SharedLibrary("mozgtk") + +SONAME = "mozgtk" + +# If LDFLAGS contains -Wl,--as-needed or if it's the default for the toolchain, +# we need to add -Wl,--no-as-needed before the gtk libraries, otherwise the +# linker will drop those dependencies because no symbols are used from them. +# But those dependencies need to be kept for things to work properly. +# Ideally, we'd only add -Wl,--no-as-needed if necessary, but it's just simpler +# to add it unconditionally. This library is also simple enough that forcing +# -Wl,--as-needed after the gtk libraries is not going to make a significant +# difference. +if CONFIG["GCC_USE_GNU_LD"]: + no_as_needed = ["-Wl,--no-as-needed"] + as_needed = ["-Wl,--as-needed"] +else: + no_as_needed = [] + as_needed = [] + +OS_LIBS += [f for f in CONFIG["MOZ_GTK3_LIBS"] if f.startswith("-L")] +OS_LIBS += no_as_needed +OS_LIBS += [ + "gtk-3", + "gdk-3", +] +OS_LIBS += as_needed diff --git a/widget/gtk/mozgtk/moz.build b/widget/gtk/mozgtk/moz.build new file mode 100644 index 0000000000..8288583745 --- /dev/null +++ b/widget/gtk/mozgtk/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ["stub", "gtk2", "gtk3"] diff --git a/widget/gtk/mozgtk/mozgtk.c b/widget/gtk/mozgtk/mozgtk.c new file mode 100644 index 0000000000..0b2e3fd494 --- /dev/null +++ b/widget/gtk/mozgtk/mozgtk.c @@ -0,0 +1,676 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Types.h" +#include "mozilla/Assertions.h" + +#define STUB(symbol) \ + MOZ_EXPORT void symbol(void) { MOZ_CRASH(); } + +#ifdef COMMON_SYMBOLS +STUB(gdk_atom_intern) +STUB(gdk_atom_name) +STUB(gdk_beep) +STUB(gdk_cairo_create) +STUB(gdk_color_free) +STUB(gdk_color_parse) +STUB(gdk_cursor_new_for_display) +STUB(gdk_cursor_new_from_name) +STUB(gdk_cursor_new_from_pixbuf) +STUB(gdk_display_close) +STUB(gdk_display_get_default) +STUB(gdk_display_get_default_screen) +STUB(gdk_display_get_pointer) +STUB(gdk_display_get_window_at_pointer) +STUB(gdk_display_manager_get) +STUB(gdk_display_manager_set_default_display) +STUB(gdk_display_open) +STUB(gdk_display_sync) +STUB(gdk_display_warp_pointer) +STUB(gdk_drag_context_get_actions) +STUB(gdk_drag_context_get_dest_window) +STUB(gdk_drag_context_get_source_window) +STUB(gdk_drag_context_list_targets) +STUB(gdk_drag_status) +STUB(gdk_error_trap_pop) +STUB(gdk_error_trap_push) +STUB(gdk_event_copy) +STUB(gdk_event_free) +STUB(gdk_event_get_axis) +STUB(gdk_event_get_time) +STUB(gdk_event_handler_set) +STUB(gdk_event_peek) +STUB(gdk_event_put) +STUB(gdk_flush) +STUB(gdk_get_default_root_window) +STUB(gdk_get_display) +STUB(gdk_get_display_arg_name) +STUB(gdk_get_program_class) +STUB(gdk_keymap_get_default) +STUB(gdk_keymap_get_direction) +STUB(gdk_keymap_get_entries_for_keyval) +STUB(gdk_keymap_get_for_display) +STUB(gdk_keymap_have_bidi_layouts) +STUB(gdk_keymap_translate_keyboard_state) +STUB(gdk_keyval_name) +STUB(gdk_keyval_to_unicode) +STUB(gdk_pango_context_get) +STUB(gdk_pointer_grab) +STUB(gdk_pointer_ungrab) +STUB(gdk_property_change) +STUB(gdk_property_get) +STUB(gdk_property_delete) +STUB(gdk_screen_get_default) +STUB(gdk_screen_get_display) +STUB(gdk_screen_get_font_options) +STUB(gdk_screen_get_height) +STUB(gdk_screen_get_height_mm) +STUB(gdk_screen_get_n_monitors) +STUB(gdk_screen_get_monitor_at_window) +STUB(gdk_screen_get_monitor_geometry) +STUB(gdk_screen_get_monitor_height_mm) +STUB(gdk_screen_get_number) +STUB(gdk_screen_get_resolution) +STUB(gdk_screen_get_rgba_visual) +STUB(gdk_screen_get_root_window) +STUB(gdk_screen_get_system_visual) +STUB(gdk_screen_get_width) +STUB(gdk_screen_height) +STUB(gdk_screen_is_composited) +STUB(gdk_screen_width) +STUB(gdk_selection_owner_get) +STUB(gdk_set_program_class) +STUB(gdk_unicode_to_keyval) +STUB(gdk_visual_get_depth) +STUB(gdk_visual_get_system) +STUB(gdk_window_add_filter) +STUB(gdk_window_begin_move_drag) +STUB(gdk_window_begin_resize_drag) +STUB(gdk_window_destroy) +STUB(gdk_window_focus) +STUB(gdk_window_get_children) +STUB(gdk_window_get_display) +STUB(gdk_window_get_events) +STUB(gdk_window_get_geometry) +STUB(gdk_window_get_height) +STUB(gdk_window_get_origin) +STUB(gdk_window_get_parent) +STUB(gdk_window_get_position) +STUB(gdk_window_get_root_origin) +STUB(gdk_window_get_screen) +STUB(gtk_window_get_size) +STUB(gdk_window_get_state) +STUB(gdk_window_get_toplevel) +STUB(gdk_window_get_update_area) +STUB(gdk_window_get_user_data) +STUB(gdk_window_get_visual) +STUB(gdk_window_get_width) +STUB(gdk_window_get_window_type) +STUB(gdk_window_hide) +STUB(gdk_window_input_shape_combine_region) +STUB(gdk_window_invalidate_rect) +STUB(gdk_window_invalidate_region) +STUB(gdk_window_is_destroyed) +STUB(gdk_window_is_visible) +STUB(gdk_window_lower) +STUB(gdk_window_move) +STUB(gdk_window_move_resize) +STUB(gdk_window_new) +STUB(gdk_window_peek_children) +STUB(gdk_window_process_updates) +STUB(gdk_window_raise) +STUB(gdk_window_remove_filter) +STUB(gdk_window_reparent) +STUB(gdk_window_resize) +STUB(gdk_window_set_cursor) +STUB(gdk_window_set_debug_updates) +STUB(gdk_window_set_decorations) +STUB(gdk_window_set_events) +STUB(gdk_window_set_role) +STUB(gdk_window_set_urgency_hint) +STUB(gdk_window_set_user_data) +STUB(gdk_window_shape_combine_region) +STUB(gdk_window_show) +STUB(gdk_window_show_unraised) +STUB(gdk_x11_atom_to_xatom) +STUB(gdk_x11_display_get_user_time) +STUB(gdk_x11_display_get_xdisplay) +STUB(gdk_x11_get_default_root_xwindow) +STUB(gdk_x11_get_default_xdisplay) +STUB(gdk_x11_get_server_time) +STUB(gdk_x11_get_xatom_by_name) +STUB(gdk_x11_get_xatom_by_name_for_display) +STUB(gdk_x11_lookup_xdisplay) +STUB(gdk_x11_screen_get_xscreen) +STUB(gdk_x11_screen_get_screen_number) +STUB(gdk_x11_screen_lookup_visual) +STUB(gdk_x11_screen_supports_net_wm_hint) +STUB(gdk_x11_visual_get_xvisual) +STUB(gdk_x11_window_foreign_new_for_display) +STUB(gdk_x11_window_lookup_for_display) +STUB(gdk_x11_window_set_user_time) +STUB(gdk_x11_xatom_to_atom) +STUB(gdk_x11_set_sm_client_id) +STUB(gtk_accel_label_new) +STUB(gtk_alignment_get_type) +STUB(gtk_alignment_new) +STUB(gtk_alignment_set_padding) +STUB(gtk_arrow_get_type) +STUB(gtk_arrow_new) +STUB(gtk_bindings_activate) +STUB(gtk_bin_get_child) +STUB(gtk_bin_get_type) +STUB(gtk_border_free) +STUB(gtk_box_get_type) +STUB(gtk_box_pack_start) +STUB(gtk_button_new) +STUB(gtk_button_new_with_label) +STUB(gtk_check_button_new_with_label) +STUB(gtk_check_button_new_with_mnemonic) +STUB(gtk_check_menu_item_new) +STUB(gtk_check_version) +STUB(gtk_clipboard_clear) +STUB(gtk_clipboard_get) +STUB(gtk_clipboard_request_contents) +STUB(gtk_clipboard_request_text) +STUB(gtk_clipboard_set_can_store) +STUB(gtk_clipboard_set_with_data) +STUB(gtk_clipboard_store) +STUB(gtk_color_selection_dialog_get_color_selection) +STUB(gtk_color_selection_dialog_get_type) +STUB(gtk_color_selection_dialog_new) +STUB(gtk_color_selection_get_current_color) +STUB(gtk_color_selection_get_type) +STUB(gtk_color_selection_set_current_color) +STUB(gtk_combo_box_get_active) +STUB(gtk_combo_box_get_type) +STUB(gtk_combo_box_new) +STUB(gtk_combo_box_new_with_entry) +STUB(gtk_combo_box_set_active) +STUB(gtk_combo_box_text_get_type) +STUB(gtk_combo_box_text_new) +STUB(gtk_container_add) +STUB(gtk_container_forall) +STUB(gtk_container_get_border_width) +STUB(gtk_container_get_type) +STUB(gtk_container_set_border_width) +STUB(gtk_container_set_resize_mode) +STUB(gtk_dialog_get_content_area) +STUB(gtk_dialog_get_type) +STUB(gtk_dialog_new_with_buttons) +STUB(gtk_dialog_run) +STUB(gtk_dialog_set_alternative_button_order) +STUB(gtk_dialog_set_default_response) +STUB(gtk_drag_begin) +STUB(gtk_drag_dest_set) +STUB(gtk_drag_finish) +STUB(gtk_drag_get_data) +STUB(gtk_drag_get_source_widget) +STUB(gtk_drag_set_icon_pixbuf) +STUB(gtk_drag_set_icon_widget) +STUB(gtk_editable_get_type) +STUB(gtk_editable_select_region) +STUB(gtk_entry_get_text) +STUB(gtk_entry_get_type) +STUB(gtk_entry_new) +STUB(gtk_entry_set_activates_default) +STUB(gtk_entry_set_text) +STUB(gtk_enumerate_printers) +STUB(gtk_expander_new) +STUB(gtk_file_chooser_add_filter) +STUB(gtk_file_chooser_dialog_new) +STUB(gtk_file_chooser_get_filenames) +STUB(gtk_file_chooser_get_filter) +STUB(gtk_file_chooser_get_preview_filename) +STUB(gtk_file_chooser_get_type) +STUB(gtk_file_chooser_get_uri) +STUB(gtk_file_chooser_list_filters) +STUB(gtk_file_chooser_set_current_folder) +STUB(gtk_file_chooser_set_current_name) +STUB(gtk_file_chooser_set_do_overwrite_confirmation) +STUB(gtk_file_chooser_set_filename) +STUB(gtk_file_chooser_set_filter) +STUB(gtk_file_chooser_set_local_only) +STUB(gtk_file_chooser_set_preview_widget) +STUB(gtk_file_chooser_set_preview_widget_active) +STUB(gtk_file_chooser_set_select_multiple) +STUB(gtk_file_chooser_widget_get_type) +STUB(gtk_file_filter_add_pattern) +STUB(gtk_file_filter_new) +STUB(gtk_file_filter_set_name) +STUB(gtk_fixed_new) +STUB(gtk_frame_new) +STUB(gtk_get_current_event_time) +STUB(gtk_grab_add) +STUB(gtk_grab_remove) +STUB(gtk_handle_box_new) +STUB(gtk_hbox_new) +STUB(gtk_icon_info_free) +STUB(gtk_icon_info_load_icon) +STUB(gtk_icon_set_add_source) +STUB(gtk_icon_set_new) +STUB(gtk_icon_set_render_icon) +STUB(gtk_icon_set_unref) +STUB(gtk_icon_size_lookup) +STUB(gtk_icon_source_free) +STUB(gtk_icon_source_new) +STUB(gtk_icon_source_set_icon_name) +STUB(gtk_icon_theme_add_builtin_icon) +STUB(gtk_icon_theme_get_default) +STUB(gtk_icon_theme_get_icon_sizes) +STUB(gtk_icon_theme_lookup_by_gicon) +STUB(gtk_icon_theme_lookup_icon) +STUB(gtk_image_get_icon_name) +STUB(gtk_image_get_type) +STUB(gtk_image_new) +STUB(gtk_image_new_from_icon_name) +STUB(gtk_image_new_from_stock) +STUB(gtk_image_set_from_pixbuf) +STUB(gtk_im_context_filter_keypress) +STUB(gtk_im_context_focus_in) +STUB(gtk_im_context_focus_out) +STUB(gtk_im_context_get_preedit_string) +STUB(gtk_im_context_reset) +STUB(gtk_im_context_set_client_window) +STUB(gtk_im_context_set_cursor_location) +STUB(gtk_im_context_set_surrounding) +STUB(gtk_im_context_simple_new) +STUB(gtk_im_multicontext_get_context_id) +STUB(gtk_im_multicontext_get_type) +STUB(gtk_im_multicontext_new) +STUB(gtk_info_bar_get_type) +STUB(gtk_info_bar_get_content_area) +STUB(gtk_info_bar_new) +STUB(gtk_init) +STUB(gtk_invisible_new) +STUB(gtk_key_snooper_install) +STUB(gtk_key_snooper_remove) +STUB(gtk_label_get_type) +STUB(gtk_label_new) +STUB(gtk_label_set_markup) +STUB(gtk_link_button_new) +STUB(gtk_main_do_event) +STUB(gtk_main_iteration) +STUB(gtk_menu_attach_to_widget) +STUB(gtk_menu_bar_new) +STUB(gtk_menu_get_type) +STUB(gtk_menu_item_get_type) +STUB(gtk_menu_item_new) +STUB(gtk_menu_item_set_submenu) +STUB(gtk_menu_new) +STUB(gtk_menu_shell_append) +STUB(gtk_menu_shell_get_type) +STUB(gtk_misc_get_alignment) +STUB(gtk_misc_get_padding) +STUB(gtk_misc_get_type) +STUB(gtk_misc_set_alignment) +STUB(gtk_misc_set_padding) +STUB(gtk_notebook_new) +STUB(gtk_page_setup_copy) +STUB(gtk_page_setup_get_bottom_margin) +STUB(gtk_page_setup_get_left_margin) +STUB(gtk_page_setup_get_orientation) +STUB(gtk_page_setup_get_paper_size) +STUB(gtk_page_setup_get_right_margin) +STUB(gtk_page_setup_get_top_margin) +STUB(gtk_page_setup_new) +STUB(gtk_page_setup_set_bottom_margin) +STUB(gtk_page_setup_set_left_margin) +STUB(gtk_page_setup_set_orientation) +STUB(gtk_page_setup_set_paper_size) +STUB(gtk_page_setup_set_paper_size_and_default_margins) +STUB(gtk_page_setup_set_right_margin) +STUB(gtk_page_setup_set_top_margin) +STUB(gtk_page_setup_to_key_file) +STUB(gtk_paper_size_free) +STUB(gtk_paper_size_get_display_name) +STUB(gtk_paper_size_get_height) +STUB(gtk_paper_size_get_name) +STUB(gtk_paper_size_get_width) +STUB(gtk_paper_size_is_custom) +STUB(gtk_paper_size_is_equal) +STUB(gtk_paper_size_new) +STUB(gtk_paper_size_new_custom) +STUB(gtk_paper_size_set_size) +STUB(gtk_parse_args) +STUB(gtk_plug_get_socket_window) +STUB(gtk_plug_get_type) +STUB(gtk_printer_accepts_pdf) +STUB(gtk_printer_get_name) +STUB(gtk_printer_get_type) +STUB(gtk_printer_is_default) +STUB(gtk_print_job_new) +STUB(gtk_print_job_send) +STUB(gtk_print_job_set_source_file) +STUB(gtk_print_run_page_setup_dialog) +STUB(gtk_print_settings_copy) +STUB(gtk_print_settings_foreach) +STUB(gtk_print_settings_get) +STUB(gtk_print_settings_get_duplex) +STUB(gtk_print_settings_get_n_copies) +STUB(gtk_print_settings_get_page_ranges) +STUB(gtk_print_settings_get_paper_size) +STUB(gtk_print_settings_get_printer) +STUB(gtk_print_settings_get_print_pages) +STUB(gtk_print_settings_get_resolution) +STUB(gtk_print_settings_get_reverse) +STUB(gtk_print_settings_get_scale) +STUB(gtk_print_settings_get_use_color) +STUB(gtk_print_settings_has_key) +STUB(gtk_print_settings_new) +STUB(gtk_print_settings_set) +STUB(gtk_print_settings_set_duplex) +STUB(gtk_print_settings_set_n_copies) +STUB(gtk_print_settings_set_orientation) +STUB(gtk_print_settings_set_page_ranges) +STUB(gtk_print_settings_set_paper_size) +STUB(gtk_print_settings_set_printer) +STUB(gtk_print_settings_set_print_pages) +STUB(gtk_print_settings_set_resolution) +STUB(gtk_print_settings_set_reverse) +STUB(gtk_print_settings_set_scale) +STUB(gtk_print_settings_set_use_color) +STUB(gtk_print_unix_dialog_add_custom_tab) +STUB(gtk_print_unix_dialog_get_page_setup) +STUB(gtk_print_unix_dialog_get_selected_printer) +STUB(gtk_print_unix_dialog_get_settings) +STUB(gtk_print_unix_dialog_get_type) +STUB(gtk_print_unix_dialog_new) +STUB(gtk_print_unix_dialog_set_manual_capabilities) +STUB(gtk_print_unix_dialog_set_page_setup) +STUB(gtk_print_unix_dialog_set_settings) +STUB(gtk_progress_bar_new) +STUB(gtk_propagate_event) +STUB(gtk_radio_button_get_type) +STUB(gtk_radio_button_new_with_label) +STUB(gtk_radio_button_new_with_mnemonic) +STUB(gtk_radio_button_new_with_mnemonic_from_widget) +STUB(gtk_range_get_min_slider_size) +STUB(gtk_range_get_type) +STUB(gtk_recent_manager_add_item) +STUB(gtk_recent_manager_get_default) +STUB(gtk_scrollbar_get_type) +STUB(gtk_scrolled_window_new) +STUB(gtk_selection_data_copy) +STUB(gtk_selection_data_free) +STUB(gtk_selection_data_get_data) +STUB(gtk_selection_data_get_length) +STUB(gtk_selection_data_get_selection) +STUB(gtk_selection_data_get_target) +STUB(gtk_selection_data_get_targets) +STUB(gtk_selection_data_set) +STUB(gtk_selection_data_set_pixbuf) +STUB(gtk_selection_data_set_text) +STUB(gtk_selection_data_targets_include_text) +STUB(gtk_separator_get_type) +STUB(gtk_separator_menu_item_new) +STUB(gtk_separator_tool_item_new) +STUB(gtk_settings_get_default) +STUB(gtk_settings_get_for_screen) +STUB(gtk_show_uri) +STUB(gtk_socket_add_id) +STUB(gtk_socket_get_id) +STUB(gtk_socket_get_type) +STUB(gtk_socket_get_plug_window) +STUB(gtk_socket_new) +STUB(gtk_spin_button_new) +STUB(gtk_statusbar_new) +STUB(gtk_style_lookup_icon_set) +STUB(gtk_table_attach) +STUB(gtk_table_get_type) +STUB(gtk_table_new) +STUB(gtk_target_list_add) +STUB(gtk_target_list_add_image_targets) +STUB(gtk_target_list_add_text_targets) +STUB(gtk_target_list_new) +STUB(gtk_target_list_unref) +STUB(gtk_targets_include_image) +STUB(gtk_targets_include_text) +STUB(gtk_target_table_free) +STUB(gtk_target_table_new_from_list) +STUB(gtk_text_view_new) +STUB(gtk_toggle_button_get_active) +STUB(gtk_toggle_button_get_type) +STUB(gtk_toggle_button_new) +STUB(gtk_toggle_button_set_active) +STUB(gtk_toggle_button_set_inconsistent) +STUB(gtk_toolbar_new) +STUB(gtk_tooltip_get_type) +STUB(gtk_tree_view_append_column) +STUB(gtk_tree_view_column_new) +STUB(gtk_tree_view_column_set_title) +STUB(gtk_tree_view_get_type) +STUB(gtk_tree_view_new) +STUB(gtk_vbox_new) +STUB(gtk_widget_add_events) +STUB(gtk_widget_class_find_style_property) +STUB(gtk_widget_destroy) +STUB(gtk_widget_destroyed) +STUB(gtk_widget_ensure_style) +STUB(gtk_widget_event) +STUB(gtk_widget_get_accessible) +STUB(gtk_widget_get_allocation) +STUB(gtk_widget_get_default_direction) +STUB(gtk_widget_get_display) +STUB(gtk_widget_get_events) +STUB(gtk_widget_get_has_window) +STUB(gtk_widget_get_mapped) +STUB(gtk_widget_get_parent) +STUB(gtk_widget_get_parent_window) +STUB(gtk_widget_get_realized) +STUB(gtk_widget_get_screen) +STUB(gtk_widget_get_style) +STUB(gtk_widget_get_toplevel) +STUB(gtk_widget_get_type) +STUB(gtk_widget_get_visible) +STUB(gtk_widget_get_visual) +STUB(gtk_widget_get_window) +STUB(gtk_widget_grab_focus) +STUB(gtk_widget_has_focus) +STUB(gtk_widget_has_grab) +STUB(gtk_widget_hide) +STUB(gtk_widget_is_focus) +STUB(gtk_widget_is_toplevel) +STUB(gtk_widget_map) +STUB(gtk_widget_modify_bg) +STUB(gtk_widget_realize) +STUB(gtk_widget_reparent) +STUB(gtk_widget_set_allocation) +STUB(gtk_widget_set_app_paintable) +STUB(gtk_window_set_auto_startup_notification) +STUB(gtk_window_set_keep_above) +STUB(gtk_window_set_opacity) +STUB(gtk_window_set_screen) +STUB(gtk_widget_set_can_focus) +STUB(gtk_widget_set_direction) +STUB(gtk_widget_set_double_buffered) +STUB(gtk_widget_set_has_window) +STUB(gtk_widget_set_mapped) +STUB(gtk_widget_set_name) +STUB(gtk_widget_set_parent) +STUB(gtk_widget_set_parent_window) +STUB(gtk_widget_set_realized) +STUB(gtk_widget_set_redraw_on_allocate) +STUB(gtk_widget_set_sensitive) +STUB(gtk_widget_set_window) +STUB(gtk_widget_show) +STUB(gtk_widget_show_all) +STUB(gtk_widget_size_allocate) +STUB(gtk_widget_style_get) +STUB(gtk_widget_unparent) +STUB(gtk_widget_unrealize) +STUB(gtk_window_deiconify) +STUB(gtk_window_fullscreen) +STUB(gtk_window_get_group) +STUB(gtk_window_get_modal) +STUB(gtk_window_get_transient_for) +STUB(gtk_window_get_type) +STUB(gtk_window_get_type_hint) +STUB(gtk_window_get_window_type) +STUB(gtk_window_group_add_window) +STUB(gtk_window_group_get_current_grab) +STUB(gtk_window_group_new) +STUB(gtk_window_iconify) +STUB(gtk_window_is_active) +STUB(gtk_window_maximize) +STUB(gtk_window_move) +STUB(gtk_window_new) +STUB(gtk_window_present_with_time) +STUB(gtk_window_resize) +STUB(gtk_window_set_accept_focus) +STUB(gtk_window_set_decorated) +STUB(gtk_window_set_deletable) +STUB(gtk_window_set_destroy_with_parent) +STUB(gtk_window_set_focus_on_map) +STUB(gtk_window_set_geometry_hints) +STUB(gtk_window_set_icon_name) +STUB(gtk_window_set_modal) +STUB(gtk_window_set_skip_taskbar_hint) +STUB(gtk_window_set_startup_id) +STUB(gtk_window_set_title) +STUB(gtk_window_set_transient_for) +STUB(gtk_window_set_type_hint) +STUB(gtk_window_set_wmclass) +STUB(gtk_window_unfullscreen) +STUB(gtk_window_unmaximize) +#endif + +#ifdef GTK3_SYMBOLS +STUB(gtk_css_provider_load_from_data) +STUB(gtk_css_provider_new) +STUB(gdk_device_get_source) +STUB(gdk_device_manager_get_client_pointer) +STUB(gdk_disable_multidevice) +STUB(gdk_device_manager_list_devices) +STUB(gdk_display_get_device_manager) +STUB(gdk_display_manager_open_display) +STUB(gdk_error_trap_pop_ignored) +STUB(gdk_event_get_source_device) +STUB(gdk_screen_get_monitor_workarea) +STUB(gdk_window_get_type) +STUB(gdk_window_set_opaque_region) +STUB(gdk_x11_window_get_xid) +STUB(gdk_x11_display_get_type) +STUB(gdk_wayland_display_get_type) +STUB(gdk_wayland_display_get_wl_compositor) +STUB(gdk_wayland_display_get_wl_display) +STUB(gdk_wayland_window_get_wl_surface) +STUB(gtk_box_new) +STUB(gtk_cairo_should_draw_window) +STUB(gtk_cairo_transform_to_window) +STUB(gtk_combo_box_text_append) +STUB(gtk_drag_set_icon_surface) +STUB(gtk_get_major_version) +STUB(gtk_get_micro_version) +STUB(gtk_get_minor_version) +STUB(gtk_icon_info_load_symbolic_for_context) +STUB(gtk_menu_button_new) +STUB(gtk_offscreen_window_new) +STUB(gtk_paned_new) +STUB(gtk_radio_menu_item_new) +STUB(gtk_render_activity) +STUB(gtk_render_arrow) +STUB(gtk_render_background) +STUB(gtk_render_check) +STUB(gtk_render_expander) +STUB(gtk_render_extension) +STUB(gtk_render_focus) +STUB(gtk_render_frame) +STUB(gtk_render_frame_gap) +STUB(gtk_render_handle) +STUB(gtk_render_icon) +STUB(gtk_render_line) +STUB(gtk_render_option) +STUB(gtk_render_slider) +STUB(gtk_scale_new) +STUB(gtk_scrollbar_new) +STUB(gtk_style_context_add_class) +STUB(gtk_style_context_add_provider) +STUB(gtk_style_context_add_region) +STUB(gtk_style_context_get) +STUB(gtk_style_context_get_background_color) +STUB(gtk_style_context_get_border) +STUB(gtk_style_context_get_border_color) +STUB(gtk_style_context_get_color) +STUB(gtk_style_context_get_direction) +STUB(gtk_style_context_get_margin) +STUB(gtk_style_context_get_padding) +STUB(gtk_style_context_get_path) +STUB(gtk_style_context_get_property) +STUB(gtk_style_context_get_state) +STUB(gtk_style_context_get_style) +STUB(gtk_style_context_has_class) +STUB(gtk_style_context_invalidate) +STUB(gtk_style_context_list_classes) +STUB(gtk_style_context_new) +STUB(gtk_style_context_remove_class) +STUB(gtk_style_context_remove_region) +STUB(gtk_style_context_restore) +STUB(gtk_style_context_save) +STUB(gtk_style_context_set_direction) +STUB(gtk_style_context_set_path) +STUB(gtk_style_context_set_parent) +STUB(gtk_style_context_set_state) +STUB(gtk_style_properties_lookup_property) +STUB(gtk_style_provider_get_type) +STUB(gtk_tree_view_column_get_button) +STUB(gtk_widget_get_preferred_size) +STUB(gtk_widget_get_preferred_width) +STUB(gtk_widget_get_preferred_height) +STUB(gtk_widget_get_state_flags) +STUB(gtk_widget_get_style_context) +STUB(gtk_widget_path_append_type) +STUB(gtk_widget_path_copy) +STUB(gtk_widget_path_free) +STUB(gtk_widget_path_iter_add_class) +STUB(gtk_widget_path_get_object_type) +STUB(gtk_widget_path_length) +STUB(gtk_widget_path_new) +STUB(gtk_widget_path_unref) +STUB(gtk_widget_set_valign) +STUB(gtk_widget_set_visual) +STUB(gtk_window_set_titlebar) +STUB(gtk_app_chooser_dialog_new_for_content_type) +STUB(gtk_app_chooser_get_type) +STUB(gtk_app_chooser_get_app_info) +STUB(gtk_app_chooser_dialog_get_type) +STUB(gtk_app_chooser_dialog_set_heading) +STUB(gtk_color_chooser_dialog_new) +STUB(gtk_color_chooser_dialog_get_type) +STUB(gtk_color_chooser_get_type) +STUB(gtk_color_chooser_set_rgba) +STUB(gtk_color_chooser_get_rgba) +STUB(gtk_color_chooser_set_use_alpha) +#endif + +#ifdef GTK2_SYMBOLS +STUB(gdk_drawable_get_screen) +STUB(gdk_rgb_get_colormap) +STUB(gdk_rgb_get_visual) +STUB(gdk_window_lookup) +STUB(gdk_window_set_back_pixmap) +STUB(gdk_x11_colormap_foreign_new) +STUB(gdk_x11_colormap_get_xcolormap) +STUB(gdk_x11_drawable_get_xdisplay) +STUB(gdk_x11_drawable_get_xid) +STUB(gdk_x11_window_get_drawable_impl) +STUB(gdkx_visual_get) +STUB(gtk_object_get_type) +#endif + +#ifndef GTK3_SYMBOLS +// Only define the following workaround when using GTK3, which we detect +// by checking if GTK3 stubs are not provided. +# include <X11/Xlib.h> +// Bug 1271100 +// We need to trick system Cairo into not using the XShm extension due to +// a race condition in it that results in frequent BadAccess errors. Cairo +// relies upon XShmQueryExtension to initially detect if XShm is available. +// So we define our own stub that always indicates XShm not being present. +// mozgtk loads before libXext/libcairo and so this stub will take priority. +// Our tree usage goes through xcb and remains unaffected by this. +MOZ_EXPORT Bool XShmQueryExtension(Display* aDisplay) { return False; } +#endif diff --git a/widget/gtk/mozgtk/stub/moz.build b/widget/gtk/mozgtk/stub/moz.build new file mode 100644 index 0000000000..8af0cc1cdf --- /dev/null +++ b/widget/gtk/mozgtk/stub/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "../mozgtk.c", +] + +for var in ("COMMON_SYMBOLS", "GTK2_SYMBOLS", "GTK3_SYMBOLS"): + DEFINES[var] = True + +SharedLibrary("mozgtk_stub") + +SONAME = "mozgtk" diff --git a/widget/gtk/mozwayland/moz.build b/widget/gtk/mozwayland/moz.build new file mode 100644 index 0000000000..94eca8a785 --- /dev/null +++ b/widget/gtk/mozwayland/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "mozwayland.c", +] +EXPORTS.mozilla.widget += [ + "mozwayland.h", +] + +SharedLibrary("mozwayland") + +CFLAGS += CONFIG["TK_CFLAGS"] diff --git a/widget/gtk/mozwayland/mozwayland.c b/widget/gtk/mozwayland/mozwayland.c new file mode 100644 index 0000000000..8a79d87865 --- /dev/null +++ b/widget/gtk/mozwayland/mozwayland.c @@ -0,0 +1,201 @@ +/* -*- 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 <stdlib.h> +#include "mozilla/Types.h" +#include <gtk/gtk.h> +#include <gtk/gtkx.h> +#include <gdk/gdkwayland.h> + +union wl_argument; + +/* Those strucures are just placeholders and will be replaced by + * real symbols from libwayland during run-time linking. We need to make + * them explicitly visible. + */ +#pragma GCC visibility push(default) +const struct wl_interface wl_buffer_interface; +const struct wl_interface wl_callback_interface; +const struct wl_interface wl_data_device_interface; +const struct wl_interface wl_data_device_manager_interface; +const struct wl_interface wl_keyboard_interface; +const struct wl_interface wl_region_interface; +const struct wl_interface wl_registry_interface; +const struct wl_interface wl_shm_interface; +const struct wl_interface wl_shm_pool_interface; +const struct wl_interface wl_seat_interface; +const struct wl_interface wl_surface_interface; +const struct wl_interface wl_subsurface_interface; +const struct wl_interface wl_compositor_interface; +const struct wl_interface wl_subcompositor_interface; +const struct wl_interface wl_output_interface; +#pragma GCC visibility pop + +MOZ_EXPORT void wl_event_queue_destroy(struct wl_event_queue* queue) {} + +MOZ_EXPORT void wl_proxy_marshal(struct wl_proxy* p, uint32_t opcode, ...) {} + +MOZ_EXPORT void wl_proxy_marshal_array(struct wl_proxy* p, uint32_t opcode, + union wl_argument* args) {} + +MOZ_EXPORT struct wl_proxy* wl_proxy_create( + struct wl_proxy* factory, const struct wl_interface* interface) { + return NULL; +} + +MOZ_EXPORT void* wl_proxy_create_wrapper(void* proxy) { return NULL; } + +MOZ_EXPORT void wl_proxy_wrapper_destroy(void* proxy_wrapper) {} + +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor( + struct wl_proxy* proxy, uint32_t opcode, + const struct wl_interface* interface, ...) { + return NULL; +} + +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor_versioned( + struct wl_proxy* proxy, uint32_t opcode, + const struct wl_interface* interface, uint32_t version, ...) { + return NULL; +} + +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_array_constructor( + struct wl_proxy* proxy, uint32_t opcode, union wl_argument* args, + const struct wl_interface* interface) { + return NULL; +} + +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_array_constructor_versioned( + struct wl_proxy* proxy, uint32_t opcode, union wl_argument* args, + const struct wl_interface* interface, uint32_t version) { + return NULL; +} + +MOZ_EXPORT void wl_proxy_destroy(struct wl_proxy* proxy) {} + +MOZ_EXPORT int wl_proxy_add_listener(struct wl_proxy* proxy, + void (**implementation)(void), + void* data) { + return -1; +} + +MOZ_EXPORT const void* wl_proxy_get_listener(struct wl_proxy* proxy) { + return NULL; +} + +typedef int (*wl_dispatcher_func_t)(const void*, void*, uint32_t, + const struct wl_message*, + union wl_argument*); + +MOZ_EXPORT int wl_proxy_add_dispatcher(struct wl_proxy* proxy, + wl_dispatcher_func_t dispatcher_func, + const void* dispatcher_data, + void* data) { + return -1; +} + +MOZ_EXPORT void wl_proxy_set_user_data(struct wl_proxy* proxy, + void* user_data) {} + +MOZ_EXPORT void* wl_proxy_get_user_data(struct wl_proxy* proxy) { return NULL; } + +MOZ_EXPORT uint32_t wl_proxy_get_version(struct wl_proxy* proxy) { return -1; } + +MOZ_EXPORT uint32_t wl_proxy_get_id(struct wl_proxy* proxy) { return -1; } + +MOZ_EXPORT const char* wl_proxy_get_class(struct wl_proxy* proxy) { + return NULL; +} + +MOZ_EXPORT void wl_proxy_set_queue(struct wl_proxy* proxy, + struct wl_event_queue* queue) {} + +MOZ_EXPORT struct wl_display* wl_display_connect(const char* name) { + return NULL; +} + +MOZ_EXPORT struct wl_display* wl_display_connect_to_fd(int fd) { return NULL; } + +MOZ_EXPORT void wl_display_disconnect(struct wl_display* display) {} + +MOZ_EXPORT int wl_display_get_fd(struct wl_display* display) { return -1; } + +MOZ_EXPORT int wl_display_dispatch(struct wl_display* display) { return -1; } + +MOZ_EXPORT int wl_display_dispatch_queue(struct wl_display* display, + struct wl_event_queue* queue) { + return -1; +} + +MOZ_EXPORT int wl_display_dispatch_queue_pending(struct wl_display* display, + struct wl_event_queue* queue) { + return -1; +} + +MOZ_EXPORT int wl_display_dispatch_pending(struct wl_display* display) { + return -1; +} + +MOZ_EXPORT int wl_display_get_error(struct wl_display* display) { return -1; } + +MOZ_EXPORT uint32_t wl_display_get_protocol_error( + struct wl_display* display, const struct wl_interface** interface, + uint32_t* id) { + return -1; +} + +MOZ_EXPORT int wl_display_flush(struct wl_display* display) { return -1; } + +MOZ_EXPORT int wl_display_roundtrip_queue(struct wl_display* display, + struct wl_event_queue* queue) { + return -1; +} + +MOZ_EXPORT int wl_display_roundtrip(struct wl_display* display) { return -1; } + +MOZ_EXPORT struct wl_event_queue* wl_display_create_queue( + struct wl_display* display) { + return NULL; +} + +MOZ_EXPORT int wl_display_prepare_read_queue(struct wl_display* display, + struct wl_event_queue* queue) { + return -1; +} + +MOZ_EXPORT int wl_display_prepare_read(struct wl_display* display) { + return -1; +} + +MOZ_EXPORT void wl_display_cancel_read(struct wl_display* display) {} + +MOZ_EXPORT int wl_display_read_events(struct wl_display* display) { return -1; } + +MOZ_EXPORT void wl_log_set_handler_client(wl_log_func_t handler) {} + +MOZ_EXPORT struct wl_egl_window* wl_egl_window_create( + struct wl_surface* surface, int width, int height) { + return NULL; +} + +MOZ_EXPORT void wl_egl_window_destroy(struct wl_egl_window* egl_window) {} + +MOZ_EXPORT void wl_egl_window_resize(struct wl_egl_window* egl_window, + int width, int height, int dx, int dy) {} + +MOZ_EXPORT void wl_list_init(struct wl_list* list) {} + +MOZ_EXPORT void wl_list_insert(struct wl_list* list, struct wl_list* elm) {} + +MOZ_EXPORT void wl_list_remove(struct wl_list* elm) {} + +MOZ_EXPORT int wl_list_length(const struct wl_list* list) { return -1; } + +MOZ_EXPORT int wl_list_empty(const struct wl_list* list) { return -1; } + +MOZ_EXPORT void wl_list_insert_list(struct wl_list* list, + struct wl_list* other) {} diff --git a/widget/gtk/mozwayland/mozwayland.h b/widget/gtk/mozwayland/mozwayland.h new file mode 100644 index 0000000000..22f80d6315 --- /dev/null +++ b/widget/gtk/mozwayland/mozwayland.h @@ -0,0 +1,134 @@ +/* -*- 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/. */ + +/* Wayland compatibility header, it makes Firefox build with + wayland-1.2 and Gtk+ 3.10. +*/ + +#ifndef __MozWayland_h_ +#define __MozWayland_h_ + +#include "mozilla/Types.h" +#include <gtk/gtk.h> +#include <gdk/gdkwayland.h> + +#ifdef __cplusplus +extern "C" { +#endif + +MOZ_EXPORT struct wl_display* wl_display_connect(const char* name); +MOZ_EXPORT int wl_display_roundtrip_queue(struct wl_display* display, + struct wl_event_queue* queue); +MOZ_EXPORT uint32_t wl_proxy_get_version(struct wl_proxy* proxy); +MOZ_EXPORT void wl_proxy_marshal(struct wl_proxy* p, uint32_t opcode, ...); +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor( + struct wl_proxy* proxy, uint32_t opcode, + const struct wl_interface* interface, ...); +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor_versioned( + struct wl_proxy* proxy, uint32_t opcode, + const struct wl_interface* interface, uint32_t version, ...); +MOZ_EXPORT void wl_proxy_destroy(struct wl_proxy* proxy); +MOZ_EXPORT void* wl_proxy_create_wrapper(void* proxy); +MOZ_EXPORT void wl_proxy_wrapper_destroy(void* proxy_wrapper); + +/* We need implement some missing functions from wayland-client-protocol.h + */ +#ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM +enum wl_data_device_manager_dnd_action { + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1, + WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2, + WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK = 4 +}; +#endif + +#ifndef WL_DATA_OFFER_SET_ACTIONS +# define WL_DATA_OFFER_SET_ACTIONS 4 + +struct moz_wl_data_offer_listener { + void (*offer)(void* data, struct wl_data_offer* wl_data_offer, + const char* mime_type); + void (*source_actions)(void* data, struct wl_data_offer* wl_data_offer, + uint32_t source_actions); + void (*action)(void* data, struct wl_data_offer* wl_data_offer, + uint32_t dnd_action); +}; + +static inline void wl_data_offer_set_actions( + struct wl_data_offer* wl_data_offer, uint32_t dnd_actions, + uint32_t preferred_action) { + wl_proxy_marshal((struct wl_proxy*)wl_data_offer, WL_DATA_OFFER_SET_ACTIONS, + dnd_actions, preferred_action); +} +#else +typedef struct wl_data_offer_listener moz_wl_data_offer_listener; +#endif + +#ifndef WL_SUBCOMPOSITOR_GET_SUBSURFACE +# define WL_SUBCOMPOSITOR_GET_SUBSURFACE 1 +struct wl_subcompositor; + +// Emulate what mozilla header wrapper does - make the +// wl_subcompositor_interface always visible. +# pragma GCC visibility push(default) +extern const struct wl_interface wl_subsurface_interface; +extern const struct wl_interface wl_subcompositor_interface; +# pragma GCC visibility pop + +# define WL_SUBSURFACE_DESTROY 0 +# define WL_SUBSURFACE_SET_POSITION 1 +# define WL_SUBSURFACE_PLACE_ABOVE 2 +# define WL_SUBSURFACE_PLACE_BELOW 3 +# define WL_SUBSURFACE_SET_SYNC 4 +# define WL_SUBSURFACE_SET_DESYNC 5 + +static inline struct wl_subsurface* wl_subcompositor_get_subsurface( + struct wl_subcompositor* wl_subcompositor, struct wl_surface* surface, + struct wl_surface* parent) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor( + (struct wl_proxy*)wl_subcompositor, WL_SUBCOMPOSITOR_GET_SUBSURFACE, + &wl_subsurface_interface, NULL, surface, parent); + + return (struct wl_subsurface*)id; +} + +static inline void wl_subsurface_set_position( + struct wl_subsurface* wl_subsurface, int32_t x, int32_t y) { + wl_proxy_marshal((struct wl_proxy*)wl_subsurface, WL_SUBSURFACE_SET_POSITION, + x, y); +} + +static inline void wl_subsurface_set_desync( + struct wl_subsurface* wl_subsurface) { + wl_proxy_marshal((struct wl_proxy*)wl_subsurface, WL_SUBSURFACE_SET_DESYNC); +} + +static inline void wl_subsurface_destroy(struct wl_subsurface* wl_subsurface) { + wl_proxy_marshal((struct wl_proxy*)wl_subsurface, WL_SUBSURFACE_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)wl_subsurface); +} +#endif + +#ifndef WL_SURFACE_DAMAGE_BUFFER +# define WL_SURFACE_DAMAGE_BUFFER 9 + +static inline void wl_surface_damage_buffer(struct wl_surface* wl_surface, + int32_t x, int32_t y, int32_t width, + int32_t height) { + wl_proxy_marshal((struct wl_proxy*)wl_surface, WL_SURFACE_DAMAGE_BUFFER, x, y, + width, height); +} +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __MozWayland_h_ */ diff --git a/widget/gtk/nsAppShell.cpp b/widget/gtk/nsAppShell.cpp new file mode 100644 index 0000000000..24bdd50833 --- /dev/null +++ b/widget/gtk/nsAppShell.cpp @@ -0,0 +1,253 @@ +/* -*- 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 <sys/types.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <gdk/gdk.h> +#include "nsAppShell.h" +#include "nsWindow.h" +#include "mozilla/Logging.h" +#include "prenv.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/Hal.h" +#include "mozilla/Unused.h" +#include "mozilla/WidgetUtils.h" +#include "GeckoProfiler.h" +#include "nsIPowerManagerService.h" +#ifdef MOZ_ENABLE_DBUS +# include "WakeLockListener.h" +#endif +#include "gfxPlatform.h" +#include "ScreenHelperGTK.h" +#include "HeadlessScreenHelper.h" +#include "mozilla/widget/ScreenManager.h" +#ifdef MOZ_WAYLAND +# include "nsWaylandDisplay.h" +#endif + +using mozilla::LazyLogModule; +using mozilla::Unused; +using mozilla::widget::HeadlessScreenHelper; +using mozilla::widget::ScreenHelperGTK; +using mozilla::widget::ScreenManager; + +#define NOTIFY_TOKEN 0xFA + +LazyLogModule gWidgetLog("Widget"); +LazyLogModule gWidgetFocusLog("WidgetFocus"); +LazyLogModule gWidgetDragLog("WidgetDrag"); +LazyLogModule gWidgetDrawLog("WidgetDraw"); +LazyLogModule gWidgetWaylandLog("WidgetWayland"); +LazyLogModule gDmabufLog("Dmabuf"); +LazyLogModule gClipboardLog("WidgetClipboard"); + +static GPollFunc sPollFunc; + +// Wrapper function to disable hang monitoring while waiting in poll(). +static gint PollWrapper(GPollFD* ufds, guint nfsd, gint timeout_) { + mozilla::BackgroundHangMonitor().NotifyWait(); + gint result; + { + AUTO_PROFILER_LABEL("PollWrapper", IDLE); + AUTO_PROFILER_THREAD_SLEEP; + result = (*sPollFunc)(ufds, nfsd, timeout_); + } + mozilla::BackgroundHangMonitor().NotifyActivity(); + return result; +} + +// Emit resume-events on GdkFrameClock if flush-events has not been +// balanced by resume-events at dispose. +// For https://bugzilla.gnome.org/show_bug.cgi?id=742636 +static decltype(GObjectClass::constructed) sRealGdkFrameClockConstructed; +static decltype(GObjectClass::dispose) sRealGdkFrameClockDispose; +static GQuark sPendingResumeQuark; + +static void OnFlushEvents(GObject* clock, gpointer) { + g_object_set_qdata(clock, sPendingResumeQuark, GUINT_TO_POINTER(1)); +} + +static void OnResumeEvents(GObject* clock, gpointer) { + g_object_set_qdata(clock, sPendingResumeQuark, nullptr); +} + +static void WrapGdkFrameClockConstructed(GObject* object) { + sRealGdkFrameClockConstructed(object); + + g_signal_connect(object, "flush-events", G_CALLBACK(OnFlushEvents), nullptr); + g_signal_connect(object, "resume-events", G_CALLBACK(OnResumeEvents), + nullptr); +} + +static void WrapGdkFrameClockDispose(GObject* object) { + if (g_object_get_qdata(object, sPendingResumeQuark)) { + g_signal_emit_by_name(object, "resume-events"); + } + + sRealGdkFrameClockDispose(object); +} + +/*static*/ +gboolean nsAppShell::EventProcessorCallback(GIOChannel* source, + GIOCondition condition, + gpointer data) { + nsAppShell* self = static_cast<nsAppShell*>(data); + + unsigned char c; + Unused << read(self->mPipeFDs[0], &c, 1); + NS_ASSERTION(c == (unsigned char)NOTIFY_TOKEN, "wrong token"); + + self->NativeEventCallback(); + return TRUE; +} + +nsAppShell::~nsAppShell() { + mozilla::hal::Shutdown(); + + if (mTag) g_source_remove(mTag); + if (mPipeFDs[0]) close(mPipeFDs[0]); + if (mPipeFDs[1]) close(mPipeFDs[1]); +} + +nsresult nsAppShell::Init() { + mozilla::hal::Init(); + +#ifdef MOZ_ENABLE_DBUS + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIPowerManagerService> powerManagerService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + + if (powerManagerService) { + powerManagerService->AddWakeLockListener( + WakeLockListener::GetSingleton()); + } else { + NS_WARNING( + "Failed to retrieve PowerManagerService, wakelocks will be broken!"); + } + } +#endif + + if (!sPollFunc) { + sPollFunc = g_main_context_get_poll_func(nullptr); + g_main_context_set_poll_func(nullptr, &PollWrapper); + } + + if (XRE_IsParentProcess()) { + ScreenManager& screenManager = ScreenManager::GetSingleton(); + if (gfxPlatform::IsHeadless()) { + screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>()); + } else { + screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperGTK>()); + } + + if (gtk_check_version(3, 16, 3) == nullptr) { + // Before 3.16.3, GDK cannot override classname by --class command line + // option when program uses gdk_set_program_class(). + // + // See https://bugzilla.gnome.org/show_bug.cgi?id=747634 + // + // Only bother doing this for the parent process, since it's the one + // creating top-level windows. (At this point, a child process hasn't + // received the list of registered chrome packages, so the + // GetBrandShortName call would fail anyway.) + nsAutoString brandName; + mozilla::widget::WidgetUtils::GetBrandShortName(brandName); + if (!brandName.IsEmpty()) { + gdk_set_program_class(NS_ConvertUTF16toUTF8(brandName).get()); + } + } + } + + if (!sPendingResumeQuark && + gtk_check_version(3, 14, 7) != nullptr) { // GTK 3.0 to GTK 3.14.7. + // GTK 3.8 - 3.14 registered this type when creating the frame clock + // for the root window of the display when the display was opened. + GType gdkFrameClockIdleType = g_type_from_name("GdkFrameClockIdle"); + if (gdkFrameClockIdleType) { // not in versions prior to 3.8 + sPendingResumeQuark = g_quark_from_string("moz-resume-is-pending"); + auto gdk_frame_clock_idle_class = + G_OBJECT_CLASS(g_type_class_peek_static(gdkFrameClockIdleType)); + auto constructed = &gdk_frame_clock_idle_class->constructed; + sRealGdkFrameClockConstructed = *constructed; + *constructed = WrapGdkFrameClockConstructed; + auto dispose = &gdk_frame_clock_idle_class->dispose; + sRealGdkFrameClockDispose = *dispose; + *dispose = WrapGdkFrameClockDispose; + } + } + + // Workaround for bug 1209659 which is fixed by Gtk3.20 + if (gtk_check_version(3, 20, 0) != nullptr) { + unsetenv("GTK_CSD"); + } + + if (PR_GetEnv("MOZ_DEBUG_PAINTS")) { + gdk_window_set_debug_updates(TRUE); + } + + // Whitelist of only common, stable formats - see bugs 1197059 and 1203078 + GSList* pixbufFormats = gdk_pixbuf_get_formats(); + for (GSList* iter = pixbufFormats; iter; iter = iter->next) { + GdkPixbufFormat* format = static_cast<GdkPixbufFormat*>(iter->data); + gchar* name = gdk_pixbuf_format_get_name(format); + if (strcmp(name, "jpeg") && strcmp(name, "png") && strcmp(name, "gif") && + strcmp(name, "bmp") && strcmp(name, "ico") && strcmp(name, "xpm") && + strcmp(name, "svg")) { + gdk_pixbuf_format_set_disabled(format, TRUE); + } + g_free(name); + } + g_slist_free(pixbufFormats); + + int err = pipe(mPipeFDs); + if (err) return NS_ERROR_OUT_OF_MEMORY; + + GIOChannel* ioc; + GSource* source; + + // make the pipe nonblocking + + int flags = fcntl(mPipeFDs[0], F_GETFL, 0); + if (flags == -1) goto failed; + err = fcntl(mPipeFDs[0], F_SETFL, flags | O_NONBLOCK); + if (err == -1) goto failed; + flags = fcntl(mPipeFDs[1], F_GETFL, 0); + if (flags == -1) goto failed; + err = fcntl(mPipeFDs[1], F_SETFL, flags | O_NONBLOCK); + if (err == -1) goto failed; + + ioc = g_io_channel_unix_new(mPipeFDs[0]); + source = g_io_create_watch(ioc, G_IO_IN); + g_io_channel_unref(ioc); + g_source_set_callback(source, (GSourceFunc)EventProcessorCallback, this, + nullptr); + g_source_set_can_recurse(source, TRUE); + mTag = g_source_attach(source, nullptr); + g_source_unref(source); + + return nsBaseAppShell::Init(); +failed: + close(mPipeFDs[0]); + close(mPipeFDs[1]); + mPipeFDs[0] = mPipeFDs[1] = 0; + return NS_ERROR_FAILURE; +} + +void nsAppShell::ScheduleNativeEventCallback() { + unsigned char buf[] = {NOTIFY_TOKEN}; + Unused << write(mPipeFDs[1], buf, 1); +} + +bool nsAppShell::ProcessNextNativeEvent(bool mayWait) { + bool ret = g_main_context_iteration(nullptr, mayWait); +#ifdef MOZ_WAYLAND + mozilla::widget::WaylandDispatchDisplays(); +#endif + return ret; +} diff --git a/widget/gtk/nsAppShell.h b/widget/gtk/nsAppShell.h new file mode 100644 index 0000000000..06543ee955 --- /dev/null +++ b/widget/gtk/nsAppShell.h @@ -0,0 +1,34 @@ +/* -*- 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 nsAppShell_h__ +#define nsAppShell_h__ + +#include <glib.h> +#include "nsBaseAppShell.h" +#include "nsCOMPtr.h" + +class nsAppShell : public nsBaseAppShell { + public: + nsAppShell() : mTag(0) { mPipeFDs[0] = mPipeFDs[1] = 0; } + + // nsBaseAppShell overrides: + nsresult Init(); + virtual void ScheduleNativeEventCallback() override; + virtual bool ProcessNextNativeEvent(bool mayWait) override; + + private: + virtual ~nsAppShell(); + + static gboolean EventProcessorCallback(GIOChannel* source, + GIOCondition condition, gpointer data); + + int mPipeFDs[2]; + unsigned mTag; +}; + +#endif /* nsAppShell_h__ */ diff --git a/widget/gtk/nsApplicationChooser.cpp b/widget/gtk/nsApplicationChooser.cpp new file mode 100644 index 0000000000..2ebb62fd1d --- /dev/null +++ b/widget/gtk/nsApplicationChooser.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Types.h" + +#include <gtk/gtk.h> + +#include "nsApplicationChooser.h" +#include "WidgetUtils.h" +#include "nsIMIMEInfo.h" +#include "nsIWidget.h" +#include "nsCExternalHandlerService.h" +#include "nsComponentManagerUtils.h" +#include "nsGtkUtils.h" +#include "nsPIDOMWindow.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsApplicationChooser, nsIApplicationChooser) + +nsApplicationChooser::nsApplicationChooser() = default; + +nsApplicationChooser::~nsApplicationChooser() = default; + +NS_IMETHODIMP +nsApplicationChooser::Init(mozIDOMWindowProxy* aParent, + const nsACString& aTitle) { + NS_ENSURE_TRUE(aParent, NS_ERROR_FAILURE); + auto* parent = nsPIDOMWindowOuter::From(aParent); + mParentWidget = widget::WidgetUtils::DOMWindowToWidget(parent); + mWindowTitle.Assign(aTitle); + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationChooser::Open(const nsACString& aContentType, + nsIApplicationChooserFinishedCallback* aCallback) { + MOZ_ASSERT(aCallback); + if (mCallback) { + NS_WARNING("Chooser is already in progress."); + return NS_ERROR_ALREADY_INITIALIZED; + } + mCallback = aCallback; + NS_ENSURE_TRUE(mParentWidget, NS_ERROR_FAILURE); + GtkWindow* parent_widget = + GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET)); + + GtkWidget* chooser = gtk_app_chooser_dialog_new_for_content_type( + parent_widget, + (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), + PromiseFlatCString(aContentType).get()); + gtk_app_chooser_dialog_set_heading(GTK_APP_CHOOSER_DIALOG(chooser), + mWindowTitle.BeginReading()); + NS_ADDREF_THIS(); + g_signal_connect(chooser, "response", G_CALLBACK(OnResponse), this); + g_signal_connect(chooser, "destroy", G_CALLBACK(OnDestroy), this); + gtk_widget_show(chooser); + return NS_OK; +} + +/* static */ +void nsApplicationChooser::OnResponse(GtkWidget* chooser, gint response_id, + gpointer user_data) { + static_cast<nsApplicationChooser*>(user_data)->Done(chooser, response_id); +} + +/* static */ +void nsApplicationChooser::OnDestroy(GtkWidget* chooser, gpointer user_data) { + static_cast<nsApplicationChooser*>(user_data)->Done(chooser, + GTK_RESPONSE_CANCEL); +} + +void nsApplicationChooser::Done(GtkWidget* chooser, gint response) { + nsCOMPtr<nsILocalHandlerApp> localHandler; + nsresult rv; + switch (response) { + case GTK_RESPONSE_OK: + case GTK_RESPONSE_ACCEPT: { + localHandler = do_CreateInstance(NS_LOCALHANDLERAPP_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("Out of memory."); + break; + } + GAppInfo* app_info = + gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(chooser)); + + nsCOMPtr<nsIFile> localExecutable; + gchar* fileWithFullPath = + g_find_program_in_path(g_app_info_get_executable(app_info)); + if (!fileWithFullPath) { + g_object_unref(app_info); + NS_WARNING("Cannot find program in path."); + break; + } + rv = NS_NewNativeLocalFile(nsDependentCString(fileWithFullPath), false, + getter_AddRefs(localExecutable)); + g_free(fileWithFullPath); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot create local filename."); + localHandler = nullptr; + } else { + localHandler->SetExecutable(localExecutable); + localHandler->SetName( + NS_ConvertUTF8toUTF16(g_app_info_get_display_name(app_info))); + } + g_object_unref(app_info); + } + + break; + case GTK_RESPONSE_CANCEL: + case GTK_RESPONSE_CLOSE: + case GTK_RESPONSE_DELETE_EVENT: + break; + default: + NS_WARNING("Unexpected response"); + break; + } + + // A "response" signal won't be sent again but "destroy" will be. + g_signal_handlers_disconnect_by_func(chooser, FuncToGpointer(OnDestroy), + this); + gtk_widget_destroy(chooser); + + if (mCallback) { + mCallback->Done(localHandler); + mCallback = nullptr; + } + NS_RELEASE_THIS(); +} diff --git a/widget/gtk/nsApplicationChooser.h b/widget/gtk/nsApplicationChooser.h new file mode 100644 index 0000000000..22f9a808c0 --- /dev/null +++ b/widget/gtk/nsApplicationChooser.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 nsApplicationChooser_h__ +#define nsApplicationChooser_h__ + +#include <gtk/gtk.h> +#include "nsCOMPtr.h" +#include "nsIApplicationChooser.h" +#include "nsString.h" + +class nsIWidget; + +class nsApplicationChooser final : public nsIApplicationChooser { + public: + nsApplicationChooser(); + NS_DECL_ISUPPORTS + NS_DECL_NSIAPPLICATIONCHOOSER + void Done(GtkWidget* chooser, gint response); + + private: + ~nsApplicationChooser(); + nsCOMPtr<nsIWidget> mParentWidget; + nsCString mWindowTitle; + nsCOMPtr<nsIApplicationChooserFinishedCallback> mCallback; + static void OnResponse(GtkWidget* chooser, gint response_id, + gpointer user_data); + static void OnDestroy(GtkWidget* chooser, gpointer user_data); +}; +#endif diff --git a/widget/gtk/nsBidiKeyboard.cpp b/widget/gtk/nsBidiKeyboard.cpp new file mode 100644 index 0000000000..a57235195b --- /dev/null +++ b/widget/gtk/nsBidiKeyboard.cpp @@ -0,0 +1,52 @@ +/* -*- 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 "prlink.h" + +#include "nsBidiKeyboard.h" +#include "nsIWidget.h" +#include <gtk/gtk.h> + +NS_IMPL_ISUPPORTS(nsBidiKeyboard, nsIBidiKeyboard) + +nsBidiKeyboard::nsBidiKeyboard() { Reset(); } + +NS_IMETHODIMP +nsBidiKeyboard::Reset() { + // NB: The default keymap can be null (e.g. in xpcshell). In that case, + // simply assume that we don't have bidi keyboards. + mHaveBidiKeyboards = false; + + GdkDisplay* display = gdk_display_get_default(); + if (!display) return NS_OK; + + GdkKeymap* keymap = gdk_keymap_get_for_display(display); + mHaveBidiKeyboards = keymap && gdk_keymap_have_bidi_layouts(keymap); + return NS_OK; +} + +nsBidiKeyboard::~nsBidiKeyboard() = default; + +NS_IMETHODIMP +nsBidiKeyboard::IsLangRTL(bool* aIsRTL) { + if (!mHaveBidiKeyboards) return NS_ERROR_FAILURE; + + *aIsRTL = (gdk_keymap_get_direction(gdk_keymap_get_default()) == + PANGO_DIRECTION_RTL); + + return NS_OK; +} + +NS_IMETHODIMP nsBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) { + // not implemented yet + return NS_ERROR_NOT_IMPLEMENTED; +} + +// static +already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() { + return do_AddRef(new nsBidiKeyboard()); +} diff --git a/widget/gtk/nsBidiKeyboard.h b/widget/gtk/nsBidiKeyboard.h new file mode 100644 index 0000000000..1bc1c7e0a2 --- /dev/null +++ b/widget/gtk/nsBidiKeyboard.h @@ -0,0 +1,25 @@ +/* -*- 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 __nsBidiKeyboard +#define __nsBidiKeyboard +#include "nsIBidiKeyboard.h" + +class nsBidiKeyboard : public nsIBidiKeyboard { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIBIDIKEYBOARD + + nsBidiKeyboard(); + + protected: + virtual ~nsBidiKeyboard(); + + bool mHaveBidiKeyboards; +}; + +#endif // __nsBidiKeyboard diff --git a/widget/gtk/nsClipboard.cpp b/widget/gtk/nsClipboard.cpp new file mode 100644 index 0000000000..42807845fb --- /dev/null +++ b/widget/gtk/nsClipboard.cpp @@ -0,0 +1,822 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "nsArrayUtils.h" +#include "nsClipboard.h" +#include "nsClipboardX11.h" +#if defined(MOZ_WAYLAND) +# include "nsClipboardWayland.h" +#endif +#include "nsContentUtils.h" +#include "HeadlessClipboard.h" +#include "nsSupportsPrimitives.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsPrimitiveHelpers.h" +#include "nsImageToPixbuf.h" +#include "nsStringStream.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/TimeStamp.h" +#include "gfxPlatformGtk.h" + +#include "imgIContainer.h" + +#include <gtk/gtk.h> +#include <gtk/gtkx.h> + +#include "mozilla/Encoding.h" + +using namespace mozilla; + +// Idle timeout for receiving selection and property notify events (microsec) +const int kClipboardTimeout = 500000; + +// We add this prefix to HTML markup, so that GetHTMLCharset can correctly +// detect the HTML as UTF-8 encoded. +static const char kHTMLMarkupPrefix[] = + R"(<meta http-equiv="content-type" content="text/html; charset=utf-8">)"; + +// Callback when someone asks us for the data +void clipboard_get_cb(GtkClipboard* aGtkClipboard, + GtkSelectionData* aSelectionData, guint info, + gpointer user_data); + +// Callback when someone asks us to clear a clipboard +void clipboard_clear_cb(GtkClipboard* aGtkClipboard, gpointer user_data); + +static bool ConvertHTMLtoUCS2(const char* data, int32_t dataLength, + nsCString& charset, char16_t** unicodeData, + int32_t& outUnicodeLen); + +static bool GetHTMLCharset(const char* data, int32_t dataLength, + nsCString& str); + +GdkAtom GetSelectionAtom(int32_t aWhichClipboard) { + if (aWhichClipboard == nsIClipboard::kGlobalClipboard) + return GDK_SELECTION_CLIPBOARD; + + return GDK_SELECTION_PRIMARY; +} + +int GetGeckoClipboardType(GtkClipboard* aGtkClipboard) { + if (aGtkClipboard == gtk_clipboard_get(GDK_SELECTION_PRIMARY)) + return nsClipboard::kSelectionClipboard; + else if (aGtkClipboard == gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)) + return nsClipboard::kGlobalClipboard; + else + return -1; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF +} + +nsClipboard::nsClipboard() = default; + +nsClipboard::~nsClipboard() { + // We have to clear clipboard before gdk_display_close() call. + // See bug 531580 for details. + if (mGlobalTransferable) { + gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)); + } + if (mSelectionTransferable) { + gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_PRIMARY)); + } +} + +NS_IMPL_ISUPPORTS(nsClipboard, nsIClipboard, nsIObserver) + +nsresult nsClipboard::Init(void) { + if (gfxPlatformGtk::GetPlatform()->IsX11Display()) { + mContext = MakeUnique<nsRetrievalContextX11>(); +#if defined(MOZ_WAYLAND) + } else { + mContext = MakeUnique<nsRetrievalContextWayland>(); +#endif + } + NS_ASSERTION(mContext, "Missing nsRetrievalContext for nsClipboard!"); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->AddObserver(this, "xpcom-shutdown", false); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsClipboard::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // Save global clipboard content to CLIPBOARD_MANAGER. + // gtk_clipboard_store() can run an event loop, so call from a dedicated + // runnable. + return SchedulerGroup::Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction("gtk_clipboard_store()", []() { + gtk_clipboard_store(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)); + })); +} + +NS_IMETHODIMP +nsClipboard::SetData(nsITransferable* aTransferable, nsIClipboardOwner* aOwner, + int32_t aWhichClipboard) { + // See if we can short cut + if ((aWhichClipboard == kGlobalClipboard && + aTransferable == mGlobalTransferable.get() && + aOwner == mGlobalOwner.get()) || + (aWhichClipboard == kSelectionClipboard && + aTransferable == mSelectionTransferable.get() && + aOwner == mSelectionOwner.get())) { + return NS_OK; + } + + LOGCLIP(("nsClipboard::SetData (%s)\n", + aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard")); + + // List of suported targets + GtkTargetList* list = gtk_target_list_new(nullptr, 0); + + // Get the types of supported flavors + nsTArray<nsCString> flavors; + nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors); + if (NS_FAILED(rv)) { + LOGCLIP((" FlavorsTransferableCanExport failed!\n")); + // Fall through. |gtkTargets| will be null below. + } + + // Add all the flavors to this widget's supported type. + bool imagesAdded = false; + for (uint32_t i = 0; i < flavors.Length(); i++) { + nsCString& flavorStr = flavors[i]; + + // Special case text/unicode since we can handle all of the string types. + if (flavorStr.EqualsLiteral(kUnicodeMime)) { + LOGCLIP((" text targets\n")); + gtk_target_list_add_text_targets(list, 0); + continue; + } + + if (nsContentUtils::IsFlavorImage(flavorStr)) { + // Don't bother adding image targets twice + if (!imagesAdded) { + // accept any writable image type + LOGCLIP((" image targets\n")); + gtk_target_list_add_image_targets(list, 0, TRUE); + imagesAdded = true; + } + continue; + } + + // Add this to our list of valid targets + GdkAtom atom = gdk_atom_intern(flavorStr.get(), FALSE); + gtk_target_list_add(list, atom, 0, 0); + } + + // Get GTK clipboard (CLIPBOARD or PRIMARY) + GtkClipboard* gtkClipboard = + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); + + gint numTargets; + GtkTargetEntry* gtkTargets = + gtk_target_table_new_from_list(list, &numTargets); + + LOGCLIP((" gtk_target_table_new_from_list() = %p\n", (void*)gtkTargets)); + + // Set getcallback and request to store data after an application exit + if (gtkTargets && + gtk_clipboard_set_with_data(gtkClipboard, gtkTargets, numTargets, + clipboard_get_cb, clipboard_clear_cb, this)) { + LOGCLIP((" gtk_clipboard_set_with_data() is ok\n")); + // We managed to set-up the clipboard so update internal state + // We have to set it now because gtk_clipboard_set_with_data() calls + // clipboard_clear_cb() which reset our internal state + if (aWhichClipboard == kSelectionClipboard) { + mSelectionOwner = aOwner; + mSelectionTransferable = aTransferable; + } else { + mGlobalOwner = aOwner; + mGlobalTransferable = aTransferable; + gtk_clipboard_set_can_store(gtkClipboard, gtkTargets, numTargets); + } + + rv = NS_OK; + } else { + LOGCLIP((" gtk_clipboard_set_with_data() failed!\n")); + // Clear references to the any old data and let GTK know that it is no + // longer available. + EmptyClipboard(aWhichClipboard); + rv = NS_ERROR_FAILURE; + } + + gtk_target_table_free(gtkTargets, numTargets); + gtk_target_list_unref(list); + + return rv; +} + +void nsClipboard::SetTransferableData(nsITransferable* aTransferable, + nsCString& aFlavor, + const char* aClipboardData, + uint32_t aClipboardDataLength) { + LOGCLIP(("nsClipboard::SetTransferableData MIME %s\n", aFlavor.get())); + + nsCOMPtr<nsISupports> wrapper; + nsPrimitiveHelpers::CreatePrimitiveForData( + aFlavor, aClipboardData, aClipboardDataLength, getter_AddRefs(wrapper)); + aTransferable->SetTransferData(aFlavor.get(), wrapper); +} + +NS_IMETHODIMP +nsClipboard::GetData(nsITransferable* aTransferable, int32_t aWhichClipboard) { + if (!aTransferable) return NS_ERROR_FAILURE; + + LOGCLIP(("nsClipboard::GetData (%s)\n", + aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard")); + + // Get a list of flavors this transferable can import + nsTArray<nsCString> flavors; + nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); + if (NS_FAILED(rv)) { + LOGCLIP((" FlavorsTransferableCanImport falied!\n")); + return rv; + } + +#ifdef MOZ_LOGGING + LOGCLIP(("Flavors which can be imported:\n")); + for (uint32_t i = 0; i < flavors.Length(); i++) { + LOGCLIP((" %s\n", flavors[i].get())); + } +#endif + + for (uint32_t i = 0; i < flavors.Length(); i++) { + nsCString& flavorStr = flavors[i]; + + if (flavorStr.EqualsLiteral(kJPEGImageMime) || + flavorStr.EqualsLiteral(kJPGImageMime) || + flavorStr.EqualsLiteral(kPNGImageMime) || + flavorStr.EqualsLiteral(kGIFImageMime)) { + // Emulate support for image/jpg + if (flavorStr.EqualsLiteral(kJPGImageMime)) { + flavorStr.Assign(kJPEGImageMime); + } + + LOGCLIP((" Getting image %s MIME clipboard data\n", flavorStr.get())); + + uint32_t clipboardDataLength; + const char* clipboardData = mContext->GetClipboardData( + flavorStr.get(), aWhichClipboard, &clipboardDataLength); + if (!clipboardData) { + LOGCLIP((" %s type is missing\n", flavorStr.get())); + continue; + } + + nsCOMPtr<nsIInputStream> byteStream; + NS_NewByteInputStream(getter_AddRefs(byteStream), + Span(clipboardData, clipboardDataLength), + NS_ASSIGNMENT_COPY); + aTransferable->SetTransferData(flavorStr.get(), byteStream); + LOGCLIP((" got %s MIME data\n", flavorStr.get())); + + mContext->ReleaseClipboardData(clipboardData); + return NS_OK; + } + + // Special case text/unicode since we can convert any + // string into text/unicode + if (flavorStr.EqualsLiteral(kUnicodeMime)) { + LOGCLIP( + (" Getting unicode %s MIME clipboard data\n", flavorStr.get())); + + const char* clipboardData = mContext->GetClipboardText(aWhichClipboard); + if (!clipboardData) { + LOGCLIP((" failed to get unicode data\n")); + // If the type was text/unicode and we couldn't get + // text off the clipboard, run the next loop + // iteration. + continue; + } + + // Convert utf-8 into our unicode format. + NS_ConvertUTF8toUTF16 ucs2string(clipboardData); + const char* unicodeData = (const char*)ToNewUnicode(ucs2string); + uint32_t unicodeDataLength = ucs2string.Length() * 2; + SetTransferableData(aTransferable, flavorStr, unicodeData, + unicodeDataLength); + free((void*)unicodeData); + + LOGCLIP((" got unicode data, length %d\n", ucs2string.Length())); + + mContext->ReleaseClipboardData(clipboardData); + return NS_OK; + } + + LOGCLIP((" Getting %s MIME clipboard data\n", flavorStr.get())); + + uint32_t clipboardDataLength; + const char* clipboardData = mContext->GetClipboardData( + flavorStr.get(), aWhichClipboard, &clipboardDataLength); + +#ifdef MOZ_LOGGING + if (!clipboardData) { + LOGCLIP((" %s type is missing\n", flavorStr.get())); + } +#endif + + if (clipboardData) { + LOGCLIP((" got %s mime type data.\n", flavorStr.get())); + + // Special case text/html since we can convert into UCS2 + if (flavorStr.EqualsLiteral(kHTMLMime)) { + char16_t* htmlBody = nullptr; + int32_t htmlBodyLen = 0; + // Convert text/html into our unicode format + nsAutoCString charset; + if (!GetHTMLCharset(clipboardData, clipboardDataLength, charset)) { + // Fall back to utf-8 in case html/data is missing kHTMLMarkupPrefix. + LOGCLIP(("Failed to get html/text encoding, fall back to utf-8.\n")); + charset.AssignLiteral("utf-8"); + } + if (!ConvertHTMLtoUCS2(clipboardData, clipboardDataLength, charset, + &htmlBody, htmlBodyLen)) { + LOGCLIP((" failed to convert text/html to UCS2.\n")); + mContext->ReleaseClipboardData(clipboardData); + continue; + } + + SetTransferableData(aTransferable, flavorStr, (const char*)htmlBody, + htmlBodyLen * 2); + free(htmlBody); + } else { + SetTransferableData(aTransferable, flavorStr, clipboardData, + clipboardDataLength); + } + + mContext->ReleaseClipboardData(clipboardData); + return NS_OK; + } + } + + LOGCLIP((" failed to get clipboard content.\n")); + return NS_OK; +} + +NS_IMETHODIMP +nsClipboard::EmptyClipboard(int32_t aWhichClipboard) { + LOGCLIP(("nsClipboard::EmptyClipboard (%s)\n", + aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard")); + if (aWhichClipboard == kSelectionClipboard) { + if (mSelectionTransferable) { + gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_PRIMARY)); + MOZ_ASSERT(!mSelectionTransferable); + } + } else { + if (mGlobalTransferable) { + gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)); + MOZ_ASSERT(!mGlobalTransferable); + } + } + + return NS_OK; +} + +void nsClipboard::ClearTransferable(int32_t aWhichClipboard) { + if (aWhichClipboard == kSelectionClipboard) { + if (mSelectionOwner) { + mSelectionOwner->LosingOwnership(mSelectionTransferable); + mSelectionOwner = nullptr; + } + mSelectionTransferable = nullptr; + } else { + if (mGlobalOwner) { + mGlobalOwner->LosingOwnership(mGlobalTransferable); + mGlobalOwner = nullptr; + } + mGlobalTransferable = nullptr; + } +} + +NS_IMETHODIMP +nsClipboard::HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList, + int32_t aWhichClipboard, bool* _retval) { + if (!_retval) { + return NS_ERROR_NULL_POINTER; + } + + LOGCLIP(("nsClipboard::HasDataMatchingFlavors (%s)\n", + aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard")); + + *_retval = false; + + int targetNums; + GdkAtom* targets = mContext->GetTargets(aWhichClipboard, &targetNums); + if (!targets) { + LOGCLIP((" no targes at clipboard (null)\n")); + return NS_OK; + } + + // Walk through the provided types and try to match it to a + // provided type. + for (auto& flavor : aFlavorList) { + // We special case text/unicode here. + if (flavor.EqualsLiteral(kUnicodeMime) && + gtk_targets_include_text(targets, targetNums)) { + *_retval = true; + LOGCLIP((" has kUnicodeMime\n")); + break; + } + + for (int32_t j = 0; j < targetNums; j++) { + gchar* atom_name = gdk_atom_name(targets[j]); + if (!atom_name) continue; + + if (flavor.Equals(atom_name)) { + *_retval = true; + LOGCLIP((" has %s\n", atom_name)); + } + // X clipboard supports image/jpeg, but we want to emulate support + // for image/jpg as well + else if (flavor.EqualsLiteral(kJPGImageMime) && + !strcmp(atom_name, kJPEGImageMime)) { + *_retval = true; + LOGCLIP((" has image/jpg\n")); + } + + g_free(atom_name); + + if (*_retval) break; + } + } + +#ifdef MOZ_LOGGING + if (!(*_retval)) { + LOGCLIP((" no targes at clipboard (bad match)\n")); + } +#endif + + g_free(targets); + return NS_OK; +} + +NS_IMETHODIMP +nsClipboard::SupportsSelectionClipboard(bool* _retval) { + *_retval = mContext->HasSelectionSupport(); + return NS_OK; +} + +NS_IMETHODIMP +nsClipboard::SupportsFindClipboard(bool* _retval) { + *_retval = false; + return NS_OK; +} + +nsITransferable* nsClipboard::GetTransferable(int32_t aWhichClipboard) { + nsITransferable* retval; + + if (aWhichClipboard == kSelectionClipboard) + retval = mSelectionTransferable.get(); + else + retval = mGlobalTransferable.get(); + + return retval; +} + +void nsClipboard::SelectionGetEvent(GtkClipboard* aClipboard, + GtkSelectionData* aSelectionData) { + // Someone has asked us to hand them something. The first thing + // that we want to do is see if that something includes text. If + // it does, try to give it text/unicode after converting it to + // utf-8. + + int32_t whichClipboard; + + // which clipboard? + GdkAtom selection = gtk_selection_data_get_selection(aSelectionData); + if (selection == GDK_SELECTION_PRIMARY) + whichClipboard = kSelectionClipboard; + else if (selection == GDK_SELECTION_CLIPBOARD) + whichClipboard = kGlobalClipboard; + else + return; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF + + LOGCLIP(("nsClipboard::SelectionGetEvent (%s)\n", + whichClipboard == kSelectionClipboard ? "primary" : "clipboard")); + + nsCOMPtr<nsITransferable> trans = GetTransferable(whichClipboard); + if (!trans) { + // We have nothing to serve + LOGCLIP(("nsClipboard::SelectionGetEvent() - %s clipboard is empty!\n", + whichClipboard == kSelectionClipboard ? "Primary" : "Clipboard")); + return; + } + + nsresult rv; + nsCOMPtr<nsISupports> item; + + GdkAtom selectionTarget = gtk_selection_data_get_target(aSelectionData); + LOGCLIP((" selection target %s\n", gdk_atom_name(selectionTarget))); + + // Check to see if the selection data is some text type. + if (gtk_targets_include_text(&selectionTarget, 1)) { + LOGCLIP((" providing text/unicode data\n")); + // Try to convert our internal type into a text string. Get + // the transferable for this clipboard and try to get the + // text/unicode type for it. + rv = trans->GetTransferData("text/unicode", getter_AddRefs(item)); + if (NS_FAILED(rv) || !item) { + LOGCLIP((" GetTransferData() failed to get text/unicode!\n")); + return; + } + + nsCOMPtr<nsISupportsString> wideString; + wideString = do_QueryInterface(item); + if (!wideString) return; + + nsAutoString ucs2string; + wideString->GetData(ucs2string); + NS_ConvertUTF16toUTF8 utf8string(ucs2string); + + LOGCLIP((" sent %d bytes of utf-8 data\n", utf8string.Length())); + if (selectionTarget == gdk_atom_intern("text/plain;charset=utf-8", FALSE)) { + LOGCLIP( + (" using gtk_selection_data_set for 'text/plain;charset=utf-8'\n")); + // Bypass gtk_selection_data_set_text, which will convert \n to \r\n + // in some versions of GTK. + gtk_selection_data_set(aSelectionData, selectionTarget, 8, + reinterpret_cast<const guchar*>(utf8string.get()), + utf8string.Length()); + } else { + gtk_selection_data_set_text(aSelectionData, utf8string.get(), + utf8string.Length()); + } + return; + } + + // Check to see if the selection data is an image type + if (gtk_targets_include_image(&selectionTarget, 1, TRUE)) { + LOGCLIP((" providing image data\n")); + // Look through our transfer data for the image + static const char* const imageMimeTypes[] = {kNativeImageMime, + kPNGImageMime, kJPEGImageMime, + kJPGImageMime, kGIFImageMime}; + nsCOMPtr<nsISupports> imageItem; + nsCOMPtr<imgIContainer> image; + for (uint32_t i = 0; i < ArrayLength(imageMimeTypes); i++) { + rv = trans->GetTransferData(imageMimeTypes[i], getter_AddRefs(imageItem)); + if (NS_FAILED(rv)) { + LOGCLIP( + (" %s is missing at GetTransferData()\n", imageMimeTypes[i])); + continue; + } + + image = do_QueryInterface(imageItem); + if (image) { + LOGCLIP( + (" %s is available at GetTransferData()\n", imageMimeTypes[i])); + break; + } + } + + if (!image) { // Not getting an image for an image mime type!? + LOGCLIP((" Failed to get any image mime from GetTransferData()!\n")); + return; + } + + GdkPixbuf* pixbuf = nsImageToPixbuf::ImageToPixbuf(image); + if (!pixbuf) { + LOGCLIP((" nsImageToPixbuf::ImageToPixbuf() failed!\n")); + return; + } + + LOGCLIP((" Setting pixbuf image data as %s\n", + gdk_atom_name(selectionTarget))); + gtk_selection_data_set_pixbuf(aSelectionData, pixbuf); + g_object_unref(pixbuf); + return; + } + + if (selectionTarget == gdk_atom_intern(kHTMLMime, FALSE)) { + LOGCLIP((" providing %s data\n", kHTMLMime)); + rv = trans->GetTransferData(kHTMLMime, getter_AddRefs(item)); + if (NS_FAILED(rv) || !item) { + LOGCLIP((" failed to get %s data by GetTransferData()!\n", kHTMLMime)); + return; + } + + nsCOMPtr<nsISupportsString> wideString; + wideString = do_QueryInterface(item); + if (!wideString) { + LOGCLIP((" failed to get wideString interface!")); + return; + } + + nsAutoString ucs2string; + wideString->GetData(ucs2string); + + nsAutoCString html; + // Add the prefix so the encoding is correctly detected. + html.AppendLiteral(kHTMLMarkupPrefix); + AppendUTF16toUTF8(ucs2string, html); + + LOGCLIP((" Setting %d bytest of %s data\n", html.Length(), + gdk_atom_name(selectionTarget))); + gtk_selection_data_set(aSelectionData, selectionTarget, 8, + (const guchar*)html.get(), html.Length()); + return; + } + + LOGCLIP((" Try if we have anything at GetTransferData() for %s\n", + gdk_atom_name(selectionTarget))); + + // Try to match up the selection data target to something our + // transferable provides. + gchar* target_name = gdk_atom_name(selectionTarget); + if (!target_name) { + LOGCLIP((" Failed to get target name!\n")); + return; + } + + rv = trans->GetTransferData(target_name, getter_AddRefs(item)); + // nothing found? + if (NS_FAILED(rv) || !item) { + LOGCLIP((" Failed to get anything from GetTransferData()!\n")); + g_free(target_name); + return; + } + + void* primitive_data = nullptr; + uint32_t dataLen = 0; + nsPrimitiveHelpers::CreateDataFromPrimitive(nsDependentCString(target_name), + item, &primitive_data, &dataLen); + + if (primitive_data) { + LOGCLIP((" Setting %s as a primitive data type, %d bytes\n", target_name, + dataLen)); + gtk_selection_data_set(aSelectionData, selectionTarget, + 8, /* 8 bits in a unit */ + (const guchar*)primitive_data, dataLen); + free(primitive_data); + } else { + LOGCLIP((" Failed to get primitive data!\n")); + } + + g_free(target_name); +} + +void nsClipboard::SelectionClearEvent(GtkClipboard* aGtkClipboard) { + int32_t whichClipboard = GetGeckoClipboardType(aGtkClipboard); + if (whichClipboard < 0) { + return; + } + + LOGCLIP(("nsClipboard::SelectionClearEvent (%s)\n", + whichClipboard == kSelectionClipboard ? "primary" : "clipboard")); + + ClearTransferable(whichClipboard); +} + +void clipboard_get_cb(GtkClipboard* aGtkClipboard, + GtkSelectionData* aSelectionData, guint info, + gpointer user_data) { + LOGCLIP(("clipboard_get_cb() callback\n")); + nsClipboard* aClipboard = static_cast<nsClipboard*>(user_data); + aClipboard->SelectionGetEvent(aGtkClipboard, aSelectionData); +} + +void clipboard_clear_cb(GtkClipboard* aGtkClipboard, gpointer user_data) { + LOGCLIP(("clipboard_clear_cb() callback\n")); + nsClipboard* aClipboard = static_cast<nsClipboard*>(user_data); + aClipboard->SelectionClearEvent(aGtkClipboard); +} + +/* + * when copy-paste, mozilla wants data encoded using UCS2, + * other app such as StarOffice use "text/html"(RFC2854). + * This function convert data(got from GTK clipboard) + * to data mozilla wanted. + * + * data from GTK clipboard can be 3 forms: + * 1. From current mozilla + * "text/html", charset = utf-16 + * 2. From old version mozilla or mozilla-based app + * content("body" only), charset = utf-16 + * 3. From other app who use "text/html" when copy-paste + * "text/html", has "charset" info + * + * data : got from GTK clipboard + * dataLength: got from GTK clipboard + * body : pass to Mozilla + * bodyLength: pass to Mozilla + */ +bool ConvertHTMLtoUCS2(const char* data, int32_t dataLength, nsCString& charset, + char16_t** unicodeData, int32_t& outUnicodeLen) { + if (charset.EqualsLiteral("UTF-16")) { // current mozilla + outUnicodeLen = (dataLength / 2) - 1; + *unicodeData = reinterpret_cast<char16_t*>( + moz_xmalloc((outUnicodeLen + sizeof('\0')) * sizeof(char16_t))); + memcpy(*unicodeData, data + sizeof(char16_t), + outUnicodeLen * sizeof(char16_t)); + (*unicodeData)[outUnicodeLen] = '\0'; + return true; + } else if (charset.EqualsLiteral("UNKNOWN")) { + outUnicodeLen = 0; + return false; + } else { + // app which use "text/html" to copy&paste + // get the decoder + auto encoding = Encoding::ForLabelNoReplacement(charset); + if (!encoding) { + LOGCLIP(("ConvertHTMLtoUCS2: get unicode decoder error\n")); + outUnicodeLen = 0; + return false; + } + + auto dataSpan = Span(data, dataLength); + // Remove kHTMLMarkupPrefix again, it won't necessarily cause any + // issues, but might confuse other users. + const size_t prefixLen = ArrayLength(kHTMLMarkupPrefix) - 1; + if (dataSpan.Length() >= prefixLen && + Substring(data, prefixLen).EqualsLiteral(kHTMLMarkupPrefix)) { + dataSpan = dataSpan.From(prefixLen); + } + + auto decoder = encoding->NewDecoder(); + CheckedInt<size_t> needed = + decoder->MaxUTF16BufferLength(dataSpan.Length()); + if (!needed.isValid() || needed.value() > INT32_MAX) { + outUnicodeLen = 0; + return false; + } + + outUnicodeLen = 0; + if (needed.value()) { + *unicodeData = reinterpret_cast<char16_t*>( + moz_xmalloc((needed.value() + 1) * sizeof(char16_t))); + uint32_t result; + size_t read; + size_t written; + bool hadErrors; + Tie(result, read, written, hadErrors) = decoder->DecodeToUTF16( + AsBytes(dataSpan), Span(*unicodeData, needed.value()), true); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == size_t(dataSpan.Length())); + MOZ_ASSERT(written <= needed.value()); + Unused << hadErrors; + outUnicodeLen = written; + // null terminate. + (*unicodeData)[outUnicodeLen] = '\0'; + return true; + } // if valid length + } + return false; +} + +/* + * get "charset" information from clipboard data + * return value can be: + * 1. "UTF-16": mozilla or "text/html" with "charset=utf-16" + * 2. "UNKNOWN": mozilla can't detect what encode it use + * 3. other: "text/html" with other charset than utf-16 + */ +bool GetHTMLCharset(const char* data, int32_t dataLength, nsCString& str) { + // if detect "FFFE" or "FEFF", assume UTF-16 + char16_t* beginChar = (char16_t*)data; + if ((beginChar[0] == 0xFFFE) || (beginChar[0] == 0xFEFF)) { + str.AssignLiteral("UTF-16"); + LOGCLIP(("GetHTMLCharset: Charset of HTML is UTF-16\n")); + return true; + } + // no "FFFE" and "FEFF", assume ASCII first to find "charset" info + const nsDependentCSubstring htmlStr(data, dataLength); + nsACString::const_iterator start, end; + htmlStr.BeginReading(start); + htmlStr.EndReading(end); + nsACString::const_iterator valueStart(start), valueEnd(start); + + if (CaseInsensitiveFindInReadable("CONTENT=\"text/html;"_ns, start, end)) { + start = end; + htmlStr.EndReading(end); + + if (CaseInsensitiveFindInReadable("charset="_ns, start, end)) { + valueStart = end; + start = end; + htmlStr.EndReading(end); + + if (FindCharInReadable('"', start, end)) valueEnd = start; + } + } + // find "charset" in HTML + if (valueStart != valueEnd) { + str = Substring(valueStart, valueEnd); + ToUpperCase(str); + LOGCLIP(("GetHTMLCharset: Charset of HTML = %s\n", str.get())); + return true; + } + str.AssignLiteral("UNKNOWN"); + LOGCLIP(("GetHTMLCharset: Failed to get HTML Charset!\n")); + return false; +} diff --git a/widget/gtk/nsClipboard.h b/widget/gtk/nsClipboard.h new file mode 100644 index 0000000000..95b6eb828a --- /dev/null +++ b/widget/gtk/nsClipboard.h @@ -0,0 +1,90 @@ +/* -*- 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 __nsClipboard_h_ +#define __nsClipboard_h_ + +#include "mozilla/UniquePtr.h" +#include "nsIClipboard.h" +#include "nsIObserver.h" +#include <gtk/gtk.h> + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gClipboardLog; +# define LOGCLIP(args) MOZ_LOG(gClipboardLog, mozilla::LogLevel::Debug, args) +#else +# define LOGCLIP(args) +#endif /* MOZ_LOGGING */ + +class nsRetrievalContext { + public: + // Get actual clipboard content (GetClipboardData/GetClipboardText) + // which has to be released by ReleaseClipboardData(). + virtual const char* GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard, + uint32_t* aContentLength) = 0; + virtual const char* GetClipboardText(int32_t aWhichClipboard) = 0; + virtual void ReleaseClipboardData(const char* aClipboardData) = 0; + + // Get data mime types which can be obtained from clipboard. + // The returned array has to be released by g_free(). + virtual GdkAtom* GetTargets(int32_t aWhichClipboard, int* aTargetNum) = 0; + + virtual bool HasSelectionSupport(void) = 0; + + virtual ~nsRetrievalContext() = default; +}; + +class nsClipboard : public nsIClipboard, public nsIObserver { + public: + nsClipboard(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSICLIPBOARD + + // Make sure we are initialized, called from the factory + // constructor + nsresult Init(void); + + // Someone requested the selection + void SelectionGetEvent(GtkClipboard* aGtkClipboard, + GtkSelectionData* aSelectionData); + void SelectionClearEvent(GtkClipboard* aGtkClipboard); + + private: + virtual ~nsClipboard(); + + // Get our hands on the correct transferable, given a specific + // clipboard + nsITransferable* GetTransferable(int32_t aWhichClipboard); + + // Send clipboard data by nsITransferable + void SetTransferableData(nsITransferable* aTransferable, nsCString& aFlavor, + const char* aClipboardData, + uint32_t aClipboardDataLength); + + void ClearTransferable(int32_t aWhichClipboard); + + // Hang on to our owners and transferables so we can transfer data + // when asked. + nsCOMPtr<nsIClipboardOwner> mSelectionOwner; + nsCOMPtr<nsIClipboardOwner> mGlobalOwner; + nsCOMPtr<nsITransferable> mSelectionTransferable; + nsCOMPtr<nsITransferable> mGlobalTransferable; + mozilla::UniquePtr<nsRetrievalContext> mContext; +}; + +extern const int kClipboardTimeout; + +GdkAtom GetSelectionAtom(int32_t aWhichClipboard); +int GetGeckoClipboardType(GtkClipboard* aGtkClipboard); + +#endif /* __nsClipboard_h_ */ diff --git a/widget/gtk/nsClipboardWayland.cpp b/widget/gtk/nsClipboardWayland.cpp new file mode 100644 index 0000000000..f8e1031e9e --- /dev/null +++ b/widget/gtk/nsClipboardWayland.cpp @@ -0,0 +1,915 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "nsArrayUtils.h" +#include "nsClipboard.h" +#include "nsClipboardWayland.h" +#include "nsSupportsPrimitives.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsPrimitiveHelpers.h" +#include "nsImageToPixbuf.h" +#include "nsStringStream.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsDragService.h" +#include "mozwayland/mozwayland.h" +#include "nsWaylandDisplay.h" + +#include <gtk/gtk.h> +#include <poll.h> +#include <stdlib.h> +#include <string.h> +#include <fcntl.h> +#include <errno.h> + +using namespace mozilla; +using namespace mozilla::widget; + +const char* nsRetrievalContextWayland::sTextMimeTypes[TEXT_MIME_TYPES_NUM] = { + "text/plain;charset=utf-8", "UTF8_STRING", "COMPOUND_TEXT"}; + +static inline GdkDragAction wl_to_gdk_actions(uint32_t dnd_actions) { + GdkDragAction actions = GdkDragAction(0); + + if (dnd_actions & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY) + actions = GdkDragAction(actions | GDK_ACTION_COPY); + if (dnd_actions & WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE) + actions = GdkDragAction(actions | GDK_ACTION_MOVE); + + return actions; +} + +static inline uint32_t gdk_to_wl_actions(GdkDragAction action) { + uint32_t dnd_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; + + if (action & (GDK_ACTION_COPY | GDK_ACTION_LINK | GDK_ACTION_PRIVATE)) + dnd_actions |= WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY; + if (action & GDK_ACTION_MOVE) + dnd_actions |= WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; + + return dnd_actions; +} + +static GtkWidget* get_gtk_widget_for_wl_surface(struct wl_surface* surface) { + GdkWindow* gdkParentWindow = + static_cast<GdkWindow*>(wl_surface_get_user_data(surface)); + + gpointer user_data = nullptr; + gdk_window_get_user_data(gdkParentWindow, &user_data); + + return GTK_WIDGET(user_data); +} + +void DataOffer::AddMIMEType(const char* aMimeType) { + GdkAtom atom = gdk_atom_intern(aMimeType, FALSE); + mTargetMIMETypes.AppendElement(atom); +} + +GdkAtom* DataOffer::GetTargets(int* aTargetNum) { + int length = mTargetMIMETypes.Length(); + if (!length) { + *aTargetNum = 0; + return nullptr; + } + + GdkAtom* targetList = + reinterpret_cast<GdkAtom*>(g_malloc(sizeof(GdkAtom) * length)); + for (int32_t j = 0; j < length; j++) { + targetList[j] = mTargetMIMETypes[j]; + } + + *aTargetNum = length; + return targetList; +} + +bool DataOffer::HasTarget(const char* aMimeType) { + int length = mTargetMIMETypes.Length(); + for (int32_t j = 0; j < length; j++) { + if (mTargetMIMETypes[j] == gdk_atom_intern(aMimeType, FALSE)) { + LOGCLIP(("DataOffer::HasTarget() we have mime %s\n", aMimeType)); + return true; + } + } + LOGCLIP(("DataOffer::HasTarget() missing mime %s\n", aMimeType)); + return false; +} + +char* DataOffer::GetData(wl_display* aDisplay, const char* aMimeType, + uint32_t* aContentLength) { + LOGCLIP(("DataOffer::GetData() mime %s\n", aMimeType)); + + int pipe_fd[2]; + if (pipe(pipe_fd) == -1) return nullptr; + + if (!RequestDataTransfer(aMimeType, pipe_fd[1])) { + NS_WARNING("DataOffer::RequestDataTransfer() failed!"); + close(pipe_fd[0]); + close(pipe_fd[1]); + return nullptr; + } + + close(pipe_fd[1]); + wl_display_flush(aDisplay); + + struct pollfd fds; + fds.fd = pipe_fd[0]; + fds.events = POLLIN; + int pollReturn = -1; + +#define MAX_CLIPBOARD_POLL_ATTEMPTS 10 + for (int i = 0; i < MAX_CLIPBOARD_POLL_ATTEMPTS; i++) { + pollReturn = poll(&fds, 1, kClipboardTimeout / 1000); + // ret > 0 means we have data available + // ret = 0 means poll timeout expired + // ret < 0 means poll failed with error + if (pollReturn >= 0) { + break; + } + // We should try again for EINTR/EAGAIN errors, + // quit for all other ones. + if (errno != EINTR && errno != EAGAIN) { + break; + } + } + // Quit for poll error() and timeout + if (pollReturn <= 0) { + NS_WARNING("DataOffer::RequestDataTransfer() poll timeout!"); + close(pipe_fd[0]); + return nullptr; + } + + GIOChannel* channel = g_io_channel_unix_new(pipe_fd[0]); + GError* error = nullptr; + char* clipboardData = nullptr; + + g_io_channel_set_encoding(channel, nullptr, &error); + if (!error) { + gsize length = 0; + g_io_channel_read_to_end(channel, &clipboardData, &length, &error); + if (length == 0) { + // We don't have valid clipboard data although + // g_io_channel_read_to_end() allocated clipboardData for us. + // Release it now and return nullptr to indicate + // we don't have reqested data flavour. + g_free((void*)clipboardData); + clipboardData = nullptr; + } + *aContentLength = length; + } + + if (error) { + NS_WARNING( + nsPrintfCString("Unexpected error when reading clipboard data: %s", + error->message) + .get()); + g_error_free(error); + } + + g_io_channel_unref(channel); + close(pipe_fd[0]); + + LOGCLIP((" Got clipboard data length %d\n", *aContentLength)); + return clipboardData; +} + +bool WaylandDataOffer::RequestDataTransfer(const char* aMimeType, int fd) { + if (mWaylandDataOffer) { + wl_data_offer_receive(mWaylandDataOffer, aMimeType, fd); + return true; + } + + return false; +} + +void WaylandDataOffer::DragOfferAccept(const char* aMimeType, uint32_t aTime) { + wl_data_offer_accept(mWaylandDataOffer, aTime, aMimeType); +} + +/* We follow logic of gdk_wayland_drag_context_commit_status()/gdkdnd-wayland.c + * here. + */ +void WaylandDataOffer::SetDragStatus(GdkDragAction aPreferredAction, + uint32_t aTime) { + uint32_t preferredAction = gdk_to_wl_actions(aPreferredAction); + uint32_t allActions = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; + + /* We only don't choose a preferred action if we don't accept any. + * If we do accept any, it is currently alway copy and move + */ + if (preferredAction != WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE) { + allActions = WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | + WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; + } + + wl_data_offer_set_actions(mWaylandDataOffer, allActions, preferredAction); + + /* Workaround Wayland D&D architecture here. To get the data_device_drop() + signal (which routes to nsDragService::GetData() call) we need to + accept at least one mime type before data_device_leave(). + + Real wl_data_offer_accept() for actualy requested data mime type is + called from nsDragService::GetData(). + */ + if (mTargetMIMETypes[0]) { + wl_data_offer_accept(mWaylandDataOffer, aTime, + gdk_atom_name(mTargetMIMETypes[0])); + } +} + +void WaylandDataOffer::SetSelectedDragAction(uint32_t aWaylandAction) { + mSelectedDragAction = aWaylandAction; +} + +GdkDragAction WaylandDataOffer::GetSelectedDragAction() { + return wl_to_gdk_actions(mSelectedDragAction); +} + +void WaylandDataOffer::SetAvailableDragActions(uint32_t aWaylandActions) { + mAvailableDragActions = aWaylandActions; +} + +GdkDragAction WaylandDataOffer::GetAvailableDragActions() { + return wl_to_gdk_actions(mAvailableDragActions); +} + +void WaylandDataOffer::SetWaylandDragContext( + nsWaylandDragContext* aDragContext) { + mDragContext = aDragContext; +} + +nsWaylandDragContext* WaylandDataOffer::GetWaylandDragContext() { + return mDragContext; +} + +static void data_offer_offer(void* data, struct wl_data_offer* wl_data_offer, + const char* type) { + auto* offer = static_cast<DataOffer*>(data); + offer->AddMIMEType(type); +} + +/* Advertise all available drag and drop actions from source. + * We don't use that but follow gdk_wayland_drag_context_commit_status() + * from gdkdnd-wayland.c here. + */ +static void data_offer_source_actions(void* data, + struct wl_data_offer* wl_data_offer, + uint32_t source_actions) { + auto* offer = static_cast<WaylandDataOffer*>(data); + offer->SetAvailableDragActions(source_actions); +} + +/* Advertise recently selected drag and drop action by compositor, based + * on source actions and user choice (key modifiers, etc.). + */ +static void data_offer_action(void* data, struct wl_data_offer* wl_data_offer, + uint32_t dnd_action) { + auto* offer = static_cast<WaylandDataOffer*>(data); + offer->SetSelectedDragAction(dnd_action); + + /* Mimic GTK which triggers the motion event callback */ + nsWaylandDragContext* dropContext = offer->GetWaylandDragContext(); + if (dropContext) { + uint32_t time; + nscoord x, y; + dropContext->GetLastDropInfo(&time, &x, &y); + + WindowDragMotionHandler(dropContext->GetWidget(), nullptr, dropContext, x, + y, time); + } +} + +/* wl_data_offer callback description: + * + * data_offer_offer - Is called for each MIME type available at wl_data_offer. + * data_offer_source_actions - This event indicates the actions offered by + * the data source. + * data_offer_action - This event indicates the action selected by + * the compositor after matching the source/destination + * side actions. + */ +static const moz_wl_data_offer_listener data_offer_listener = { + data_offer_offer, data_offer_source_actions, data_offer_action}; + +WaylandDataOffer::WaylandDataOffer(wl_data_offer* aWaylandDataOffer) + : mWaylandDataOffer(aWaylandDataOffer), + mDragContext(nullptr), + mSelectedDragAction(0), + mAvailableDragActions(0) { + wl_data_offer_add_listener( + mWaylandDataOffer, (struct wl_data_offer_listener*)&data_offer_listener, + this); +} + +WaylandDataOffer::~WaylandDataOffer(void) { + if (mWaylandDataOffer) { + wl_data_offer_destroy(mWaylandDataOffer); + } +} + +bool PrimaryDataOffer::RequestDataTransfer(const char* aMimeType, int fd) { + if (mPrimaryDataOfferGtk) { + gtk_primary_selection_offer_receive(mPrimaryDataOfferGtk, aMimeType, fd); + return true; + } + if (mPrimaryDataOfferZwpV1) { + zwp_primary_selection_offer_v1_receive(mPrimaryDataOfferZwpV1, aMimeType, + fd); + return true; + } + return false; +} + +static void primary_data_offer( + void* data, gtk_primary_selection_offer* primary_selection_offer, + const char* mime_type) { + auto* offer = static_cast<DataOffer*>(data); + offer->AddMIMEType(mime_type); +} + +static void primary_data_offer( + void* data, zwp_primary_selection_offer_v1* primary_selection_offer, + const char* mime_type) { + auto* offer = static_cast<DataOffer*>(data); + offer->AddMIMEType(mime_type); +} + +/* gtk_primary_selection_offer_listener callback description: + * + * primary_data_offer - Is called for each MIME type available at + * gtk_primary_selection_offer. + */ +static const struct gtk_primary_selection_offer_listener + primary_selection_offer_listener_gtk = {primary_data_offer}; + +static const struct zwp_primary_selection_offer_v1_listener + primary_selection_offer_listener_zwp_v1 = {primary_data_offer}; + +PrimaryDataOffer::PrimaryDataOffer( + gtk_primary_selection_offer* aPrimaryDataOffer) + : mPrimaryDataOfferGtk(aPrimaryDataOffer), mPrimaryDataOfferZwpV1(nullptr) { + gtk_primary_selection_offer_add_listener( + aPrimaryDataOffer, &primary_selection_offer_listener_gtk, this); +} + +PrimaryDataOffer::PrimaryDataOffer( + zwp_primary_selection_offer_v1* aPrimaryDataOffer) + : mPrimaryDataOfferGtk(nullptr), mPrimaryDataOfferZwpV1(aPrimaryDataOffer) { + zwp_primary_selection_offer_v1_add_listener( + aPrimaryDataOffer, &primary_selection_offer_listener_zwp_v1, this); +} + +PrimaryDataOffer::~PrimaryDataOffer(void) { + if (mPrimaryDataOfferGtk) { + gtk_primary_selection_offer_destroy(mPrimaryDataOfferGtk); + } + if (mPrimaryDataOfferZwpV1) { + zwp_primary_selection_offer_v1_destroy(mPrimaryDataOfferZwpV1); + } +} + +NS_IMPL_ISUPPORTS(nsWaylandDragContext, nsISupports); + +nsWaylandDragContext::nsWaylandDragContext(WaylandDataOffer* aDataOffer, + wl_display* aDisplay) + : mDataOffer(aDataOffer), + mDisplay(aDisplay), + mTime(0), + mGtkWidget(nullptr), + mX(0), + mY(0) { + aDataOffer->SetWaylandDragContext(this); +} + +void nsWaylandDragContext::DropDataEnter(GtkWidget* aGtkWidget, uint32_t aTime, + nscoord aX, nscoord aY) { + mTime = aTime; + mGtkWidget = aGtkWidget; + mX = aX; + mY = aY; +} + +void nsWaylandDragContext::DropMotion(uint32_t aTime, nscoord aX, nscoord aY) { + mTime = aTime; + mX = aX; + mY = aY; +} + +void nsWaylandDragContext::GetLastDropInfo(uint32_t* aTime, nscoord* aX, + nscoord* aY) { + *aTime = mTime; + *aX = mX; + *aY = mY; +} + +void nsWaylandDragContext::SetDragStatus(GdkDragAction aPreferredAction) { + mDataOffer->SetDragStatus(aPreferredAction, mTime); +} + +GdkDragAction nsWaylandDragContext::GetAvailableDragActions() { + GdkDragAction gdkAction = mDataOffer->GetSelectedDragAction(); + + // We emulate gdk_drag_context_get_actions() here. + if (!gdkAction) { + gdkAction = mDataOffer->GetAvailableDragActions(); + } + + return gdkAction; +} + +GList* nsWaylandDragContext::GetTargets() { + int targetNums; + GdkAtom* atoms = mDataOffer->GetTargets(&targetNums); + + GList* targetList = nullptr; + for (int i = 0; i < targetNums; i++) { + targetList = g_list_append(targetList, GDK_ATOM_TO_POINTER(atoms[i])); + } + + return targetList; +} + +char* nsWaylandDragContext::GetData(const char* aMimeType, + uint32_t* aContentLength) { + mDataOffer->DragOfferAccept(aMimeType, mTime); + return mDataOffer->GetData(mDisplay, aMimeType, aContentLength); +} + +void nsRetrievalContextWayland::RegisterNewDataOffer( + wl_data_offer* aWaylandDataOffer) { + DataOffer* dataOffer = static_cast<DataOffer*>( + g_hash_table_lookup(mActiveOffers, aWaylandDataOffer)); + MOZ_ASSERT( + dataOffer == nullptr, + "Registered WaylandDataOffer already exists. Wayland protocol error?"); + + if (!dataOffer) { + dataOffer = new WaylandDataOffer(aWaylandDataOffer); + g_hash_table_insert(mActiveOffers, aWaylandDataOffer, dataOffer); + } +} + +void nsRetrievalContextWayland::RegisterNewDataOffer( + gtk_primary_selection_offer* aPrimaryDataOffer) { + DataOffer* dataOffer = static_cast<DataOffer*>( + g_hash_table_lookup(mActiveOffers, aPrimaryDataOffer)); + MOZ_ASSERT( + dataOffer == nullptr, + "Registered PrimaryDataOffer already exists. Wayland protocol error?"); + + if (!dataOffer) { + dataOffer = new PrimaryDataOffer(aPrimaryDataOffer); + g_hash_table_insert(mActiveOffers, aPrimaryDataOffer, dataOffer); + } +} + +void nsRetrievalContextWayland::RegisterNewDataOffer( + zwp_primary_selection_offer_v1* aPrimaryDataOffer) { + DataOffer* dataOffer = static_cast<DataOffer*>( + g_hash_table_lookup(mActiveOffers, aPrimaryDataOffer)); + MOZ_ASSERT( + dataOffer == nullptr, + "Registered PrimaryDataOffer already exists. Wayland protocol error?"); + + if (!dataOffer) { + dataOffer = new PrimaryDataOffer(aPrimaryDataOffer); + g_hash_table_insert(mActiveOffers, aPrimaryDataOffer, dataOffer); + } +} + +void nsRetrievalContextWayland::SetClipboardDataOffer( + wl_data_offer* aWaylandDataOffer) { + // Delete existing clipboard data offer + mClipboardOffer = nullptr; + + // null aWaylandDataOffer indicates that our clipboard content + // is no longer valid and should be release. + if (aWaylandDataOffer != nullptr) { + DataOffer* dataOffer = static_cast<DataOffer*>( + g_hash_table_lookup(mActiveOffers, aWaylandDataOffer)); + NS_ASSERTION(dataOffer, "We're missing stored clipboard data offer!"); + if (dataOffer) { + g_hash_table_remove(mActiveOffers, aWaylandDataOffer); + mClipboardOffer = WrapUnique(dataOffer); + } + } +} + +void nsRetrievalContextWayland::SetPrimaryDataOffer( + gtk_primary_selection_offer* aPrimaryDataOffer) { + // Release any primary offer we have. + mPrimaryOffer = nullptr; + + // aPrimaryDataOffer can be null which means we lost + // the mouse selection. + if (aPrimaryDataOffer) { + DataOffer* dataOffer = static_cast<DataOffer*>( + g_hash_table_lookup(mActiveOffers, aPrimaryDataOffer)); + NS_ASSERTION(dataOffer, "We're missing primary data offer!"); + if (dataOffer) { + g_hash_table_remove(mActiveOffers, aPrimaryDataOffer); + mPrimaryOffer = WrapUnique(dataOffer); + } + } +} + +void nsRetrievalContextWayland::SetPrimaryDataOffer( + zwp_primary_selection_offer_v1* aPrimaryDataOffer) { + // Release any primary offer we have. + mPrimaryOffer = nullptr; + + // aPrimaryDataOffer can be null which means we lost + // the mouse selection. + if (aPrimaryDataOffer) { + DataOffer* dataOffer = static_cast<DataOffer*>( + g_hash_table_lookup(mActiveOffers, aPrimaryDataOffer)); + NS_ASSERTION(dataOffer, "We're missing primary data offer!"); + if (dataOffer) { + g_hash_table_remove(mActiveOffers, aPrimaryDataOffer); + mPrimaryOffer = WrapUnique(dataOffer); + } + } +} + +void nsRetrievalContextWayland::AddDragAndDropDataOffer( + wl_data_offer* aDropDataOffer) { + // Remove any existing D&D contexts. + mDragContext = nullptr; + + WaylandDataOffer* dataOffer = static_cast<WaylandDataOffer*>( + g_hash_table_lookup(mActiveOffers, aDropDataOffer)); + NS_ASSERTION(dataOffer, "We're missing drag and drop data offer!"); + if (dataOffer) { + g_hash_table_remove(mActiveOffers, aDropDataOffer); + mDragContext = new nsWaylandDragContext(dataOffer, mDisplay->GetDisplay()); + } +} + +nsWaylandDragContext* nsRetrievalContextWayland::GetDragContext(void) { + return mDragContext; +} + +void nsRetrievalContextWayland::ClearDragAndDropDataOffer(void) { + mDragContext = nullptr; +} + +// We have a new fresh data content. +// We should attach listeners to it and save for further use. +static void data_device_data_offer(void* data, + struct wl_data_device* data_device, + struct wl_data_offer* offer) { + LOGCLIP(("data_device_data_offer() callback\n")); + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->RegisterNewDataOffer(offer); +} + +// The new fresh data content is clipboard. +static void data_device_selection(void* data, + struct wl_data_device* wl_data_device, + struct wl_data_offer* offer) { + LOGCLIP(("data_device_selection() callback\n")); + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->SetClipboardDataOffer(offer); +} + +// The new fresh wayland data content is drag and drop. +static void data_device_enter(void* data, struct wl_data_device* data_device, + uint32_t time, struct wl_surface* surface, + int32_t x_fixed, int32_t y_fixed, + struct wl_data_offer* offer) { + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->AddDragAndDropDataOffer(offer); + + nsWaylandDragContext* dragContext = context->GetDragContext(); + + GtkWidget* gtkWidget = get_gtk_widget_for_wl_surface(surface); + if (!gtkWidget) { + NS_WARNING("DragAndDrop: Unable to get GtkWidget for wl_surface!"); + return; + } + + LOGDRAG(("nsWindow data_device_enter for GtkWidget %p\n", (void*)gtkWidget)); + dragContext->DropDataEnter(gtkWidget, time, wl_fixed_to_int(x_fixed), + wl_fixed_to_int(y_fixed)); +} + +static void data_device_leave(void* data, struct wl_data_device* data_device) { + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + + nsWaylandDragContext* dropContext = context->GetDragContext(); + WindowDragLeaveHandler(dropContext->GetWidget()); + + LOGDRAG(("nsWindow data_device_leave for GtkWidget %p\n", + (void*)dropContext->GetWidget())); + context->ClearDragAndDropDataOffer(); +} + +static void data_device_motion(void* data, struct wl_data_device* data_device, + uint32_t time, int32_t x_fixed, + int32_t y_fixed) { + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + + nsWaylandDragContext* dropContext = context->GetDragContext(); + + nscoord x = wl_fixed_to_int(x_fixed); + nscoord y = wl_fixed_to_int(y_fixed); + dropContext->DropMotion(time, x, y); + + LOGDRAG(("nsWindow data_device_motion for GtkWidget %p\n", + (void*)dropContext->GetWidget())); + WindowDragMotionHandler(dropContext->GetWidget(), nullptr, dropContext, x, y, + time); +} + +static void data_device_drop(void* data, struct wl_data_device* data_device) { + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + nsWaylandDragContext* dropContext = context->GetDragContext(); + + uint32_t time; + nscoord x, y; + dropContext->GetLastDropInfo(&time, &x, &y); + + LOGDRAG(("nsWindow data_device_drop GtkWidget %p\n", + (void*)dropContext->GetWidget())); + WindowDragDropHandler(dropContext->GetWidget(), nullptr, dropContext, x, y, + time); +} + +/* wl_data_device callback description: + * + * data_device_data_offer - It's called when there's a new wl_data_offer + * available. We need to attach wl_data_offer_listener + * to it to get available MIME types. + * + * data_device_selection - It's called when the new wl_data_offer + * is a clipboard content. + * + * data_device_enter - It's called when the new wl_data_offer is a drag & drop + * content and it's tied to actual wl_surface. + * data_device_leave - It's called when the wl_data_offer (drag & dop) is not + * valid any more. + * data_device_motion - It's called when the drag and drop selection moves + * across wl_surface. + * data_device_drop - It's called when D&D operation is sucessfully finished + * and we can read the data from D&D. + * It's generated only if we call wl_data_offer_accept() and + * wl_data_offer_set_actions() from data_device_motion + * callback. + */ +static const struct wl_data_device_listener data_device_listener = { + data_device_data_offer, data_device_enter, data_device_leave, + data_device_motion, data_device_drop, data_device_selection}; + +static void primary_selection_data_offer( + void* data, struct gtk_primary_selection_device* primary_selection_device, + struct gtk_primary_selection_offer* primary_offer) { + LOGCLIP(("primary_selection_data_offer() callback\n")); + // create and add listener + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->RegisterNewDataOffer(primary_offer); +} + +static void primary_selection_data_offer( + void* data, + struct zwp_primary_selection_device_v1* primary_selection_device, + struct zwp_primary_selection_offer_v1* primary_offer) { + LOGCLIP(("primary_selection_data_offer() callback\n")); + // create and add listener + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->RegisterNewDataOffer(primary_offer); +} + +static void primary_selection_selection( + void* data, struct gtk_primary_selection_device* primary_selection_device, + struct gtk_primary_selection_offer* primary_offer) { + LOGCLIP(("primary_selection_selection() callback\n")); + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->SetPrimaryDataOffer(primary_offer); +} + +static void primary_selection_selection( + void* data, + struct zwp_primary_selection_device_v1* primary_selection_device, + struct zwp_primary_selection_offer_v1* primary_offer) { + LOGCLIP(("primary_selection_selection() callback\n")); + nsRetrievalContextWayland* context = + static_cast<nsRetrievalContextWayland*>(data); + context->SetPrimaryDataOffer(primary_offer); +} + +/* gtk_primary_selection_device callback description: + * + * primary_selection_data_offer - It's called when there's a new + * gtk_primary_selection_offer available. We need to + * attach gtk_primary_selection_offer_listener to it + * to get available MIME types. + * + * primary_selection_selection - It's called when the new + * gtk_primary_selection_offer is a primary selection + * content. It can be also called with + * gtk_primary_selection_offer = null which means + * there's no primary selection. + */ +static const struct gtk_primary_selection_device_listener + primary_selection_device_listener_gtk = { + primary_selection_data_offer, + primary_selection_selection, +}; + +static const struct zwp_primary_selection_device_v1_listener + primary_selection_device_listener_zwp_v1 = { + primary_selection_data_offer, + primary_selection_selection, +}; + +bool nsRetrievalContextWayland::HasSelectionSupport(void) { + return (mDisplay->GetPrimarySelectionDeviceManagerZwpV1() != nullptr || + mDisplay->GetPrimarySelectionDeviceManagerGtk() != nullptr); +} + +nsRetrievalContextWayland::nsRetrievalContextWayland(void) + : mInitialized(false), + mDisplay(WaylandDisplayGet()), + mActiveOffers(g_hash_table_new(NULL, NULL)), + mClipboardOffer(nullptr), + mPrimaryOffer(nullptr), + mDragContext(nullptr), + mClipboardRequestNumber(0), + mClipboardData(nullptr), + mClipboardDataLength(0) { + wl_data_device* dataDevice = wl_data_device_manager_get_data_device( + mDisplay->GetDataDeviceManager(), mDisplay->GetSeat()); + wl_data_device_add_listener(dataDevice, &data_device_listener, this); + + if (mDisplay->GetPrimarySelectionDeviceManagerZwpV1()) { + zwp_primary_selection_device_v1* primaryDataDevice = + zwp_primary_selection_device_manager_v1_get_device( + mDisplay->GetPrimarySelectionDeviceManagerZwpV1(), + mDisplay->GetSeat()); + zwp_primary_selection_device_v1_add_listener( + primaryDataDevice, &primary_selection_device_listener_zwp_v1, this); + } else if (mDisplay->GetPrimarySelectionDeviceManagerGtk()) { + gtk_primary_selection_device* primaryDataDevice = + gtk_primary_selection_device_manager_get_device( + mDisplay->GetPrimarySelectionDeviceManagerGtk(), + mDisplay->GetSeat()); + gtk_primary_selection_device_add_listener( + primaryDataDevice, &primary_selection_device_listener_gtk, this); + } + + mInitialized = true; +} + +static gboolean offer_hash_remove(gpointer wl_offer, gpointer aDataOffer, + gpointer user_data) { +#ifdef DEBUG + nsPrintfCString msg("nsRetrievalContextWayland(): leaked nsDataOffer %p\n", + aDataOffer); + NS_WARNING(msg.get()); +#endif + delete static_cast<DataOffer*>(aDataOffer); + return true; +} + +nsRetrievalContextWayland::~nsRetrievalContextWayland(void) { + g_hash_table_foreach_remove(mActiveOffers, offer_hash_remove, nullptr); + g_hash_table_destroy(mActiveOffers); +} + +GdkAtom* nsRetrievalContextWayland::GetTargets(int32_t aWhichClipboard, + int* aTargetNum) { + if (GetSelectionAtom(aWhichClipboard) == GDK_SELECTION_CLIPBOARD) { + if (mClipboardOffer) { + return mClipboardOffer->GetTargets(aTargetNum); + } + } else { + if (mPrimaryOffer) { + return mPrimaryOffer->GetTargets(aTargetNum); + } + } + + *aTargetNum = 0; + return nullptr; +} + +struct FastTrackClipboard { + FastTrackClipboard(int aClipboardRequestNumber, + nsRetrievalContextWayland* aRetrievalContex) + : mClipboardRequestNumber(aClipboardRequestNumber), + mRetrievalContex(aRetrievalContex) {} + + int mClipboardRequestNumber; + nsRetrievalContextWayland* mRetrievalContex; +}; + +static void wayland_clipboard_contents_received( + GtkClipboard* clipboard, GtkSelectionData* selection_data, gpointer data) { + LOGCLIP(("wayland_clipboard_contents_received() callback\n")); + FastTrackClipboard* fastTrack = static_cast<FastTrackClipboard*>(data); + fastTrack->mRetrievalContex->TransferFastTrackClipboard( + fastTrack->mClipboardRequestNumber, selection_data); + delete fastTrack; +} + +void nsRetrievalContextWayland::TransferFastTrackClipboard( + int aClipboardRequestNumber, GtkSelectionData* aSelectionData) { + if (mClipboardRequestNumber == aClipboardRequestNumber) { + int dataLength = gtk_selection_data_get_length(aSelectionData); + if (dataLength > 0) { + mClipboardDataLength = dataLength; + mClipboardData = reinterpret_cast<char*>( + g_malloc(sizeof(char) * (mClipboardDataLength + 1))); + memcpy(mClipboardData, gtk_selection_data_get_data(aSelectionData), + sizeof(char) * mClipboardDataLength); + mClipboardData[mClipboardDataLength] = '\0'; + } + } else { + NS_WARNING("Received obsoleted clipboard data!"); + } +} + +const char* nsRetrievalContextWayland::GetClipboardData( + const char* aMimeType, int32_t aWhichClipboard, uint32_t* aContentLength) { + NS_ASSERTION(mClipboardData == nullptr && mClipboardDataLength == 0, + "Looks like we're leaking clipboard data here!"); + + LOGCLIP(("nsRetrievalContextWayland::GetClipboardData [%p] mime %s\n", this, + aMimeType)); + + /* If actual clipboard data is owned by us we don't need to go + * through Wayland but we ask Gtk+ to directly call data + * getter callback nsClipboard::SelectionGetEvent(). + * see gtk_selection_convert() at gtk+/gtkselection.c. + */ + GdkAtom selection = GetSelectionAtom(aWhichClipboard); + if (gdk_selection_owner_get(selection)) { + LOGCLIP((" Internal clipboard content\n")); + mClipboardRequestNumber++; + gtk_clipboard_request_contents( + gtk_clipboard_get(selection), gdk_atom_intern(aMimeType, FALSE), + wayland_clipboard_contents_received, + new FastTrackClipboard(mClipboardRequestNumber, this)); + } else { + LOGCLIP((" Remote clipboard content\n")); + const auto& dataOffer = + (selection == GDK_SELECTION_PRIMARY) ? mPrimaryOffer : mClipboardOffer; + if (!dataOffer) { + // Something went wrong. We're requested to provide clipboard data + // but we haven't got any from wayland. + NS_WARNING("Requested data without valid DataOffer!"); + mClipboardData = nullptr; + mClipboardDataLength = 0; + } else { + mClipboardData = dataOffer->GetData(mDisplay->GetDisplay(), aMimeType, + &mClipboardDataLength); + } + } + + *aContentLength = mClipboardDataLength; + return reinterpret_cast<const char*>(mClipboardData); +} + +const char* nsRetrievalContextWayland::GetClipboardText( + int32_t aWhichClipboard) { + LOGCLIP(("nsRetrievalContextWayland::GetClipboardText [%p]\n", this)); + + GdkAtom selection = GetSelectionAtom(aWhichClipboard); + const auto& dataOffer = + (selection == GDK_SELECTION_PRIMARY) ? mPrimaryOffer : mClipboardOffer; + if (!dataOffer) return nullptr; + + for (unsigned int i = 0; i < TEXT_MIME_TYPES_NUM; i++) { + if (dataOffer->HasTarget(sTextMimeTypes[i])) { + uint32_t unused; + return GetClipboardData(sTextMimeTypes[i], aWhichClipboard, &unused); + } + } + return nullptr; +} + +void nsRetrievalContextWayland::ReleaseClipboardData( + const char* aClipboardData) { + LOGCLIP(("nsRetrievalContextWayland::ReleaseClipboardData [%p]\n", this)); + + NS_ASSERTION(aClipboardData == mClipboardData, + "Releasing unknown clipboard data!"); + g_free((void*)aClipboardData); + + mClipboardData = nullptr; + mClipboardDataLength = 0; +} diff --git a/widget/gtk/nsClipboardWayland.h b/widget/gtk/nsClipboardWayland.h new file mode 100644 index 0000000000..ddf5d40dc6 --- /dev/null +++ b/widget/gtk/nsClipboardWayland.h @@ -0,0 +1,162 @@ +/* -*- 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 __nsClipboardWayland_h_ +#define __nsClipboardWayland_h_ + +#include <gtk/gtk.h> +#include <gdk/gdkwayland.h> +#include <nsTArray.h> + +#include "mozilla/UniquePtr.h" +#include "nsClipboard.h" +#include "nsWaylandDisplay.h" + +struct FastTrackClipboard; + +class DataOffer { + public: + void AddMIMEType(const char* aMimeType); + + GdkAtom* GetTargets(int* aTargetNum); + bool HasTarget(const char* aMimeType); + + char* GetData(wl_display* aDisplay, const char* aMimeType, + uint32_t* aContentLength); + + virtual ~DataOffer() = default; + + private: + virtual bool RequestDataTransfer(const char* aMimeType, int fd) = 0; + + protected: + nsTArray<GdkAtom> mTargetMIMETypes; +}; + +class WaylandDataOffer : public DataOffer { + public: + explicit WaylandDataOffer(wl_data_offer* aWaylandDataOffer); + + void DragOfferAccept(const char* aMimeType, uint32_t aTime); + void SetDragStatus(GdkDragAction aPreferredAction, uint32_t aTime); + + GdkDragAction GetSelectedDragAction(); + void SetSelectedDragAction(uint32_t aWaylandAction); + + void SetAvailableDragActions(uint32_t aWaylandActions); + GdkDragAction GetAvailableDragActions(); + + void SetWaylandDragContext(nsWaylandDragContext* aDragContext); + nsWaylandDragContext* GetWaylandDragContext(); + + virtual ~WaylandDataOffer(); + + private: + bool RequestDataTransfer(const char* aMimeType, int fd) override; + + wl_data_offer* mWaylandDataOffer; + RefPtr<nsWaylandDragContext> mDragContext; + uint32_t mSelectedDragAction; + uint32_t mAvailableDragActions; +}; + +class PrimaryDataOffer : public DataOffer { + public: + explicit PrimaryDataOffer(gtk_primary_selection_offer* aPrimaryDataOffer); + explicit PrimaryDataOffer(zwp_primary_selection_offer_v1* aPrimaryDataOffer); + void SetAvailableDragActions(uint32_t aWaylandActions){}; + + virtual ~PrimaryDataOffer(); + + private: + bool RequestDataTransfer(const char* aMimeType, int fd) override; + + gtk_primary_selection_offer* mPrimaryDataOfferGtk; + zwp_primary_selection_offer_v1* mPrimaryDataOfferZwpV1; +}; + +class nsWaylandDragContext : public nsISupports { + NS_DECL_ISUPPORTS + + public: + nsWaylandDragContext(WaylandDataOffer* aWaylandDataOffer, + wl_display* aDisplay); + + void DropDataEnter(GtkWidget* aGtkWidget, uint32_t aTime, nscoord aX, + nscoord aY); + void DropMotion(uint32_t aTime, nscoord aX, nscoord aY); + void GetLastDropInfo(uint32_t* aTime, nscoord* aX, nscoord* aY); + + void SetDragStatus(GdkDragAction aPreferredAction); + GdkDragAction GetAvailableDragActions(); + + GtkWidget* GetWidget() { return mGtkWidget; } + GList* GetTargets(); + char* GetData(const char* aMimeType, uint32_t* aContentLength); + + private: + virtual ~nsWaylandDragContext() = default; + + mozilla::UniquePtr<WaylandDataOffer> mDataOffer; + wl_display* mDisplay; + uint32_t mTime; + GtkWidget* mGtkWidget; + nscoord mX, mY; +}; + +class nsRetrievalContextWayland : public nsRetrievalContext { + public: + nsRetrievalContextWayland(); + + virtual const char* GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard, + uint32_t* aContentLength) override; + virtual const char* GetClipboardText(int32_t aWhichClipboard) override; + virtual void ReleaseClipboardData(const char* aClipboardData) override; + + virtual GdkAtom* GetTargets(int32_t aWhichClipboard, + int* aTargetNum) override; + virtual bool HasSelectionSupport(void) override; + + void RegisterNewDataOffer(wl_data_offer* aWaylandDataOffer); + void RegisterNewDataOffer(gtk_primary_selection_offer* aPrimaryDataOffer); + void RegisterNewDataOffer(zwp_primary_selection_offer_v1* aPrimaryDataOffer); + + void SetClipboardDataOffer(wl_data_offer* aWaylandDataOffer); + void SetPrimaryDataOffer(gtk_primary_selection_offer* aPrimaryDataOffer); + void SetPrimaryDataOffer(zwp_primary_selection_offer_v1* aPrimaryDataOffer); + void AddDragAndDropDataOffer(wl_data_offer* aWaylandDataOffer); + nsWaylandDragContext* GetDragContext(); + + void ClearDragAndDropDataOffer(); + + void TransferFastTrackClipboard(int aClipboardRequestNumber, + GtkSelectionData* aSelectionData); + + virtual ~nsRetrievalContextWayland() override; + + private: + bool mInitialized; + RefPtr<mozilla::widget::nsWaylandDisplay> mDisplay; + + // Data offers provided by Wayland data device + GHashTable* mActiveOffers; + mozilla::UniquePtr<DataOffer> mClipboardOffer; + mozilla::UniquePtr<DataOffer> mPrimaryOffer; + RefPtr<nsWaylandDragContext> mDragContext; + + int mClipboardRequestNumber; + char* mClipboardData; + uint32_t mClipboardDataLength; + +// Mime types used for text data at Gtk+, see request_text_received_func() +// at gtkclipboard.c. +#define TEXT_MIME_TYPES_NUM 3 + static const char* sTextMimeTypes[TEXT_MIME_TYPES_NUM]; +}; + +#endif /* __nsClipboardWayland_h_ */ diff --git a/widget/gtk/nsClipboardX11.cpp b/widget/gtk/nsClipboardX11.cpp new file mode 100644 index 0000000000..0439c2e68c --- /dev/null +++ b/widget/gtk/nsClipboardX11.cpp @@ -0,0 +1,340 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "nsArrayUtils.h" +#include "nsClipboard.h" +#include "nsClipboardX11.h" +#include "nsSupportsPrimitives.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsPrimitiveHelpers.h" +#include "nsImageToPixbuf.h" +#include "nsStringStream.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" + +#include <gtk/gtk.h> + +// For manipulation of the X event queue +#include <X11/Xlib.h> +#include <poll.h> +#include <gdk/gdkx.h> +#include <sys/time.h> +#include <sys/types.h> +#include <errno.h> +#include <unistd.h> +#include "X11UndefineNone.h" + +using namespace mozilla; + +bool nsRetrievalContextX11::HasSelectionSupport(void) { + // yeah, unix supports the selection clipboard on X11. + return true; +} + +nsRetrievalContextX11::nsRetrievalContextX11() + : mState(INITIAL), + mClipboardRequestNumber(0), + mClipboardData(nullptr), + mClipboardDataLength(0), + mTargetMIMEType(gdk_atom_intern("TARGETS", FALSE)) {} + +static void DispatchSelectionNotifyEvent(GtkWidget* widget, XEvent* xevent) { + GdkEvent event = {}; + event.selection.type = GDK_SELECTION_NOTIFY; + event.selection.window = gtk_widget_get_window(widget); + event.selection.selection = + gdk_x11_xatom_to_atom(xevent->xselection.selection); + event.selection.target = gdk_x11_xatom_to_atom(xevent->xselection.target); + event.selection.property = gdk_x11_xatom_to_atom(xevent->xselection.property); + event.selection.time = xevent->xselection.time; + + gtk_widget_event(widget, &event); +} + +static void DispatchPropertyNotifyEvent(GtkWidget* widget, XEvent* xevent) { + GdkWindow* window = gtk_widget_get_window(widget); + if ((gdk_window_get_events(window)) & GDK_PROPERTY_CHANGE_MASK) { + GdkEvent event = {}; + event.property.type = GDK_PROPERTY_NOTIFY; + event.property.window = window; + event.property.atom = gdk_x11_xatom_to_atom(xevent->xproperty.atom); + event.property.time = xevent->xproperty.time; + event.property.state = xevent->xproperty.state; + + gtk_widget_event(widget, &event); + } +} + +struct checkEventContext { + GtkWidget* cbWidget; + Atom selAtom; +}; + +static Bool checkEventProc(Display* display, XEvent* event, XPointer arg) { + checkEventContext* context = (checkEventContext*)arg; + + if (event->xany.type == SelectionNotify || + (event->xany.type == PropertyNotify && + event->xproperty.atom == context->selAtom)) { + GdkWindow* cbWindow = gdk_x11_window_lookup_for_display( + gdk_x11_lookup_xdisplay(display), event->xany.window); + if (cbWindow) { + GtkWidget* cbWidget = nullptr; + gdk_window_get_user_data(cbWindow, (gpointer*)&cbWidget); + if (cbWidget && GTK_IS_WIDGET(cbWidget)) { + context->cbWidget = cbWidget; + return X11True; + } + } + } + + return X11False; +} + +bool nsRetrievalContextX11::WaitForX11Content() { + if (mState == COMPLETED) { // the request completed synchronously + return true; + } + + GdkDisplay* gdkDisplay = gdk_display_get_default(); + // gdk_display_get_default() returns null on headless + if (gdkDisplay && GDK_IS_X11_DISPLAY(gdkDisplay)) { + Display* xDisplay = GDK_DISPLAY_XDISPLAY(gdkDisplay); + checkEventContext context; + context.cbWidget = nullptr; + context.selAtom = + gdk_x11_atom_to_xatom(gdk_atom_intern("GDK_SELECTION", FALSE)); + + // Send X events which are relevant to the ongoing selection retrieval + // to the clipboard widget. Wait until either the operation completes, or + // we hit our timeout. All other X events remain queued. + + int poll_result; + + struct pollfd pfd; + pfd.fd = ConnectionNumber(xDisplay); + pfd.events = POLLIN; + TimeStamp start = TimeStamp::Now(); + + do { + XEvent xevent; + + while (XCheckIfEvent(xDisplay, &xevent, checkEventProc, + (XPointer)&context)) { + if (xevent.xany.type == SelectionNotify) + DispatchSelectionNotifyEvent(context.cbWidget, &xevent); + else + DispatchPropertyNotifyEvent(context.cbWidget, &xevent); + + if (mState == COMPLETED) { + return true; + } + } + + TimeStamp now = TimeStamp::Now(); + int timeout = std::max<int>( + 0, kClipboardTimeout / 1000 - (now - start).ToMilliseconds()); + poll_result = poll(&pfd, 1, timeout); + } while ((poll_result == 1 && (pfd.revents & (POLLHUP | POLLERR)) == 0) || + (poll_result == -1 && errno == EINTR)); + } +#ifdef DEBUG_CLIPBOARD + printf("exceeded clipboard timeout\n"); +#endif + mState = TIMED_OUT; + return false; +} + +// Call this when data has been retrieved. +void nsRetrievalContextX11::Complete(ClipboardDataType aDataType, + const void* aData, + int aDataRequestNumber) { + LOGCLIP(("nsRetrievalContextX11::Complete\n")); + + if (mClipboardRequestNumber != aDataRequestNumber) { + NS_WARNING( + "nsRetrievalContextX11::Complete() got obsoleted clipboard data."); + return; + } + + if (mState == INITIAL) { + mState = COMPLETED; + + MOZ_ASSERT(mClipboardData == nullptr && mClipboardDataLength == 0, + "We're leaking clipboard data!"); + + switch (aDataType) { + case CLIPBOARD_TEXT: { + const char* text = static_cast<const char*>(aData); + if (text) { + mClipboardDataLength = sizeof(char) * (strlen(text) + 1); + mClipboardData = moz_xmalloc(mClipboardDataLength); + memcpy(mClipboardData, text, mClipboardDataLength); + } + } break; + case CLIPBOARD_TARGETS: { + const GtkSelectionData* selection = + static_cast<const GtkSelectionData*>(aData); + + gint n_targets = 0; + GdkAtom* targets = nullptr; + + if (!gtk_selection_data_get_targets(selection, &targets, &n_targets) || + !n_targets) { + return; + } + + mClipboardData = targets; + mClipboardDataLength = n_targets; + } break; + case CLIPBOARD_DATA: { + const GtkSelectionData* selection = + static_cast<const GtkSelectionData*>(aData); + + gint dataLength = gtk_selection_data_get_length(selection); + if (dataLength > 0) { + mClipboardDataLength = dataLength; + mClipboardData = moz_xmalloc(dataLength); + memcpy(mClipboardData, gtk_selection_data_get_data(selection), + dataLength); + } + } break; + } + } else { + // Already timed out + MOZ_ASSERT(mState == TIMED_OUT); + } +} + +static void clipboard_contents_received(GtkClipboard* clipboard, + GtkSelectionData* selection_data, + gpointer data) { + int whichClipboard = GetGeckoClipboardType(clipboard); + LOGCLIP(("clipboard_contents_received (%s) callback\n", + whichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard")); + + ClipboardRequestHandler* handler = + static_cast<ClipboardRequestHandler*>(data); + handler->Complete(selection_data); + delete handler; +} + +static void clipboard_text_received(GtkClipboard* clipboard, const gchar* text, + gpointer data) { + int whichClipboard = GetGeckoClipboardType(clipboard); + LOGCLIP(("clipboard_text_received (%s) callback\n", + whichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard")); + + ClipboardRequestHandler* handler = + static_cast<ClipboardRequestHandler*>(data); + handler->Complete(text); + delete handler; +} + +bool nsRetrievalContextX11::WaitForClipboardData(ClipboardDataType aDataType, + GtkClipboard* clipboard, + const char* aMimeType) { + LOGCLIP(("nsRetrievalContextX11::WaitForClipboardData\n")); + + mState = INITIAL; + NS_ASSERTION(!mClipboardData, "Leaking clipboard content!"); + + // Call ClipboardRequestHandler() with unique clipboard request number. + // The request number pairs gtk_clipboard_request_contents() data request + // with clipboard_contents_received() callback where the data + // is provided by Gtk. + mClipboardRequestNumber++; + ClipboardRequestHandler* handler = + new ClipboardRequestHandler(this, aDataType, mClipboardRequestNumber); + + switch (aDataType) { + case CLIPBOARD_DATA: + gtk_clipboard_request_contents(clipboard, + gdk_atom_intern(aMimeType, FALSE), + clipboard_contents_received, handler); + break; + case CLIPBOARD_TEXT: + gtk_clipboard_request_text(clipboard, clipboard_text_received, handler); + break; + case CLIPBOARD_TARGETS: + gtk_clipboard_request_contents(clipboard, mTargetMIMEType, + clipboard_contents_received, handler); + break; + } + + return WaitForX11Content(); +} + +GdkAtom* nsRetrievalContextX11::GetTargets(int32_t aWhichClipboard, + int* aTargetNums) { + LOGCLIP(("nsRetrievalContextX11::GetTargets(%s)\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard")); + + GtkClipboard* clipboard = + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); + + if (!WaitForClipboardData(CLIPBOARD_TARGETS, clipboard)) { + LOGCLIP((" WaitForClipboardData() failed!\n")); + return nullptr; + } + + *aTargetNums = mClipboardDataLength; + GdkAtom* targets = static_cast<GdkAtom*>(mClipboardData); + + // We don't hold the target list internally but we transfer the ownership. + mClipboardData = nullptr; + mClipboardDataLength = 0; + + LOGCLIP((" returned %d targets\n", *aTargetNums)); + return targets; +} + +const char* nsRetrievalContextX11::GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard, + uint32_t* aContentLength) { + LOGCLIP(("nsRetrievalContextX11::GetClipboardData(%s)\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard")); + + GtkClipboard* clipboard; + clipboard = gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); + + if (!WaitForClipboardData(CLIPBOARD_DATA, clipboard, aMimeType)) + return nullptr; + + *aContentLength = mClipboardDataLength; + return static_cast<const char*>(mClipboardData); +} + +const char* nsRetrievalContextX11::GetClipboardText(int32_t aWhichClipboard) { + LOGCLIP(("nsRetrievalContextX11::GetClipboardText(%s)\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard")); + + GtkClipboard* clipboard; + clipboard = gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); + + if (!WaitForClipboardData(CLIPBOARD_TEXT, clipboard)) return nullptr; + + return static_cast<const char*>(mClipboardData); +} + +void nsRetrievalContextX11::ReleaseClipboardData(const char* aClipboardData) { + LOGCLIP(("nsRetrievalContextX11::ReleaseClipboardData\n")); + NS_ASSERTION(aClipboardData == mClipboardData, + "Releasing unknown clipboard data!"); + free((void*)aClipboardData); + + mClipboardData = nullptr; + mClipboardDataLength = 0; +} diff --git a/widget/gtk/nsClipboardX11.h b/widget/gtk/nsClipboardX11.h new file mode 100644 index 0000000000..2363111f74 --- /dev/null +++ b/widget/gtk/nsClipboardX11.h @@ -0,0 +1,73 @@ +/* -*- 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 __nsClipboardX11_h_ +#define __nsClipboardX11_h_ + +#include <gtk/gtk.h> + +enum ClipboardDataType { CLIPBOARD_DATA, CLIPBOARD_TEXT, CLIPBOARD_TARGETS }; + +class nsRetrievalContextX11 : public nsRetrievalContext { + public: + enum State { INITIAL, COMPLETED, TIMED_OUT }; + + virtual const char* GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard, + uint32_t* aContentLength) override; + virtual const char* GetClipboardText(int32_t aWhichClipboard) override; + virtual void ReleaseClipboardData(const char* aClipboardData) override; + + virtual GdkAtom* GetTargets(int32_t aWhichClipboard, + int* aTargetNums) override; + + virtual bool HasSelectionSupport(void) override; + + // Call this when data or text has been retrieved. + void Complete(ClipboardDataType aDataType, const void* aData, + int aDataRequestNumber); + + nsRetrievalContextX11(); + + private: + bool WaitForClipboardData(ClipboardDataType aDataType, + GtkClipboard* clipboard, + const char* aMimeType = nullptr); + + /** + * Spins X event loop until timing out or being completed. Returns + * null if we time out, otherwise returns the completed data (passing + * ownership to caller). + */ + bool WaitForX11Content(); + + State mState; + int mClipboardRequestNumber; + void* mClipboardData; + uint32_t mClipboardDataLength; + GdkAtom mTargetMIMEType; +}; + +class ClipboardRequestHandler { + public: + ClipboardRequestHandler(nsRetrievalContextX11* aContext, + ClipboardDataType aDataType, int aDataRequestNumber) + : mContext(aContext), + mDataRequestNumber(aDataRequestNumber), + mDataType(aDataType) {} + + void Complete(const void* aData) { + mContext->Complete(mDataType, aData, mDataRequestNumber); + } + + private: + nsRetrievalContextX11* mContext; + int mDataRequestNumber; + ClipboardDataType mDataType; +}; + +#endif /* __nsClipboardX11_h_ */ diff --git a/widget/gtk/nsColorPicker.cpp b/widget/gtk/nsColorPicker.cpp new file mode 100644 index 0000000000..21341e552f --- /dev/null +++ b/widget/gtk/nsColorPicker.cpp @@ -0,0 +1,242 @@ +/* -*- 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 <gtk/gtk.h> + +#include "nsColor.h" +#include "nsColorPicker.h" +#include "nsGtkUtils.h" +#include "nsIWidget.h" +#include "WidgetUtils.h" +#include "nsPIDOMWindow.h" + +NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker) + +#if defined(ACTIVATE_GTK3_COLOR_PICKER) +int nsColorPicker::convertGdkRgbaComponent(gdouble color_component) { + // GdkRGBA value is in range [0.0..1.0]. We need something in range [0..255] + return color_component * 255 + 0.5; +} + +gdouble nsColorPicker::convertToGdkRgbaComponent(int color_component) { + return color_component / 255.0; +} + +GdkRGBA nsColorPicker::convertToRgbaColor(nscolor color) { + GdkRGBA result = {convertToGdkRgbaComponent(NS_GET_R(color)), + convertToGdkRgbaComponent(NS_GET_G(color)), + convertToGdkRgbaComponent(NS_GET_B(color)), + convertToGdkRgbaComponent(NS_GET_A(color))}; + + return result; +} +#else +int nsColorPicker::convertGdkColorComponent(guint16 color_component) { + // GdkColor value is in range [0..65535]. We need something in range [0..255] + return (color_component * 255 + 127) / 65535; +} + +guint16 nsColorPicker::convertToGdkColorComponent(int color_component) { + return color_component * 65535 / 255; +} + +GdkColor nsColorPicker::convertToGdkColor(nscolor color) { + GdkColor result = {0 /* obsolete, unused 'pixel' value */, + convertToGdkColorComponent(NS_GET_R(color)), + convertToGdkColorComponent(NS_GET_G(color)), + convertToGdkColorComponent(NS_GET_B(color))}; + + return result; +} + +GtkColorSelection* nsColorPicker::WidgetGetColorSelection(GtkWidget* widget) { + return GTK_COLOR_SELECTION(gtk_color_selection_dialog_get_color_selection( + GTK_COLOR_SELECTION_DIALOG(widget))); +} +#endif + +NS_IMETHODIMP nsColorPicker::Init(mozIDOMWindowProxy* aParent, + const nsAString& title, + const nsAString& initialColor) { + auto* parent = nsPIDOMWindowOuter::From(aParent); + mParentWidget = mozilla::widget::WidgetUtils::DOMWindowToWidget(parent); + mTitle = title; + mInitialColor = initialColor; + + return NS_OK; +} + +NS_IMETHODIMP nsColorPicker::Open( + nsIColorPickerShownCallback* aColorPickerShownCallback) { + // Input color string should be 7 length (i.e. a string representing a valid + // simple color) + if (mInitialColor.Length() != 7) { + return NS_ERROR_FAILURE; + } + + const nsAString& withoutHash = StringTail(mInitialColor, 6); + nscolor color; + if (!NS_HexToRGBA(withoutHash, nsHexColorType::NoAlpha, &color)) { + return NS_ERROR_FAILURE; + } + + if (mCallback) { + // It means Open has already been called: this is not allowed + NS_WARNING("mCallback is already set. Open called twice?"); + return NS_ERROR_FAILURE; + } + mCallback = aColorPickerShownCallback; + + NS_ConvertUTF16toUTF8 title(mTitle); + GtkWindow* parent_window = + GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET)); + +#if defined(ACTIVATE_GTK3_COLOR_PICKER) + GtkWidget* color_chooser = gtk_color_chooser_dialog_new(title, parent_window); + + if (parent_window) { + GtkWindow* window = GTK_WINDOW(color_chooser); + gtk_window_set_destroy_with_parent(window, TRUE); + if (gtk_window_get_modal(parent_window)) { + gtk_window_set_modal(window, TRUE); + } + } + + gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(color_chooser), FALSE); + GdkRGBA color_rgba = convertToRgbaColor(color); + gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(color_chooser), &color_rgba); + + g_signal_connect(GTK_COLOR_CHOOSER(color_chooser), "color-activated", + G_CALLBACK(OnColorChanged), this); +#else + GtkWidget* color_chooser = gtk_color_selection_dialog_new(title.get()); + + if (parent_window) { + GtkWindow* window = GTK_WINDOW(color_chooser); + gtk_window_set_transient_for(window, parent_window); + gtk_window_set_destroy_with_parent(window, TRUE); + if (gtk_window_get_modal(parent_window)) { + gtk_window_set_modal(window, TRUE); + } + } + + GdkColor color_gdk = convertToGdkColor(color); + gtk_color_selection_set_current_color(WidgetGetColorSelection(color_chooser), + &color_gdk); + + g_signal_connect(WidgetGetColorSelection(color_chooser), "color-changed", + G_CALLBACK(OnColorChanged), this); +#endif + + NS_ADDREF_THIS(); + + g_signal_connect(color_chooser, "response", G_CALLBACK(OnResponse), this); + g_signal_connect(color_chooser, "destroy", G_CALLBACK(OnDestroy), this); + gtk_widget_show(color_chooser); + + return NS_OK; +} + +#if defined(ACTIVATE_GTK3_COLOR_PICKER) +/* static */ +void nsColorPicker::OnColorChanged(GtkColorChooser* color_chooser, + GdkRGBA* color, gpointer user_data) { + static_cast<nsColorPicker*>(user_data)->Update(color); +} + +void nsColorPicker::Update(GdkRGBA* color) { + SetColor(color); + if (mCallback) { + mCallback->Update(mColor); + } +} + +void nsColorPicker::SetColor(const GdkRGBA* color) { + mColor.Assign('#'); + mColor += ToHexString(convertGdkRgbaComponent(color->red)); + mColor += ToHexString(convertGdkRgbaComponent(color->green)); + mColor += ToHexString(convertGdkRgbaComponent(color->blue)); +} +#else +/* static */ +void nsColorPicker::OnColorChanged(GtkColorSelection* colorselection, + gpointer user_data) { + static_cast<nsColorPicker*>(user_data)->Update(colorselection); +} + +void nsColorPicker::Update(GtkColorSelection* colorselection) { + ReadValueFromColorSelection(colorselection); + if (mCallback) { + mCallback->Update(mColor); + } +} + +void nsColorPicker::ReadValueFromColorSelection( + GtkColorSelection* colorselection) { + GdkColor rgba; + gtk_color_selection_get_current_color(colorselection, &rgba); + + mColor.Assign('#'); + mColor += ToHexString(convertGdkColorComponent(rgba.red)); + mColor += ToHexString(convertGdkColorComponent(rgba.green)); + mColor += ToHexString(convertGdkColorComponent(rgba.blue)); +} +#endif + +/* static */ +void nsColorPicker::OnResponse(GtkWidget* color_chooser, gint response_id, + gpointer user_data) { + static_cast<nsColorPicker*>(user_data)->Done(color_chooser, response_id); +} + +/* static */ +void nsColorPicker::OnDestroy(GtkWidget* color_chooser, gpointer user_data) { + static_cast<nsColorPicker*>(user_data)->Done(color_chooser, + GTK_RESPONSE_CANCEL); +} + +void nsColorPicker::Done(GtkWidget* color_chooser, gint response) { + switch (response) { + case GTK_RESPONSE_OK: + case GTK_RESPONSE_ACCEPT: +#if defined(ACTIVATE_GTK3_COLOR_PICKER) + GdkRGBA color; + gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(color_chooser), &color); + SetColor(&color); +#else + ReadValueFromColorSelection(WidgetGetColorSelection(color_chooser)); +#endif + break; + case GTK_RESPONSE_CANCEL: + case GTK_RESPONSE_CLOSE: + case GTK_RESPONSE_DELETE_EVENT: + mColor = mInitialColor; + break; + default: + NS_WARNING("Unexpected response"); + break; + } + + // A "response" signal won't be sent again but "destroy" will be. + g_signal_handlers_disconnect_by_func(color_chooser, FuncToGpointer(OnDestroy), + this); + + gtk_widget_destroy(color_chooser); + if (mCallback) { + mCallback->Done(mColor); + mCallback = nullptr; + } + + NS_RELEASE_THIS(); +} + +nsString nsColorPicker::ToHexString(int n) { + nsString result; + if (n <= 0x0F) { + result.Append('0'); + } + result.AppendInt(n, 16); + return result; +} diff --git a/widget/gtk/nsColorPicker.h b/widget/gtk/nsColorPicker.h new file mode 100644 index 0000000000..c1f108f5c3 --- /dev/null +++ b/widget/gtk/nsColorPicker.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsColorPicker_h__ +#define nsColorPicker_h__ + +#include <gtk/gtk.h> + +#include "nsCOMPtr.h" +#include "nsIColorPicker.h" +#include "nsString.h" + +// Don't activate the GTK3 color picker for now, because it is missing a few +// things, mainly the ability to let the user select a color on the screen. +// See bug 1198256. +#undef ACTIVATE_GTK3_COLOR_PICKER + +class nsIWidget; + +class nsColorPicker final : public nsIColorPicker { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICOLORPICKER + + nsColorPicker() = default; + + private: + ~nsColorPicker() = default; + + static nsString ToHexString(int n); + + static void OnResponse(GtkWidget* dialog, gint response_id, + gpointer user_data); + static void OnDestroy(GtkWidget* dialog, gpointer user_data); + +#if defined(ACTIVATE_GTK3_COLOR_PICKER) + static void OnColorChanged(GtkColorChooser* color_chooser, GdkRGBA* color, + gpointer user_data); + + static int convertGdkRgbaComponent(gdouble color_component); + static gdouble convertToGdkRgbaComponent(int color_component); + static GdkRGBA convertToRgbaColor(nscolor color); + + void Update(GdkRGBA* color); + void SetColor(const GdkRGBA* color); +#else + static void OnColorChanged(GtkColorSelection* colorselection, + gpointer user_data); + + // Conversion functions for color + static int convertGdkColorComponent(guint16 color_component); + static guint16 convertToGdkColorComponent(int color_component); + static GdkColor convertToGdkColor(nscolor color); + + static GtkColorSelection* WidgetGetColorSelection(GtkWidget* widget); + + void Update(GtkColorSelection* colorselection); + void ReadValueFromColorSelection(GtkColorSelection* colorselection); +#endif + + void Done(GtkWidget* dialog, gint response_id); + + nsCOMPtr<nsIWidget> mParentWidget; + nsCOMPtr<nsIColorPickerShownCallback> mCallback; + nsString mTitle; + nsString mColor; + nsString mInitialColor; +}; + +#endif // nsColorPicker_h__ diff --git a/widget/gtk/nsDeviceContextSpecG.cpp b/widget/gtk/nsDeviceContextSpecG.cpp new file mode 100644 index 0000000000..631ce9d3d4 --- /dev/null +++ b/widget/gtk/nsDeviceContextSpecG.cpp @@ -0,0 +1,341 @@ +/* -*- 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 "nsDeviceContextSpecG.h" + +#include "mozilla/gfx/PrintTargetPDF.h" +#include "mozilla/gfx/PrintTargetPS.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" + +#include "plstr.h" +#include "prenv.h" /* for PR_GetEnv */ + +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsThreadUtils.h" + +#include "nsCUPSShim.h" +#include "nsPrinterCUPS.h" + +#include "nsPrintSettingsGTK.h" + +#include "nsIFileStreams.h" +#include "nsIFile.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_print.h" + +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> + +// To check if we need to use flatpak portal for printing +#include "nsIGIOService.h" + +using namespace mozilla; + +using mozilla::gfx::IntSize; +using mozilla::gfx::PrintTarget; +using mozilla::gfx::PrintTargetPDF; +using mozilla::gfx::PrintTargetPS; + +nsDeviceContextSpecGTK::nsDeviceContextSpecGTK() + : mGtkPrintSettings(nullptr), mGtkPageSetup(nullptr) {} + +nsDeviceContextSpecGTK::~nsDeviceContextSpecGTK() { + if (mGtkPageSetup) { + g_object_unref(mGtkPageSetup); + } + + if (mGtkPrintSettings) { + g_object_unref(mGtkPrintSettings); + } + + if (mSpoolFile) { + mSpoolFile->Remove(false); + } +} + +NS_IMPL_ISUPPORTS(nsDeviceContextSpecGTK, nsIDeviceContextSpec) + +already_AddRefed<PrintTarget> nsDeviceContextSpecGTK::MakePrintTarget() { + double width, height; + mPrintSettings->GetEffectiveSheetSize(&width, &height); + + // convert twips to points + width /= TWIPS_PER_POINT_FLOAT; + height /= TWIPS_PER_POINT_FLOAT; + + nsresult rv; + + // We shouldn't be attempting to get a surface if we've already got a spool + // file. + MOZ_ASSERT(!mSpoolFile); + + // Spool file. Use Glib's temporary file function since we're + // already dependent on the gtk software stack. + gchar* buf; + gint fd = g_file_open_tmp("XXXXXX.tmp", &buf, nullptr); + if (-1 == fd) return nullptr; + close(fd); + + rv = NS_NewNativeLocalFile(nsDependentCString(buf), false, + getter_AddRefs(mSpoolFile)); + if (NS_FAILED(rv)) { + unlink(buf); + g_free(buf); + return nullptr; + } + + mSpoolName = buf; + g_free(buf); + + mSpoolFile->SetPermissions(0600); + + nsCOMPtr<nsIFileOutputStream> stream = + do_CreateInstance("@mozilla.org/network/file-output-stream;1"); + rv = stream->Init(mSpoolFile, -1, -1, 0); + if (NS_FAILED(rv)) return nullptr; + + int16_t format; + mPrintSettings->GetOutputFormat(&format); + + // We assume PDF output if asked for native output. + if (format == nsIPrintSettings::kOutputFormatNative) { + format = nsIPrintSettings::kOutputFormatPDF; + } + + IntSize size = IntSize::Ceil(width, height); + if (format == nsIPrintSettings::kOutputFormatPDF) { + return PrintTargetPDF::CreateOrNull(stream, size); + } + + int32_t orientation = mPrintSettings->GetSheetOrientation(); + return PrintTargetPS::CreateOrNull( + stream, size, + orientation == nsIPrintSettings::kPortraitOrientation + ? PrintTargetPS::PORTRAIT + : PrintTargetPS::LANDSCAPE); +} + +#define DECLARE_KNOWN_MONOCHROME_SETTING(key_, value_) {"cups-" key_, value_}, + +struct { + const char* mKey; + const char* mValue; +} kKnownMonochromeSettings[] = { + CUPS_EACH_MONOCHROME_PRINTER_SETTING(DECLARE_KNOWN_MONOCHROME_SETTING)}; + +#undef DECLARE_KNOWN_MONOCHROME_SETTING + +/** ------------------------------------------------------- + * Initialize the nsDeviceContextSpecGTK + * @update dc 2/15/98 + * @update syd 3/2/99 + */ +NS_IMETHODIMP nsDeviceContextSpecGTK::Init(nsIWidget* aWidget, + nsIPrintSettings* aPS, + bool aIsPrintPreview) { + if (gtk_major_version < 2 || + (gtk_major_version == 2 && gtk_minor_version < 10)) + return NS_ERROR_NOT_AVAILABLE; // I'm so sorry bz + + mPrintSettings = do_QueryInterface(aPS); + if (!mPrintSettings) return NS_ERROR_NO_INTERFACE; + + // This is only set by embedders + bool toFile; + aPS->GetPrintToFile(&toFile); + + mToPrinter = !toFile && !aIsPrintPreview; + + mGtkPrintSettings = mPrintSettings->GetGtkPrintSettings(); + mGtkPageSetup = mPrintSettings->GetGtkPageSetup(); + + // This is a horrible workaround for some printer driver bugs that treat + // custom page sizes different to standard ones. If our paper object matches + // one of a standard one, use a standard paper size object instead. See bug + // 414314 for more info. + GtkPaperSize* geckosHackishPaperSize = + gtk_page_setup_get_paper_size(mGtkPageSetup); + GtkPaperSize* standardGtkPaperSize = + gtk_paper_size_new(gtk_paper_size_get_name(geckosHackishPaperSize)); + + mGtkPageSetup = gtk_page_setup_copy(mGtkPageSetup); + mGtkPrintSettings = gtk_print_settings_copy(mGtkPrintSettings); + + if (!aPS->GetPrintInColor() && StaticPrefs::print_cups_monochrome_enabled()) { + for (const auto& setting : kKnownMonochromeSettings) { + gtk_print_settings_set(mGtkPrintSettings, setting.mKey, setting.mValue); + } + auto applySetting = [&](const nsACString& aKey, const nsACString& aVal) { + nsAutoCString extra; + extra.AppendASCII("cups-"); + extra.Append(aKey); + gtk_print_settings_set(mGtkPrintSettings, extra.get(), + nsAutoCString(aVal).get()); + }; + nsPrinterCUPS::ForEachExtraMonochromeSetting(applySetting); + } + + GtkPaperSize* properPaperSize; + if (gtk_paper_size_is_equal(geckosHackishPaperSize, standardGtkPaperSize)) { + properPaperSize = standardGtkPaperSize; + } else { + properPaperSize = geckosHackishPaperSize; + } + gtk_print_settings_set_paper_size(mGtkPrintSettings, properPaperSize); + gtk_page_setup_set_paper_size_and_default_margins(mGtkPageSetup, + properPaperSize); + gtk_paper_size_free(standardGtkPaperSize); + + return NS_OK; +} + +static void print_callback(GtkPrintJob* aJob, gpointer aData, + const GError* aError) { + g_object_unref(aJob); + ((nsIFile*)aData)->Remove(false); +} + +/* static */ +gboolean nsDeviceContextSpecGTK::PrinterEnumerator(GtkPrinter* aPrinter, + gpointer aData) { + nsDeviceContextSpecGTK* spec = (nsDeviceContextSpecGTK*)aData; + + // Find the printer whose name matches the one inside the settings. + nsString printerName; + nsresult rv = spec->mPrintSettings->GetPrinterName(printerName); + if (NS_SUCCEEDED(rv) && !printerName.IsVoid()) { + NS_ConvertUTF16toUTF8 requestedName(printerName); + const char* currentName = gtk_printer_get_name(aPrinter); + if (requestedName.Equals(currentName)) { + spec->mPrintSettings->SetGtkPrinter(aPrinter); + + // Bug 1145916 - attempting to kick off a print job for this printer + // during this tick of the event loop will result in the printer backend + // misunderstanding what the capabilities of the printer are due to a + // GTK bug (https://bugzilla.gnome.org/show_bug.cgi?id=753041). We + // sidestep this by deferring the print to the next tick. + NS_DispatchToCurrentThread( + NewRunnableMethod("nsDeviceContextSpecGTK::StartPrintJob", spec, + &nsDeviceContextSpecGTK::StartPrintJob)); + return TRUE; + } + } + + // We haven't found it yet - keep searching... + return FALSE; +} + +void nsDeviceContextSpecGTK::StartPrintJob() { + GtkPrintJob* job = + gtk_print_job_new(mTitle.get(), mPrintSettings->GetGtkPrinter(), + mGtkPrintSettings, mGtkPageSetup); + + if (!gtk_print_job_set_source_file(job, mSpoolName.get(), nullptr)) return; + + // Now gtk owns the print job, and will be released via our callback. + gtk_print_job_send(job, print_callback, mSpoolFile.forget().take(), + [](gpointer aData) { + auto* spoolFile = static_cast<nsIFile*>(aData); + NS_RELEASE(spoolFile); + }); +} + +void nsDeviceContextSpecGTK::EnumeratePrinters() { + gtk_enumerate_printers(&nsDeviceContextSpecGTK::PrinterEnumerator, this, + nullptr, TRUE); +} + +NS_IMETHODIMP +nsDeviceContextSpecGTK::BeginDocument(const nsAString& aTitle, + const nsAString& aPrintToFileName, + int32_t aStartPage, int32_t aEndPage) { + // Print job names exceeding 255 bytes are safe with GTK version 3.18.2 or + // newer. This is a workaround for old GTK. + if (gtk_check_version(3, 18, 2) != nullptr) { + PrintTarget::AdjustPrintJobNameForIPP(aTitle, mTitle); + } else { + CopyUTF16toUTF8(aTitle, mTitle); + } + + return NS_OK; +} + +NS_IMETHODIMP nsDeviceContextSpecGTK::EndDocument() { + if (mToPrinter) { + // At this point, we might have a GtkPrinter set up in nsPrintSettingsGTK, + // or we might not. In the single-process case, we probably will, as this + // is populated by the print settings dialog, or set to the default + // printer. + // In the multi-process case, we proxy the print settings dialog over to + // the parent process, and only get the name of the printer back on the + // content process side. In that case, we need to enumerate the printers + // on the content side, and find a printer with a matching name. + + if (mPrintSettings->GetGtkPrinter()) { + // We have a printer, so we can print right away. + StartPrintJob(); + } else { + // We don't have a printer. We have to enumerate the printers and find + // one with a matching name. + NS_DispatchToCurrentThread( + NewRunnableMethod("nsDeviceContextSpecGTK::EnumeratePrinters", this, + &nsDeviceContextSpecGTK::EnumeratePrinters)); + } + } else { + // Handle print-to-file ourselves for the benefit of embedders + nsString targetPath; + nsCOMPtr<nsIFile> destFile; + mPrintSettings->GetToFileName(targetPath); + + nsresult rv = NS_NewLocalFile(targetPath, false, getter_AddRefs(destFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString destLeafName; + rv = destFile->GetLeafName(destLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> destDir; + rv = destFile->GetParent(getter_AddRefs(destDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSpoolFile->MoveTo(destDir, destLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + mSpoolFile = nullptr; + + // This is the standard way to get the UNIX umask. Ugh. + mode_t mask = umask(0); + umask(mask); + // If you're not familiar with umasks, they contain the bits of what NOT + // to set in the permissions (thats because files and directories have + // different numbers of bits for their permissions) + destFile->SetPermissions(0666 & ~(mask)); + + // Notify flatpak printing portal that file is completely written + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + bool shouldUsePortal; + if (giovfs) { + giovfs->ShouldUseFlatpakPortal(&shouldUsePortal); + if (shouldUsePortal) { + // Use the name of the file for printing to match with + // nsFlatpakPrintPortal + nsCOMPtr<nsIObserverService> os = + mozilla::services::GetObserverService(); + // Pass filename to be sure that observer process the right data + os->NotifyObservers(nullptr, "print-to-file-finished", + targetPath.get()); + } + } + } + return NS_OK; +} diff --git a/widget/gtk/nsDeviceContextSpecG.h b/widget/gtk/nsDeviceContextSpecG.h new file mode 100644 index 0000000000..e3b0537782 --- /dev/null +++ b/widget/gtk/nsDeviceContextSpecG.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDeviceContextSpecGTK_h___ +#define nsDeviceContextSpecGTK_h___ + +struct JSContext; + +#include "nsIDeviceContextSpec.h" +#include "nsIPrinterList.h" +#include "nsIPrintSettings.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "mozilla/Attributes.h" + +#include "nsCRT.h" /* should be <limits.h>? */ + +#include <gtk/gtk.h> +#include <gtk/gtkunixprint.h> + +#define NS_PORTRAIT 0 +#define NS_LANDSCAPE 1 + +class nsPrintSettingsGTK; + +class nsDeviceContextSpecGTK : public nsIDeviceContextSpec { + public: + nsDeviceContextSpecGTK(); + + NS_DECL_ISUPPORTS + + already_AddRefed<PrintTarget> MakePrintTarget() final; + + NS_IMETHOD Init(nsIWidget* aWidget, nsIPrintSettings* aPS, + bool aIsPrintPreview) override; + NS_IMETHOD BeginDocument(const nsAString& aTitle, + const nsAString& aPrintToFileName, + int32_t aStartPage, int32_t aEndPage) override; + NS_IMETHOD EndDocument() override; + NS_IMETHOD BeginPage() override { return NS_OK; } + NS_IMETHOD EndPage() override { return NS_OK; } + + protected: + virtual ~nsDeviceContextSpecGTK(); + nsCOMPtr<nsPrintSettingsGTK> mPrintSettings; + bool mToPrinter : 1; /* If true, print to printer */ + GtkPrintSettings* mGtkPrintSettings; + GtkPageSetup* mGtkPageSetup; + + nsCString mSpoolName; + nsCOMPtr<nsIFile> mSpoolFile; + nsCString mTitle; + + private: + void EnumeratePrinters(); + void StartPrintJob(); + static gboolean PrinterEnumerator(GtkPrinter* aPrinter, gpointer aData); +}; + +#endif /* !nsDeviceContextSpecGTK_h___ */ diff --git a/widget/gtk/nsDragService.cpp b/widget/gtk/nsDragService.cpp new file mode 100644 index 0000000000..7d57c35df9 --- /dev/null +++ b/widget/gtk/nsDragService.cpp @@ -0,0 +1,2122 @@ +/* -*- 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 "nsDragService.h" +#include "nsArrayUtils.h" +#include "nsIObserverService.h" +#include "nsWidgetsCID.h" +#include "nsWindow.h" +#include "nsSystemInfo.h" +#include "nsXPCOM.h" +#include "nsICookieJarSettings.h" +#include "nsISupportsPrimitives.h" +#include "nsIIOService.h" +#include "nsIFileURL.h" +#include "nsNetUtil.h" +#include "mozilla/Logging.h" +#include "nsTArray.h" +#include "nsPrimitiveHelpers.h" +#include "prtime.h" +#include "prthread.h" +#include <dlfcn.h> +#include <gtk/gtk.h> +#include <gdk/gdkx.h> +#include "nsCRT.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Services.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "GRefPtr.h" + +#include "gfxXlibSurface.h" +#include "gfxContext.h" +#include "nsImageToPixbuf.h" +#include "nsPresContext.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "nsViewManager.h" +#include "nsIFrame.h" +#include "nsGtkUtils.h" +#include "nsGtkKeyUtils.h" +#include "mozilla/gfx/2D.h" +#include "gfxPlatform.h" +#include "ScreenHelperGTK.h" +#include "nsArrayUtils.h" +#ifdef MOZ_WAYLAND +# include "nsClipboardWayland.h" +# include "gfxPlatformGtk.h" +#endif + +using namespace mozilla; +using namespace mozilla::gfx; + +#define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1" + +// This sets how opaque the drag image is +#define DRAG_IMAGE_ALPHA_LEVEL 0.5 + +// These values are copied from GtkDragResult (rather than using GtkDragResult +// directly) so that this code can be compiled against versions of GTK+ that +// do not have GtkDragResult. +// GtkDragResult is available from GTK+ version 2.12. +enum { + MOZ_GTK_DRAG_RESULT_SUCCESS, + MOZ_GTK_DRAG_RESULT_NO_TARGET, + MOZ_GTK_DRAG_RESULT_USER_CANCELLED, + MOZ_GTK_DRAG_RESULT_TIMEOUT_EXPIRED, + MOZ_GTK_DRAG_RESULT_GRAB_BROKEN, + MOZ_GTK_DRAG_RESULT_ERROR +}; + +static LazyLogModule sDragLm("nsDragService"); + +// data used for synthetic periodic motion events sent to the source widget +// grabbing real events for the drag. +static guint sMotionEventTimerID; +static GdkEvent* sMotionEvent; +static GtkWidget* sGrabWidget; + +static const char gMimeListType[] = "application/x-moz-internal-item-list"; +static const char gMozUrlType[] = "_NETSCAPE_URL"; +static const char gTextUriListType[] = "text/uri-list"; +static const char gTextPlainUTF8Type[] = "text/plain;charset=utf-8"; +static const char gXdndDirectSaveType[] = "XdndDirectSave0"; +static const char gTabDropType[] = "application/x-moz-tabbrowser-tab"; + +static void invisibleSourceDragBegin(GtkWidget* aWidget, + GdkDragContext* aContext, gpointer aData); + +static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext, + gpointer aData); + +static gboolean invisibleSourceDragFailed(GtkWidget* aWidget, + GdkDragContext* aContext, + gint aResult, gpointer aData); + +static void invisibleSourceDragDataGet(GtkWidget* aWidget, + GdkDragContext* aContext, + GtkSelectionData* aSelectionData, + guint aInfo, guint32 aTime, + gpointer aData); + +nsDragService::nsDragService() + : mScheduledTask(eDragTaskNone), + mTaskSource(0) +#ifdef MOZ_WAYLAND + , + mPendingWaylandDragContext(nullptr), + mTargetWaylandDragContext(nullptr) +#endif +{ + // We have to destroy the hidden widget before the event loop stops + // running. + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + obsServ->AddObserver(this, "quit-application", false); + + // our hidden source widget + // Using an offscreen window works around bug 983843. + mHiddenWidget = gtk_offscreen_window_new(); + // make sure that the widget is realized so that + // we can use it as a drag source. + gtk_widget_realize(mHiddenWidget); + // hook up our internal signals so that we can get some feedback + // from our drag source + g_signal_connect(mHiddenWidget, "drag_begin", + G_CALLBACK(invisibleSourceDragBegin), this); + g_signal_connect(mHiddenWidget, "drag_data_get", + G_CALLBACK(invisibleSourceDragDataGet), this); + g_signal_connect(mHiddenWidget, "drag_end", + G_CALLBACK(invisibleSourceDragEnd), this); + // drag-failed is available from GTK+ version 2.12 + guint dragFailedID = + g_signal_lookup("drag-failed", G_TYPE_FROM_INSTANCE(mHiddenWidget)); + if (dragFailedID) { + g_signal_connect_closure_by_id( + mHiddenWidget, dragFailedID, 0, + g_cclosure_new(G_CALLBACK(invisibleSourceDragFailed), this, nullptr), + FALSE); + } + + // set up our logging module + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::nsDragService")); + mCanDrop = false; + mTargetDragDataReceived = false; + mTargetDragData = 0; + mTargetDragDataLen = 0; +} + +nsDragService::~nsDragService() { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::~nsDragService")); + if (mTaskSource) g_source_remove(mTaskSource); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsDragService, nsBaseDragService, nsIObserver) + +mozilla::StaticRefPtr<nsDragService> sDragServiceInstance; +/* static */ +already_AddRefed<nsDragService> nsDragService::GetInstance() { + if (gfxPlatform::IsHeadless()) { + return nullptr; + } + if (!sDragServiceInstance) { + sDragServiceInstance = new nsDragService(); + ClearOnShutdown(&sDragServiceInstance); + } + + RefPtr<nsDragService> service = sDragServiceInstance.get(); + return service.forget(); +} + +// nsIObserver + +NS_IMETHODIMP +nsDragService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!nsCRT::strcmp(aTopic, "quit-application")) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("nsDragService::Observe(\"quit-application\")")); + if (mHiddenWidget) { + gtk_widget_destroy(mHiddenWidget); + mHiddenWidget = 0; + } + TargetResetData(); + } else { + MOZ_ASSERT_UNREACHABLE("unexpected topic"); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +// Support for periodic drag events + +// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model +// and the Xdnd protocol both recommend that drag events are sent periodically, +// but GTK does not normally provide this. +// +// Here GTK is periodically stimulated by copies of the most recent mouse +// motion events so as to send drag position messages to the destination when +// appropriate (after it has received a status event from the previous +// message). +// +// (If events were sent only on the destination side then the destination +// would have no message to which it could reply with a drag status. Without +// sending a drag status to the source, the destination would not be able to +// change its feedback re whether it could accept the drop, and so the +// source's behavior on drop will not be consistent.) + +static gboolean DispatchMotionEventCopy(gpointer aData) { + // Clear the timer id before OnSourceGrabEventAfter is called during event + // dispatch. + sMotionEventTimerID = 0; + + GdkEvent* event = sMotionEvent; + sMotionEvent = nullptr; + // If there is no longer a grab on the widget, then the drag is over and + // there is no need to continue drag motion. + if (gtk_widget_has_grab(sGrabWidget)) { + gtk_propagate_event(sGrabWidget, event); + } + gdk_event_free(event); + + // Cancel this timer; + // We've already started another if the motion event was dispatched. + return FALSE; +} + +static void OnSourceGrabEventAfter(GtkWidget* widget, GdkEvent* event, + gpointer user_data) { + // If there is no longer a grab on the widget, then the drag motion is + // over (though the data may not be fetched yet). + if (!gtk_widget_has_grab(sGrabWidget)) return; + + if (event->type == GDK_MOTION_NOTIFY) { + if (sMotionEvent) { + gdk_event_free(sMotionEvent); + } + sMotionEvent = gdk_event_copy(event); + + // Update the cursor position. The last of these recorded gets used for + // the eDragEnd event. + nsDragService* dragService = static_cast<nsDragService*>(user_data); + gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor(); + auto p = LayoutDeviceIntPoint::Round(event->motion.x_root * scale, + event->motion.y_root * scale); + dragService->SetDragEndPoint(p); + } else if (sMotionEvent && + (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) { + // Update modifier state from key events. + sMotionEvent->motion.state = event->key.state; + } else { + return; + } + + if (sMotionEventTimerID) { + g_source_remove(sMotionEventTimerID); + } + + // G_PRIORITY_DEFAULT_IDLE is lower priority than GDK's redraw idle source + // and lower than GTK's idle source that sends drag position messages after + // motion-notify signals. + // + // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model + // recommends an interval of 350ms +/- 200ms. + sMotionEventTimerID = g_timeout_add_full( + G_PRIORITY_DEFAULT_IDLE, 350, DispatchMotionEventCopy, nullptr, nullptr); +} + +static GtkWindow* GetGtkWindow(dom::Document* aDocument) { + if (!aDocument) return nullptr; + + PresShell* presShell = aDocument->GetPresShell(); + if (!presShell) { + return nullptr; + } + + RefPtr<nsViewManager> vm = presShell->GetViewManager(); + if (!vm) return nullptr; + + nsCOMPtr<nsIWidget> widget; + vm->GetRootWidget(getter_AddRefs(widget)); + if (!widget) return nullptr; + + GtkWidget* gtkWidget = + static_cast<nsWindow*>(widget.get())->GetMozContainerWidget(); + if (!gtkWidget) return nullptr; + + GtkWidget* toplevel = nullptr; + toplevel = gtk_widget_get_toplevel(gtkWidget); + if (!GTK_IS_WINDOW(toplevel)) return nullptr; + + return GTK_WINDOW(toplevel); +} + +// nsIDragService + +NS_IMETHODIMP +nsDragService::InvokeDragSession( + nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp, + nsICookieJarSettings* aCookieJarSettings, nsIArray* aArrayTransferables, + uint32_t aActionType, + nsContentPolicyType aContentPolicyType = nsIContentPolicy::TYPE_OTHER) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::InvokeDragSession")); + + // If the previous source drag has not yet completed, signal handlers need + // to be removed from sGrabWidget and dragend needs to be dispatched to + // the source node, but we can't call EndDragSession yet because we don't + // know whether or not the drag succeeded. + if (mSourceNode) return NS_ERROR_NOT_AVAILABLE; + + return nsBaseDragService::InvokeDragSession( + aDOMNode, aPrincipal, aCsp, aCookieJarSettings, aArrayTransferables, + aActionType, aContentPolicyType); +} + +// nsBaseDragService +nsresult nsDragService::InvokeDragSessionImpl( + nsIArray* aArrayTransferables, const Maybe<CSSIntRegion>& aRegion, + uint32_t aActionType) { + // make sure that we have an array of transferables to use + if (!aArrayTransferables) return NS_ERROR_INVALID_ARG; + // set our reference to the transferables. this will also addref + // the transferables since we're going to hang onto this beyond the + // length of this call + mSourceDataItems = aArrayTransferables; + // get the list of items we offer for drags + GtkTargetList* sourceList = GetSourceList(); + + if (!sourceList) return NS_OK; + + // save our action type + GdkDragAction action = GDK_ACTION_DEFAULT; + + if (aActionType & DRAGDROP_ACTION_COPY) + action = (GdkDragAction)(action | GDK_ACTION_COPY); + if (aActionType & DRAGDROP_ACTION_MOVE) + action = (GdkDragAction)(action | GDK_ACTION_MOVE); + if (aActionType & DRAGDROP_ACTION_LINK) + action = (GdkDragAction)(action | GDK_ACTION_LINK); + + // Create a fake event for the drag so we can pass the time (so to speak). + // If we don't do this, then, when the timestamp for the pending button + // release event is used for the ungrab, the ungrab can fail due to the + // timestamp being _earlier_ than CurrentTime. + GdkEvent event; + memset(&event, 0, sizeof(GdkEvent)); + event.type = GDK_BUTTON_PRESS; + event.button.window = gtk_widget_get_window(mHiddenWidget); + event.button.time = nsWindow::GetLastUserInputTime(); + + // Put the drag widget in the window group of the source node so that the + // gtk_grab_add during gtk_drag_begin is effective. + // gtk_window_get_group(nullptr) returns the default window group. + GtkWindowGroup* window_group = + gtk_window_get_group(GetGtkWindow(mSourceDocument)); + gtk_window_group_add_window(window_group, GTK_WINDOW(mHiddenWidget)); + + // Get device for event source + GdkDisplay* display = gdk_display_get_default(); + GdkDeviceManager* device_manager = gdk_display_get_device_manager(display); + event.button.device = gdk_device_manager_get_client_pointer(device_manager); + + // start our drag. + GdkDragContext* context = + gtk_drag_begin(mHiddenWidget, sourceList, action, 1, &event); + + nsresult rv; + if (context) { + StartDragSession(); + + // GTK uses another hidden window for receiving mouse events. + sGrabWidget = gtk_window_group_get_current_grab(window_group); + if (sGrabWidget) { + g_object_ref(sGrabWidget); + // Only motion and key events are required but connect to + // "event-after" as this is never blocked by other handlers. + g_signal_connect(sGrabWidget, "event-after", + G_CALLBACK(OnSourceGrabEventAfter), this); + } + // We don't have a drag end point yet. + mEndDragPoint = LayoutDeviceIntPoint(-1, -1); + rv = NS_OK; + } else { + rv = NS_ERROR_FAILURE; + } + + gtk_target_list_unref(sourceList); + + return rv; +} + +bool nsDragService::SetAlphaPixmap(SourceSurface* aSurface, + GdkDragContext* aContext, int32_t aXOffset, + int32_t aYOffset, + const LayoutDeviceIntRect& dragRect) { + GdkScreen* screen = gtk_widget_get_screen(mHiddenWidget); + + // Transparent drag icons need, like a lot of transparency-related things, + // a compositing X window manager + if (!gdk_screen_is_composited(screen)) return false; + +#ifdef cairo_image_surface_create +# error "Looks like we're including Mozilla's cairo instead of system cairo" +#endif + + // TODO: grab X11 pixmap or image data instead of expensive readback. + cairo_surface_t* surf = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, dragRect.width, dragRect.height); + if (!surf) return false; + + RefPtr<DrawTarget> dt = gfxPlatform::CreateDrawTargetForData( + cairo_image_surface_get_data(surf), + nsIntSize(dragRect.width, dragRect.height), + cairo_image_surface_get_stride(surf), SurfaceFormat::B8G8R8A8); + if (!dt) return false; + + dt->ClearRect(Rect(0, 0, dragRect.width, dragRect.height)); + dt->DrawSurface( + aSurface, Rect(0, 0, dragRect.width, dragRect.height), + Rect(0, 0, dragRect.width, dragRect.height), DrawSurfaceOptions(), + DrawOptions(DRAG_IMAGE_ALPHA_LEVEL, CompositionOp::OP_SOURCE)); + + cairo_surface_mark_dirty(surf); + cairo_surface_set_device_offset(surf, -aXOffset, -aYOffset); + + // Ensure that the surface is drawn at the correct scale on HiDPI displays. + static auto sCairoSurfaceSetDeviceScalePtr = + (void (*)(cairo_surface_t*, double, double))dlsym( + RTLD_DEFAULT, "cairo_surface_set_device_scale"); + if (sCairoSurfaceSetDeviceScalePtr) { + gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor(); + sCairoSurfaceSetDeviceScalePtr(surf, scale, scale); + } + + gtk_drag_set_icon_surface(aContext, surf); + cairo_surface_destroy(surf); + return true; +} + +NS_IMETHODIMP +nsDragService::StartDragSession() { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::StartDragSession")); + return nsBaseDragService::StartDragSession(); +} + +NS_IMETHODIMP +nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("nsDragService::EndDragSession %d", aDoneDrag)); + + if (sGrabWidget) { + g_signal_handlers_disconnect_by_func( + sGrabWidget, FuncToGpointer(OnSourceGrabEventAfter), this); + g_object_unref(sGrabWidget); + sGrabWidget = nullptr; + + if (sMotionEventTimerID) { + g_source_remove(sMotionEventTimerID); + sMotionEventTimerID = 0; + } + if (sMotionEvent) { + gdk_event_free(sMotionEvent); + sMotionEvent = nullptr; + } + } + + // unset our drag action + SetDragAction(DRAGDROP_ACTION_NONE); + + // We're done with the drag context. + mTargetDragContextForRemote = nullptr; +#ifdef MOZ_WAYLAND + mTargetWaylandDragContextForRemote = nullptr; +#endif + + return nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers); +} + +// nsIDragSession +NS_IMETHODIMP +nsDragService::SetCanDrop(bool aCanDrop) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::SetCanDrop %d", aCanDrop)); + mCanDrop = aCanDrop; + return NS_OK; +} + +NS_IMETHODIMP +nsDragService::GetCanDrop(bool* aCanDrop) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::GetCanDrop")); + *aCanDrop = mCanDrop; + return NS_OK; +} + +static void UTF16ToNewUTF8(const char16_t* aUTF16, uint32_t aUTF16Len, + char** aUTF8, uint32_t* aUTF8Len) { + nsDependentSubstring utf16(aUTF16, aUTF16Len); + *aUTF8 = ToNewUTF8String(utf16, aUTF8Len); +} + +static void UTF8ToNewUTF16(const char* aUTF8, uint32_t aUTF8Len, + char16_t** aUTF16, uint32_t* aUTF16Len) { + nsDependentCSubstring utf8(aUTF8, aUTF8Len); + *aUTF16 = UTF8ToNewUnicode(utf8, aUTF16Len); +} + +// count the number of URIs in some text/uri-list format data. +static uint32_t CountTextUriListItems(const char* data, uint32_t datalen) { + const char* p = data; + const char* endPtr = p + datalen; + uint32_t count = 0; + + while (p < endPtr) { + // skip whitespace (if any) + while (p < endPtr && *p != '\0' && isspace(*p)) p++; + // if we aren't at the end of the line ... + if (p != endPtr && *p != '\0' && *p != '\n' && *p != '\r') count++; + // skip to the end of the line + while (p < endPtr && *p != '\0' && *p != '\n') p++; + p++; // skip the actual newline as well. + } + return count; +} + +// extract an item from text/uri-list formatted data and convert it to +// unicode. +static void GetTextUriListItem(const char* data, uint32_t datalen, + uint32_t aItemIndex, char16_t** convertedText, + uint32_t* convertedTextLen) { + const char* p = data; + const char* endPtr = p + datalen; + unsigned int count = 0; + + *convertedText = nullptr; + while (p < endPtr) { + // skip whitespace (if any) + while (p < endPtr && *p != '\0' && isspace(*p)) p++; + // if we aren't at the end of the line, we have a url + if (p != endPtr && *p != '\0' && *p != '\n' && *p != '\r') count++; + // this is the item we are after ... + if (aItemIndex + 1 == count) { + const char* q = p; + while (q < endPtr && *q != '\0' && *q != '\n' && *q != '\r') q++; + UTF8ToNewUTF16(p, q - p, convertedText, convertedTextLen); + break; + } + // skip to the end of the line + while (p < endPtr && *p != '\0' && *p != '\n') p++; + p++; // skip the actual newline as well. + } + + // didn't find the desired item, so just pass the whole lot + if (!*convertedText) { + UTF8ToNewUTF16(data, datalen, convertedText, convertedTextLen); + } +} + +NS_IMETHODIMP +nsDragService::GetNumDropItems(uint32_t* aNumItems) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::GetNumDropItems")); + + if (!mTargetWidget) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("*** warning: GetNumDropItems \ + called without a valid target widget!\n")); + *aNumItems = 0; + return NS_OK; + } + + bool isList = IsTargetContextList(); + if (isList) + mSourceDataItems->GetLength(aNumItems); + else { + GdkAtom gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE); + GetTargetDragData(gdkFlavor); + if (mTargetDragData) { + const char* data = reinterpret_cast<char*>(mTargetDragData); + *aNumItems = CountTextUriListItems(data, mTargetDragDataLen); + } else + *aNumItems = 1; + } + MOZ_LOG(sDragLm, LogLevel::Debug, ("%d items", *aNumItems)); + return NS_OK; +} + +NS_IMETHODIMP +nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::GetData %d", aItemIndex)); + + // make sure that we have a transferable + if (!aTransferable) return NS_ERROR_INVALID_ARG; + + if (!mTargetWidget) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("*** warning: GetData \ + called without a valid target widget!\n")); + return NS_ERROR_FAILURE; + } + + // get flavor list that includes all acceptable flavors (including + // ones obtained through conversion). + nsTArray<nsCString> flavors; + nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); + if (NS_FAILED(rv)) return rv; + + // check to see if this is an internal list + bool isList = IsTargetContextList(); + + if (isList) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("it's a list...")); + // find a matching flavor + for (uint32_t i = 0; i < flavors.Length(); ++i) { + nsCString& flavorStr = flavors[i]; + MOZ_LOG(sDragLm, LogLevel::Debug, ("flavor is %s\n", flavorStr.get())); + // get the item with the right index + nsCOMPtr<nsITransferable> item = + do_QueryElementAt(mSourceDataItems, aItemIndex); + if (!item) continue; + + nsCOMPtr<nsISupports> data; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("trying to get transfer data for %s\n", flavorStr.get())); + rv = item->GetTransferData(flavorStr.get(), getter_AddRefs(data)); + if (NS_FAILED(rv)) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("failed.\n")); + continue; + } + MOZ_LOG(sDragLm, LogLevel::Debug, ("succeeded.\n")); + rv = aTransferable->SetTransferData(flavorStr.get(), data); + if (NS_FAILED(rv)) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("fail to set transfer data into transferable!\n")); + continue; + } + // ok, we got the data + return NS_OK; + } + // if we got this far, we failed + return NS_ERROR_FAILURE; + } + + // Now walk down the list of flavors. When we find one that is + // actually present, copy out the data into the transferable in that + // format. SetTransferData() implicitly handles conversions. + for (uint32_t i = 0; i < flavors.Length(); ++i) { + nsCString& flavorStr = flavors[i]; + GdkAtom gdkFlavor = gdk_atom_intern(flavorStr.get(), FALSE); + MOZ_LOG(sDragLm, LogLevel::Debug, + ("looking for data in type %s, gdk flavor %p\n", flavorStr.get(), + gdkFlavor)); + bool dataFound = false; + if (gdkFlavor) { + GetTargetDragData(gdkFlavor); + } + if (mTargetDragData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("dataFound = true\n")); + dataFound = true; + } else { + MOZ_LOG(sDragLm, LogLevel::Debug, ("dataFound = false\n")); + + // Dragging and dropping from the file manager would cause us + // to parse the source text as a nsIFile URL. + if (flavorStr.EqualsLiteral(kFileMime)) { + gdkFlavor = gdk_atom_intern(kTextMime, FALSE); + GetTargetDragData(gdkFlavor); + if (!mTargetDragData) { + gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE); + GetTargetDragData(gdkFlavor); + } + if (mTargetDragData) { + const char* text = static_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + uint32_t convertedTextLen = 0; + + GetTextUriListItem(text, mTargetDragDataLen, aItemIndex, + &convertedText, &convertedTextLen); + + if (convertedText) { + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + nsCOMPtr<nsIURI> fileURI; + rv = ioService->NewURI(NS_ConvertUTF16toUTF8(convertedText), + nullptr, nullptr, getter_AddRefs(fileURI)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + // The common wrapping code at the end of + // this function assumes the data is text + // and calls text-specific operations. + // Make a secret hideout here for nsIFile + // objects and return early. + aTransferable->SetTransferData(flavorStr.get(), file); + g_free(convertedText); + return NS_OK; + } + } + } + g_free(convertedText); + } + continue; + } + } + + // if we are looking for text/unicode and we fail to find it + // on the clipboard first, try again with text/plain. If that + // is present, convert it to unicode. + if (flavorStr.EqualsLiteral(kUnicodeMime)) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("we were looking for text/unicode... \ + trying with text/plain;charset=utf-8\n")); + gdkFlavor = gdk_atom_intern(gTextPlainUTF8Type, FALSE); + GetTargetDragData(gdkFlavor); + if (mTargetDragData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("Got textplain data\n")); + const char* castedText = reinterpret_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + NS_ConvertUTF8toUTF16 ucs2string(castedText, mTargetDragDataLen); + convertedText = ToNewUnicode(ucs2string, mozilla::fallible); + if (convertedText) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("successfully converted plain text \ + to unicode.\n")); + // out with the old, in with the new + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = ucs2string.Length() * 2; + dataFound = true; + } // if plain text data on clipboard + } else { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("we were looking for text/unicode... \ + trying again with text/plain\n")); + gdkFlavor = gdk_atom_intern(kTextMime, FALSE); + GetTargetDragData(gdkFlavor); + if (mTargetDragData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("Got textplain data\n")); + const char* castedText = reinterpret_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + uint32_t convertedTextLen = 0; + UTF8ToNewUTF16(castedText, mTargetDragDataLen, &convertedText, + &convertedTextLen); + if (convertedText) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("successfully converted plain text \ + to unicode.\n")); + // out with the old, in with the new + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = convertedTextLen * 2; + dataFound = true; + } // if plain text data on clipboard + } // if plain text flavor present + } // if plain text charset=utf-8 flavor present + } // if looking for text/unicode + + // if we are looking for text/x-moz-url and we failed to find + // it on the clipboard, try again with text/uri-list, and then + // _NETSCAPE_URL + if (flavorStr.EqualsLiteral(kURLMime)) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("we were looking for text/x-moz-url...\ + trying again with text/uri-list\n")); + gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE); + GetTargetDragData(gdkFlavor); + if (mTargetDragData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("Got text/uri-list data\n")); + const char* data = reinterpret_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + uint32_t convertedTextLen = 0; + + GetTextUriListItem(data, mTargetDragDataLen, aItemIndex, + &convertedText, &convertedTextLen); + + if (convertedText) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("successfully converted \ + _NETSCAPE_URL to unicode.\n")); + // out with the old, in with the new + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = convertedTextLen * 2; + dataFound = true; + } + } else { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("failed to get text/uri-list data\n")); + } + if (!dataFound) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("we were looking for text/x-moz-url...\ + trying again with _NETSCAP_URL\n")); + gdkFlavor = gdk_atom_intern(gMozUrlType, FALSE); + GetTargetDragData(gdkFlavor); + if (mTargetDragData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("Got _NETSCAPE_URL data\n")); + const char* castedText = reinterpret_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + uint32_t convertedTextLen = 0; + UTF8ToNewUTF16(castedText, mTargetDragDataLen, &convertedText, + &convertedTextLen); + if (convertedText) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("successfully converted _NETSCAPE_URL \ + to unicode.\n")); + // out with the old, in with the new + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = convertedTextLen * 2; + dataFound = true; + } + } else { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("failed to get _NETSCAPE_URL data\n")); + } + } + } + + } // else we try one last ditch effort to find our data + + if (dataFound) { + if (!flavorStr.EqualsLiteral(kCustomTypesMime)) { + // the DOM only wants LF, so convert from MacOS line endings + // to DOM line endings. + nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks( + flavorStr, &mTargetDragData, + reinterpret_cast<int*>(&mTargetDragDataLen)); + } + + // put it into the transferable. + nsCOMPtr<nsISupports> genericDataWrapper; + nsPrimitiveHelpers::CreatePrimitiveForData( + flavorStr, mTargetDragData, mTargetDragDataLen, + getter_AddRefs(genericDataWrapper)); + aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper); + // we found one, get out of this loop! + MOZ_LOG(sDragLm, LogLevel::Debug, ("dataFound and converted!\n")); + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("nsDragService::IsDataFlavorSupported %s", aDataFlavor)); + if (!_retval) return NS_ERROR_INVALID_ARG; + + // set this to no by default + *_retval = false; + + // check to make sure that we have a drag object set, here + if (!mTargetWidget) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("*** warning: IsDataFlavorSupported \ + called without a valid target widget!\n")); + return NS_OK; + } + + // check to see if the target context is a list. + bool isList = IsTargetContextList(); + // if it is, just look in the internal data since we are the source + // for it. + if (isList) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("It's a list..")); + uint32_t numDragItems = 0; + // if we don't have mDataItems we didn't start this drag so it's + // an external client trying to fool us. + if (!mSourceDataItems) return NS_OK; + mSourceDataItems->GetLength(&numDragItems); + for (uint32_t itemIndex = 0; itemIndex < numDragItems; ++itemIndex) { + nsCOMPtr<nsITransferable> currItem = + do_QueryElementAt(mSourceDataItems, itemIndex); + if (currItem) { + nsTArray<nsCString> flavors; + currItem->FlavorsTransferableCanExport(flavors); + for (uint32_t i = 0; i < flavors.Length(); ++i) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("checking %s against %s\n", flavors[i].get(), aDataFlavor)); + if (flavors[i].Equals(aDataFlavor)) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("boioioioiooioioioing!\n")); + *_retval = true; + } + } + } + } + return NS_OK; + } + + // check the target context vs. this flavor, one at a time + GList* tmp = nullptr; + if (mTargetDragContext) { + tmp = gdk_drag_context_list_targets(mTargetDragContext); + } +#ifdef MOZ_WAYLAND + else if (mTargetWaylandDragContext) { + tmp = mTargetWaylandDragContext->GetTargets(); + } + GList* tmp_head = tmp; +#endif + + for (; tmp; tmp = tmp->next) { + /* Bug 331198 */ + GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data); + gchar* name = nullptr; + name = gdk_atom_name(atom); + MOZ_LOG(sDragLm, LogLevel::Debug, + ("checking %s against %s\n", name, aDataFlavor)); + if (name && (strcmp(name, aDataFlavor) == 0)) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("good!\n")); + *_retval = true; + } + // check for automatic text/uri-list -> text/x-moz-url mapping + if (!*_retval && name && (strcmp(name, gTextUriListType) == 0) && + (strcmp(aDataFlavor, kURLMime) == 0 || + strcmp(aDataFlavor, kFileMime) == 0)) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("good! ( it's text/uri-list and \ + we're checking against text/x-moz-url )\n")); + *_retval = true; + } + // check for automatic _NETSCAPE_URL -> text/x-moz-url mapping + if (!*_retval && name && (strcmp(name, gMozUrlType) == 0) && + (strcmp(aDataFlavor, kURLMime) == 0)) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("good! ( it's _NETSCAPE_URL and \ + we're checking against text/x-moz-url )\n")); + *_retval = true; + } + // check for auto text/plain -> text/unicode mapping + if (!*_retval && name && (strcmp(name, kTextMime) == 0) && + ((strcmp(aDataFlavor, kUnicodeMime) == 0) || + (strcmp(aDataFlavor, kFileMime) == 0))) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("good! ( it's text plain and we're checking \ + against text/unicode or application/x-moz-file)\n")); + *_retval = true; + } + g_free(name); + } + +#ifdef MOZ_WAYLAND + // mTargetWaylandDragContext->GetTargets allocates the list + // so we need to free it here. + if (!mTargetDragContext && tmp_head) { + g_list_free(tmp_head); + } +#endif + + return NS_OK; +} + +void nsDragService::ReplyToDragMotion(GdkDragContext* aDragContext) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("nsDragService::ReplyToDragMotion %d", mCanDrop)); + + GdkDragAction action = (GdkDragAction)0; + if (mCanDrop) { + // notify the dragger if we can drop + switch (mDragAction) { + case DRAGDROP_ACTION_COPY: + action = GDK_ACTION_COPY; + break; + case DRAGDROP_ACTION_LINK: + action = GDK_ACTION_LINK; + break; + case DRAGDROP_ACTION_NONE: + action = (GdkDragAction)0; + break; + default: + action = GDK_ACTION_MOVE; + break; + } + } + + gdk_drag_status(aDragContext, action, mTargetTime); +} + +#ifdef MOZ_WAYLAND +void nsDragService::ReplyToDragMotion(nsWaylandDragContext* aDragContext) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("nsDragService::ReplyToDragMotion %d", mCanDrop)); + + GdkDragAction action = (GdkDragAction)0; + if (mCanDrop) { + // notify the dragger if we can drop + switch (mDragAction) { + case DRAGDROP_ACTION_COPY: + action = GDK_ACTION_COPY; + break; + case DRAGDROP_ACTION_LINK: + action = GDK_ACTION_LINK; + break; + case DRAGDROP_ACTION_NONE: + action = (GdkDragAction)0; + break; + default: + action = GDK_ACTION_MOVE; + break; + } + } + + aDragContext->SetDragStatus(action); +} +#endif + +void nsDragService::TargetDataReceived(GtkWidget* aWidget, + GdkDragContext* aContext, gint aX, + gint aY, + GtkSelectionData* aSelectionData, + guint aInfo, guint32 aTime) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::TargetDataReceived")); + TargetResetData(); + + mTargetDragDataReceived = true; + gint len = gtk_selection_data_get_length(aSelectionData); + const guchar* data = gtk_selection_data_get_data(aSelectionData); + + GdkAtom target = gtk_selection_data_get_target(aSelectionData); + char* name = gdk_atom_name(target); + nsCString flavor(name); + g_free(name); + + if (len > 0 && data) { + mTargetDragDataLen = len; + mTargetDragData = g_malloc(mTargetDragDataLen); + memcpy(mTargetDragData, data, mTargetDragDataLen); + + nsTArray<uint8_t> copy; + if (!copy.SetLength(len, fallible)) { + return; + } + memcpy(copy.Elements(), data, len); + + mCachedData.Put(flavor, std::move(copy)); + } else { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("Failed to get data. selection data len was %d\n", + mTargetDragDataLen)); + + mCachedData.Put(flavor, nsTArray<uint8_t>()); + } +} + +bool nsDragService::IsTargetContextList(void) { + bool retval = false; + + // gMimeListType drags only work for drags within a single process. The + // gtk_drag_get_source_widget() function will return nullptr if the source + // of the drag is another app, so we use it to check if a gMimeListType + // drop will work or not. + if (mTargetDragContext && + gtk_drag_get_source_widget(mTargetDragContext) == nullptr) { + return retval; + } + + GList* tmp = nullptr; + if (mTargetDragContext) { + tmp = gdk_drag_context_list_targets(mTargetDragContext); + } +#ifdef MOZ_WAYLAND + GList* tmp_head = nullptr; + if (mTargetWaylandDragContext) { + tmp_head = tmp = mTargetWaylandDragContext->GetTargets(); + } +#endif + + // walk the list of context targets and see if one of them is a list + // of items. + for (; tmp; tmp = tmp->next) { + /* Bug 331198 */ + GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data); + gchar* name = nullptr; + name = gdk_atom_name(atom); + if (name && strcmp(name, gMimeListType) == 0) retval = true; + g_free(name); + if (retval) break; + } + +#ifdef MOZ_WAYLAND + // mTargetWaylandDragContext->GetTargets allocates the list + // so we need to free it here. + if (mTargetWaylandDragContext && tmp_head) { + g_list_free(tmp_head); + } +#endif + + return retval; +} + +// Maximum time to wait for a "drag_received" arrived, in microseconds +#define NS_DND_TIMEOUT 500000 + +void nsDragService::GetTargetDragData(GdkAtom aFlavor) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("getting data flavor %p\n", aFlavor)); + MOZ_LOG(sDragLm, LogLevel::Debug, + ("mLastWidget is %p and mLastContext is %p\n", mTargetWidget.get(), + mTargetDragContext.get())); + // reset our target data areas + TargetResetData(); + + if (mTargetDragContext) { + char* name = gdk_atom_name(aFlavor); + nsCString flavor(name); + g_free(name); + + // We keep a copy of the requested data with the same life-time + // as mTargetDragContext. + // Especially with multiple items the same data is requested + // very often. + if (nsTArray<uint8_t>* cached = mCachedData.GetValue(flavor)) { + mTargetDragDataLen = cached->Length(); + MOZ_LOG(sDragLm, LogLevel::Debug, + ("Using cached data for %s, length is %d", flavor.get(), + mTargetDragDataLen)); + + if (mTargetDragDataLen) { + mTargetDragData = g_malloc(mTargetDragDataLen); + memcpy(mTargetDragData, cached->Elements(), mTargetDragDataLen); + } + + mTargetDragDataReceived = true; + return; + } + + gtk_drag_get_data(mTargetWidget, mTargetDragContext, aFlavor, mTargetTime); + + MOZ_LOG(sDragLm, LogLevel::Debug, ("about to start inner iteration.")); + PRTime entryTime = PR_Now(); + while (!mTargetDragDataReceived && mDoingDrag) { + // check the number of iterations + MOZ_LOG(sDragLm, LogLevel::Debug, ("doing iteration...\n")); + PR_Sleep(20 * PR_TicksPerSecond() / 1000); /* sleep for 20 ms/iteration */ + if (PR_Now() - entryTime > NS_DND_TIMEOUT) break; + gtk_main_iteration(); + } + } +#ifdef MOZ_WAYLAND + else { + mTargetDragData = mTargetWaylandDragContext->GetData(gdk_atom_name(aFlavor), + &mTargetDragDataLen); + mTargetDragDataReceived = true; + } +#endif + MOZ_LOG(sDragLm, LogLevel::Debug, ("finished inner iteration\n")); +} + +void nsDragService::TargetResetData(void) { + mTargetDragDataReceived = false; + // make sure to free old data if we have to + g_free(mTargetDragData); + mTargetDragData = 0; + mTargetDragDataLen = 0; +} + +GtkTargetList* nsDragService::GetSourceList(void) { + if (!mSourceDataItems) return nullptr; + nsTArray<GtkTargetEntry*> targetArray; + GtkTargetEntry* targets; + GtkTargetList* targetList = 0; + uint32_t targetCount = 0; + unsigned int numDragItems = 0; + + mSourceDataItems->GetLength(&numDragItems); + + // Check to see if we're dragging > 1 item. + if (numDragItems > 1) { + // as the Xdnd protocol only supports a single item (or is it just + // gtk's implementation?), we don't advertise all flavours listed + // in the nsITransferable. + + // the application/x-moz-internal-item-list format, which preserves + // all information for drags within the same mozilla instance. + GtkTargetEntry* listTarget = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + listTarget->target = g_strdup(gMimeListType); + listTarget->flags = 0; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", listTarget->target)); + targetArray.AppendElement(listTarget); + + // check what flavours are supported so we can decide what other + // targets to advertise. + nsCOMPtr<nsITransferable> currItem = do_QueryElementAt(mSourceDataItems, 0); + + if (currItem) { + nsTArray<nsCString> flavors; + currItem->FlavorsTransferableCanExport(flavors); + for (uint32_t i = 0; i < flavors.Length(); ++i) { + // check if text/x-moz-url is supported. + // If so, advertise + // text/uri-list. + if (flavors[i].EqualsLiteral(kURLMime)) { + listTarget = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + listTarget->target = g_strdup(gTextUriListType); + listTarget->flags = 0; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", listTarget->target)); + targetArray.AppendElement(listTarget); + } + } + } // if item is a transferable + } else if (numDragItems == 1) { + nsCOMPtr<nsITransferable> currItem = do_QueryElementAt(mSourceDataItems, 0); + if (currItem) { + nsTArray<nsCString> flavors; + currItem->FlavorsTransferableCanExport(flavors); + for (uint32_t i = 0; i < flavors.Length(); ++i) { + nsCString& flavorStr = flavors[i]; + + GtkTargetEntry* target = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + target->target = g_strdup(flavorStr.get()); + target->flags = 0; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("adding target %s\n", target->target)); + targetArray.AppendElement(target); + + // If there is a file, add the text/uri-list type. + if (flavorStr.EqualsLiteral(kFileMime)) { + GtkTargetEntry* urilistTarget = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + urilistTarget->target = g_strdup(gTextUriListType); + urilistTarget->flags = 0; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", urilistTarget->target)); + targetArray.AppendElement(urilistTarget); + } + // Check to see if this is text/unicode. + // If it is, add text/plain + // since we automatically support text/plain + // if we support text/unicode. + else if (flavorStr.EqualsLiteral(kUnicodeMime)) { + GtkTargetEntry* plainUTF8Target = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + plainUTF8Target->target = g_strdup(gTextPlainUTF8Type); + plainUTF8Target->flags = 0; + MOZ_LOG( + sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", plainUTF8Target->target)); + targetArray.AppendElement(plainUTF8Target); + + GtkTargetEntry* plainTarget = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + plainTarget->target = g_strdup(kTextMime); + plainTarget->flags = 0; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", plainTarget->target)); + targetArray.AppendElement(plainTarget); + } + // Check to see if this is the x-moz-url type. + // If it is, add _NETSCAPE_URL + // this is a type used by everybody. + else if (flavorStr.EqualsLiteral(kURLMime)) { + GtkTargetEntry* urlTarget = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + urlTarget->target = g_strdup(gMozUrlType); + urlTarget->flags = 0; + MOZ_LOG(sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", urlTarget->target)); + targetArray.AppendElement(urlTarget); + } + // XdndDirectSave + else if (flavorStr.EqualsLiteral(kFilePromiseMime)) { + GtkTargetEntry* directsaveTarget = + (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + directsaveTarget->target = g_strdup(gXdndDirectSaveType); + directsaveTarget->flags = 0; + MOZ_LOG( + sDragLm, LogLevel::Debug, + ("automatically adding target %s\n", directsaveTarget->target)); + targetArray.AppendElement(directsaveTarget); + } + } + } + } + + // get all the elements that we created. + targetCount = targetArray.Length(); + if (targetCount) { + // allocate space to create the list of valid targets + targets = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry) * targetCount); + uint32_t targetIndex; + for (targetIndex = 0; targetIndex < targetCount; ++targetIndex) { + GtkTargetEntry* disEntry = targetArray.ElementAt(targetIndex); + // this is a string reference but it will be freed later. + targets[targetIndex].target = disEntry->target; + targets[targetIndex].flags = disEntry->flags; + targets[targetIndex].info = 0; + } + targetList = gtk_target_list_new(targets, targetCount); + // clean up the target list + for (uint32_t cleanIndex = 0; cleanIndex < targetCount; ++cleanIndex) { + GtkTargetEntry* thisTarget = targetArray.ElementAt(cleanIndex); + g_free(thisTarget->target); + g_free(thisTarget); + } + g_free(targets); + } else { + // We need to create a dummy target list to be able initialize dnd. + targetList = gtk_target_list_new(nullptr, 0); + } + return targetList; +} + +void nsDragService::SourceEndDragSession(GdkDragContext* aContext, + gint aResult) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("SourceEndDragSession result %d\n", aResult)); + + // this just releases the list of data items that we provide + mSourceDataItems = nullptr; + + // Remove this property, if it exists, to satisfy the Direct Save Protocol. + GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE); + gdk_property_delete(gdk_drag_context_get_source_window(aContext), property); + + if (!mDoingDrag || mScheduledTask == eDragTaskSourceEnd) + // EndDragSession() was already called on drop + // or SourceEndDragSession on drag-failed + return; + + if (mEndDragPoint.x < 0) { + // We don't have a drag end point, so guess + gint x, y; + GdkDisplay* display = gdk_display_get_default(); + if (display) { + gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor(); + gdk_display_get_pointer(display, nullptr, &x, &y, nullptr); + SetDragEndPoint(LayoutDeviceIntPoint(x * scale, y * scale)); + MOZ_LOG(sDragLm, LogLevel::Debug, + ("guess drag end point %d %d\n", x * scale, y * scale)); + } + } + + // Either the drag was aborted or the drop occurred outside the app. + // The dropEffect of mDataTransfer is not updated for motion outside the + // app, but is needed for the dragend event, so set it now. + + uint32_t dropEffect; + + if (aResult == MOZ_GTK_DRAG_RESULT_SUCCESS) { + // With GTK+ versions 2.10.x and prior the drag may have been + // cancelled (but no drag-failed signal would have been sent). + // aContext->dest_window will be non-nullptr only if the drop was + // sent. + GdkDragAction action = gdk_drag_context_get_dest_window(aContext) + ? gdk_drag_context_get_actions(aContext) + : (GdkDragAction)0; + + // Only one bit of action should be set, but, just in case someone + // does something funny, erring away from MOVE, and not recording + // unusual action combinations as NONE. + if (!action) + dropEffect = DRAGDROP_ACTION_NONE; + else if (action & GDK_ACTION_COPY) + dropEffect = DRAGDROP_ACTION_COPY; + else if (action & GDK_ACTION_LINK) + dropEffect = DRAGDROP_ACTION_LINK; + else if (action & GDK_ACTION_MOVE) + dropEffect = DRAGDROP_ACTION_MOVE; + else + dropEffect = DRAGDROP_ACTION_COPY; + + } else { + dropEffect = DRAGDROP_ACTION_NONE; + + bool isWaylandTabDrop = false; +#ifdef MOZ_WAYLAND + // Bug 1527976. Wayland protocol does not have any way how to handle + // MOZ_GTK_DRAG_RESULT_NO_TARGET drop result so consider all tab + // drops as not cancelled on wayland. + if (gfxPlatformGtk::GetPlatform()->IsWaylandDisplay() && + aResult == MOZ_GTK_DRAG_RESULT_ERROR) { + for (GList* tmp = gdk_drag_context_list_targets(aContext); tmp; + tmp = tmp->next) { + GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data); + gchar* name = gdk_atom_name(atom); + if (name && (strcmp(name, gTabDropType) == 0)) { + isWaylandTabDrop = true; + MOZ_LOG(sDragLm, LogLevel::Debug, ("is wayland tab drop\n")); + break; + } + } + } +#endif + if (aResult != MOZ_GTK_DRAG_RESULT_NO_TARGET && !isWaylandTabDrop) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("drop is user chancelled\n")); + mUserCancelled = true; + } + } + + if (mDataTransfer) { + mDataTransfer->SetDropEffectInt(dropEffect); + } + + // Schedule the appropriate drag end dom events. + Schedule(eDragTaskSourceEnd, nullptr, nullptr, nullptr, + LayoutDeviceIntPoint(), 0); +} + +static void CreateURIList(nsIArray* aItems, nsACString& aURIList) { + uint32_t length = 0; + aItems->GetLength(&length); + + for (uint32_t i = 0; i < length; ++i) { + nsCOMPtr<nsITransferable> item = do_QueryElementAt(aItems, i); + if (!item) { + continue; + } + + nsCOMPtr<nsISupports> data; + nsresult rv = item->GetTransferData(kURLMime, getter_AddRefs(data)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsISupportsString> string = do_QueryInterface(data); + + nsAutoString text; + if (string) { + string->GetData(text); + } + + // text/x-moz-url is of form url + "\n" + title. + // We just want the url. + int32_t separatorPos = text.FindChar(u'\n'); + if (separatorPos >= 0) { + text.Truncate(separatorPos); + } + + AppendUTF16toUTF8(text, aURIList); + aURIList.AppendLiteral("\r\n"); + continue; + } + + // There is no URI available. If there is a file available, create + // a URI from the file. + rv = item->GetTransferData(kFileMime, getter_AddRefs(data)); + if (NS_SUCCEEDED(rv)) { + if (nsCOMPtr<nsIFile> file = do_QueryInterface(data)) { + nsCOMPtr<nsIURI> fileURI; + NS_NewFileURI(getter_AddRefs(fileURI), file); + if (fileURI) { + nsAutoCString spec; + fileURI->GetSpec(spec); + + aURIList.Append(spec); + aURIList.AppendLiteral("\r\n"); + } + } + } + } +} + +void nsDragService::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext, + GtkSelectionData* aSelectionData, + guint32 aTime) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("nsDragService::SourceDataGet")); + GdkAtom target = gtk_selection_data_get_target(aSelectionData); + gchar* typeName = gdk_atom_name(target); + if (!typeName) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("failed to get atom name.\n")); + return; + } + + MOZ_LOG(sDragLm, LogLevel::Debug, ("Type is %s\n", typeName)); + auto freeTypeName = mozilla::MakeScopeExit([&] { g_free(typeName); }); + // check to make sure that we have data items to return. + if (!mSourceDataItems) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("Failed to get our data items\n")); + return; + } + + nsDependentCSubstring mimeFlavor(typeName, strlen(typeName)); + nsCOMPtr<nsITransferable> item; + item = do_QueryElementAt(mSourceDataItems, 0); + if (item) { + // if someone was asking for text/plain, lookup unicode instead so + // we can convert it. + bool needToDoConversionToPlainText = false; + const char* actualFlavor; + if (mimeFlavor.EqualsLiteral(kTextMime) || + mimeFlavor.EqualsLiteral(gTextPlainUTF8Type)) { + actualFlavor = kUnicodeMime; + needToDoConversionToPlainText = true; + } + // if someone was asking for _NETSCAPE_URL we need to convert to + // plain text but we also need to look for x-moz-url + else if (mimeFlavor.EqualsLiteral(gMozUrlType)) { + actualFlavor = kURLMime; + needToDoConversionToPlainText = true; + } + // if someone was asking for text/uri-list we need to convert to + // plain text. + else if (mimeFlavor.EqualsLiteral(gTextUriListType)) { + actualFlavor = gTextUriListType; + needToDoConversionToPlainText = true; + } + // Someone is asking for the special Direct Save Protocol type. + else if (mimeFlavor.EqualsLiteral(gXdndDirectSaveType)) { + // Indicate failure by default. + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"E", 1); + + GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE); + GdkAtom type = gdk_atom_intern(kTextMime, FALSE); + + guchar* data; + gint length; + if (!gdk_property_get(gdk_drag_context_get_source_window(aContext), + property, type, 0, INT32_MAX, FALSE, nullptr, + nullptr, &length, &data)) { + return; + } + + // Zero-terminate the string. + data = (guchar*)g_realloc(data, length + 1); + if (!data) return; + data[length] = '\0'; + + gchar* hostname; + char* gfullpath = + g_filename_from_uri((const gchar*)data, &hostname, nullptr); + g_free(data); + if (!gfullpath) return; + + nsCString fullpath(gfullpath); + g_free(gfullpath); + + MOZ_LOG(sDragLm, LogLevel::Debug, + ("XdndDirectSave filepath is %s\n", fullpath.get())); + + // If there is no hostname in the URI, NULL will be stored. + // We should not accept uris with from a different host. + if (hostname) { + nsCOMPtr<nsIPropertyBag2> infoService = + do_GetService(NS_SYSTEMINFO_CONTRACTID); + if (!infoService) return; + + nsAutoCString host; + if (NS_SUCCEEDED( + infoService->GetPropertyAsACString(u"host"_ns, host))) { + if (!host.Equals(hostname)) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("ignored drag because of different host.\n")); + + // Special error code "F" for this case. + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"F", 1); + g_free(hostname); + return; + } + } + + g_free(hostname); + } + + nsCOMPtr<nsIFile> file; + if (NS_FAILED( + NS_NewNativeLocalFile(fullpath, false, getter_AddRefs(file)))) { + return; + } + + // We have to split the path into a directory and filename, + // because our internal file-promise API is based on these. + + nsCOMPtr<nsIFile> directory; + file->GetParent(getter_AddRefs(directory)); + + item->SetTransferData(kFilePromiseDirectoryMime, directory); + + nsCOMPtr<nsISupportsString> filenamePrimitive = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + if (!filenamePrimitive) return; + + nsAutoString leafName; + file->GetLeafName(leafName); + filenamePrimitive->SetData(leafName); + + item->SetTransferData(kFilePromiseDestFilename, filenamePrimitive); + + // Request a different type in GetTransferData. + actualFlavor = kFilePromiseMime; + } else { + actualFlavor = typeName; + } + nsresult rv; + nsCOMPtr<nsISupports> data; + rv = item->GetTransferData(actualFlavor, getter_AddRefs(data)); + + if (strcmp(actualFlavor, kFilePromiseMime) == 0) { + if (NS_SUCCEEDED(rv)) { + // Indicate success. + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"S", 1); + } + return; + } + + if (NS_SUCCEEDED(rv)) { + void* tmpData = nullptr; + uint32_t tmpDataLen = 0; + nsPrimitiveHelpers::CreateDataFromPrimitive( + nsDependentCString(actualFlavor), data, &tmpData, &tmpDataLen); + // if required, do the extra work to convert unicode to plain + // text and replace the output values with the plain text. + if (needToDoConversionToPlainText) { + char* plainTextData = nullptr; + char16_t* castedUnicode = reinterpret_cast<char16_t*>(tmpData); + uint32_t plainTextLen = 0; + UTF16ToNewUTF8(castedUnicode, tmpDataLen / 2, &plainTextData, + &plainTextLen); + if (tmpData) { + // this was not allocated using glib + free(tmpData); + tmpData = plainTextData; + tmpDataLen = plainTextLen; + } + } + if (tmpData) { + // this copies the data + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)tmpData, + tmpDataLen); + // this wasn't allocated with glib + free(tmpData); + } + } else { + if (mimeFlavor.EqualsLiteral(gTextUriListType)) { + // fall back for text/uri-list + nsAutoCString list; + CreateURIList(mSourceDataItems, list); + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)list.get(), + list.Length()); + return; + } + } + } +} + +void nsDragService::SourceBeginDrag(GdkDragContext* aContext) { + nsCOMPtr<nsITransferable> transferable = + do_QueryElementAt(mSourceDataItems, 0); + if (!transferable) return; + + nsTArray<nsCString> flavors; + nsresult rv = transferable->FlavorsTransferableCanImport(flavors); + NS_ENSURE_SUCCESS(rv, ); + + for (uint32_t i = 0; i < flavors.Length(); ++i) { + if (flavors[i].EqualsLiteral(kFilePromiseDestFilename)) { + nsCOMPtr<nsISupports> data; + rv = transferable->GetTransferData(kFilePromiseDestFilename, + getter_AddRefs(data)); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr<nsISupportsString> fileName = do_QueryInterface(data); + if (!fileName) { + return; + } + + nsAutoString fileNameStr; + fileName->GetData(fileNameStr); + + nsCString fileNameCStr; + CopyUTF16toUTF8(fileNameStr, fileNameCStr); + + GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE); + GdkAtom type = gdk_atom_intern(kTextMime, FALSE); + + gdk_property_change(gdk_drag_context_get_source_window(aContext), + property, type, 8, GDK_PROP_MODE_REPLACE, + (const guchar*)fileNameCStr.get(), + fileNameCStr.Length()); + } + } +} + +void nsDragService::SetDragIcon(GdkDragContext* aContext) { + if (!mHasImage && !mSelection) return; + + LayoutDeviceIntRect dragRect; + nsPresContext* pc; + RefPtr<SourceSurface> surface; + DrawDrag(mSourceNode, mRegion, mScreenPosition, &dragRect, &surface, &pc); + if (!pc) return; + + LayoutDeviceIntPoint screenPoint = + ConvertToUnscaledDevPixels(pc, mScreenPosition); + int32_t offsetX = screenPoint.x - dragRect.x; + int32_t offsetY = screenPoint.y - dragRect.y; + + // If a popup is set as the drag image, use its widget. Otherwise, use + // the surface that DrawDrag created. + // + // XXX: Disable drag popups on GTK 3.19.4 and above: see bug 1264454. + // Fix this once a new GTK version ships that does not destroy our + // widget in gtk_drag_set_icon_widget. + if (mDragPopup && gtk_check_version(3, 19, 4)) { + GtkWidget* gtkWidget = nullptr; + nsIFrame* frame = mDragPopup->GetPrimaryFrame(); + if (frame) { + // DrawDrag ensured that this is a popup frame. + nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(); + if (widget) { + gtkWidget = (GtkWidget*)widget->GetNativeData(NS_NATIVE_SHELLWIDGET); + if (gtkWidget) { + OpenDragPopup(); + gtk_drag_set_icon_widget(aContext, gtkWidget, offsetX, offsetY); + } + } + } + } else if (surface) { + if (!SetAlphaPixmap(surface, aContext, offsetX, offsetY, dragRect)) { + GdkPixbuf* dragPixbuf = nsImageToPixbuf::SourceSurfaceToPixbuf( + surface, dragRect.width, dragRect.height); + if (dragPixbuf) { + gtk_drag_set_icon_pixbuf(aContext, dragPixbuf, offsetX, offsetY); + g_object_unref(dragPixbuf); + } + } + } +} + +static void invisibleSourceDragBegin(GtkWidget* aWidget, + GdkDragContext* aContext, gpointer aData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("invisibleSourceDragBegin")); + nsDragService* dragService = (nsDragService*)aData; + + dragService->SourceBeginDrag(aContext); + dragService->SetDragIcon(aContext); +} + +static void invisibleSourceDragDataGet(GtkWidget* aWidget, + GdkDragContext* aContext, + GtkSelectionData* aSelectionData, + guint aInfo, guint32 aTime, + gpointer aData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("invisibleSourceDragDataGet")); + nsDragService* dragService = (nsDragService*)aData; + dragService->SourceDataGet(aWidget, aContext, aSelectionData, aTime); +} + +static gboolean invisibleSourceDragFailed(GtkWidget* aWidget, + GdkDragContext* aContext, + gint aResult, gpointer aData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("invisibleSourceDragFailed %i", aResult)); + nsDragService* dragService = (nsDragService*)aData; + // End the drag session now (rather than waiting for the drag-end signal) + // so that operations performed on dropEffect == none can start immediately + // rather than waiting for the drag-failed animation to finish. + dragService->SourceEndDragSession(aContext, aResult); + + // We should return TRUE to disable the drag-failed animation iff the + // source performed an operation when dropEffect was none, but the handler + // of the dragend DOM event doesn't provide this information. + return FALSE; +} + +static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext, + gpointer aData) { + MOZ_LOG(sDragLm, LogLevel::Debug, ("invisibleSourceDragEnd")); + nsDragService* dragService = (nsDragService*)aData; + + // The drag has ended. Release the hostages! + dragService->SourceEndDragSession(aContext, MOZ_GTK_DRAG_RESULT_SUCCESS); +} + +// The following methods handle responding to GTK drag signals and +// tracking state between these signals. +// +// In general, GTK does not expect us to run the event loop while handling its +// drag signals, however our drag event handlers may run the +// event loop, most often to fetch information about the drag data. +// +// GTK, for example, uses the return value from drag-motion signals to +// determine whether drag-leave signals should be sent. If an event loop is +// run during drag-motion the XdndLeave message can get processed but when GTK +// receives the message it does not yet know that it needs to send the +// drag-leave signal to our widget. +// +// After a drag-drop signal, we need to reply with gtk_drag_finish(). +// However, gtk_drag_finish should happen after the drag-drop signal handler +// returns so that when the Motif drag protocol is used, the +// XmTRANSFER_SUCCESS during gtk_drag_finish is sent after the XmDROP_START +// reply sent on return from the drag-drop signal handler. +// +// Similarly drag-end for a successful drag and drag-failed are not good +// times to run a nested event loop as gtk_drag_drop_finished() and +// gtk_drag_source_info_destroy() don't gtk_drag_clear_source_info() or remove +// drop_timeout until after at least the first of these signals is sent. +// Processing other events (e.g. a slow GDK_DROP_FINISHED reply, or the drop +// timeout) could cause gtk_drag_drop_finished to be called again with the +// same GtkDragSourceInfo, which won't like being destroyed twice. +// +// Therefore we reply to the signals immediately and schedule a task to +// dispatch the Gecko events, which may run the event loop. +// +// Action in response to drag-leave signals is also delayed until the event +// loop runs again so that we find out whether a drag-drop signal follows. +// +// A single task is scheduled to manage responses to all three GTK signals. +// If further signals are received while the task is scheduled, the scheduled +// response is updated, sometimes effectively compressing successive signals. +// +// No Gecko drag events are dispatched (during nested event loops) while other +// Gecko drag events are in flight. This helps event handlers that may not +// expect nested events, while accessing an event's dataTransfer for example. + +gboolean nsDragService::ScheduleMotionEvent( + nsWindow* aWindow, GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + LayoutDeviceIntPoint aWindowPoint, guint aTime) { + if (aDragContext && mScheduledTask == eDragTaskMotion) { + // The drag source has sent another motion message before we've + // replied to the previous. That shouldn't happen with Xdnd. The + // spec for Motif drags is less clear, but we'll just update the + // scheduled task with the new position reply only to the most + // recent message. + NS_WARNING("Drag Motion message received before previous reply was sent"); + } + + // Returning TRUE means we'll reply with a status message, unless we first + // get a leave. + return Schedule(eDragTaskMotion, aWindow, aDragContext, aWaylandDragContext, + aWindowPoint, aTime); +} + +void nsDragService::ScheduleLeaveEvent() { + // We don't know at this stage whether a drop signal will immediately + // follow. If the drop signal gets sent it will happen before we return + // to the main loop and the scheduled leave task will be replaced. + if (!Schedule(eDragTaskLeave, nullptr, nullptr, nullptr, + LayoutDeviceIntPoint(), 0)) { + NS_WARNING("Drag leave after drop"); + } +} + +gboolean nsDragService::ScheduleDropEvent( + nsWindow* aWindow, GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + LayoutDeviceIntPoint aWindowPoint, guint aTime) { + if (!Schedule(eDragTaskDrop, aWindow, aDragContext, aWaylandDragContext, + aWindowPoint, aTime)) { + NS_WARNING("Additional drag drop ignored"); + return FALSE; + } + + SetDragEndPoint(aWindowPoint + aWindow->WidgetToScreenOffset()); + + // We'll reply with gtk_drag_finish(). + return TRUE; +} + +gboolean nsDragService::Schedule(DragTask aTask, nsWindow* aWindow, + GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + LayoutDeviceIntPoint aWindowPoint, + guint aTime) { + // If there is an existing leave or motion task scheduled, then that + // will be replaced. When the new task is run, it will dispatch + // any necessary leave or motion events. + + // If aTask is eDragTaskSourceEnd, then it will replace even a scheduled + // drop event (which could happen if the drop event has not been processed + // within the allowed time). Otherwise, if we haven't yet run a scheduled + // drop or end task, just say that we are not ready to receive another + // drop. + if (mScheduledTask == eDragTaskSourceEnd || + (mScheduledTask == eDragTaskDrop && aTask != eDragTaskSourceEnd)) + return FALSE; + + mScheduledTask = aTask; + mPendingWindow = aWindow; + mPendingDragContext = aDragContext; +#ifdef MOZ_WAYLAND + mPendingWaylandDragContext = aWaylandDragContext; +#endif + mPendingWindowPoint = aWindowPoint; + mPendingTime = aTime; + + if (!mTaskSource) { + // High priority is used here because the native events involved have + // already waited at default priority. Perhaps a lower than default + // priority could be used for motion tasks because there is a chance + // that a leave or drop is waiting, but managing different priorities + // may not be worth the effort. Motion tasks shouldn't queue up as + // they should be throttled based on replies. + mTaskSource = + g_idle_add_full(G_PRIORITY_HIGH, TaskDispatchCallback, this, nullptr); + } + return TRUE; +} + +gboolean nsDragService::TaskDispatchCallback(gpointer data) { + RefPtr<nsDragService> dragService = static_cast<nsDragService*>(data); + return dragService->RunScheduledTask(); +} + +gboolean nsDragService::RunScheduledTask() { + if (mTargetWindow && mTargetWindow != mPendingWindow) { + MOZ_LOG(sDragLm, LogLevel::Debug, + ("nsDragService: dispatch drag leave (%p)\n", mTargetWindow.get())); + mTargetWindow->DispatchDragEvent(eDragExit, mTargetWindowPoint, 0); + + if (!mSourceNode) { + // The drag that was initiated in a different app. End the drag + // session, since we're done with it for now (until the user drags + // back into this app). + EndDragSession(false, GetCurrentModifiers()); + } + } + + // It is possible that the pending state has been updated during dispatch + // of the leave event. That's fine. + + // Now we collect the pending state because, from this point on, we want + // to use the same state for all events dispatched. All state is updated + // so that when other tasks are scheduled during dispatch here, this + // task is considered to have already been run. + bool positionHasChanged = mPendingWindow != mTargetWindow || + mPendingWindowPoint != mTargetWindowPoint; + DragTask task = mScheduledTask; + mScheduledTask = eDragTaskNone; + mTargetWindow = std::move(mPendingWindow); + mTargetWindowPoint = mPendingWindowPoint; + + if (task == eDragTaskLeave || task == eDragTaskSourceEnd) { + if (task == eDragTaskSourceEnd) { + // Dispatch drag end events. + EndDragSession(true, GetCurrentModifiers()); + } + + // Nothing more to do + // Returning false removes the task source from the event loop. + mTaskSource = 0; + return FALSE; + } + + // This may be the start of a destination drag session. + StartDragSession(); + + // mTargetWidget may be nullptr if the window has been destroyed. + // (The leave event is not scheduled if a drop task is still scheduled.) + // We still reply appropriately to indicate that the drop will or didn't + // succeeed. + mTargetWidget = mTargetWindow->GetMozContainerWidget(); + mTargetDragContext = std::move(mPendingDragContext); +#ifdef MOZ_WAYLAND + mTargetWaylandDragContext = std::move(mPendingWaylandDragContext); +#endif + mTargetTime = mPendingTime; + + mCachedData.Clear(); + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model + // (as at 27 December 2010) indicates that a "drop" event should only be + // fired (at the current target element) if the current drag operation is + // not none. The current drag operation will only be set to a non-none + // value during a "dragover" event. + // + // If the user has ended the drag before any dragover events have been + // sent, then the spec recommends skipping the drop (because the current + // drag operation is none). However, here we assume that, by releasing + // the mouse button, the user has indicated that they want to drop, so we + // proceed with the drop where possible. + // + // In order to make the events appear to content in the same way as if the + // spec is being followed we make sure to dispatch a "dragover" event with + // appropriate coordinates and check canDrop before the "drop" event. + // + // When the Xdnd protocol is used for source/destination communication (as + // should be the case with GTK source applications) a dragover event + // should have already been sent during the drag-motion signal, which + // would have already been received because XdndDrop messages do not + // contain a position. However, we can't assume the same when the Motif + // protocol is used. + if (task == eDragTaskMotion || positionHasChanged) { + UpdateDragAction(); + TakeDragEventDispatchedToChildProcess(); // Clear the old value. + DispatchMotionEvents(); + if (task == eDragTaskMotion) { + if (TakeDragEventDispatchedToChildProcess()) { + mTargetDragContextForRemote = mTargetDragContext; +#ifdef MOZ_WAYLAND + mTargetWaylandDragContextForRemote = mTargetWaylandDragContext; +#endif + } else { + // Reply to tell the source whether we can drop and what + // action would be taken. + if (mTargetDragContext) { + ReplyToDragMotion(mTargetDragContext); + } +#ifdef MOZ_WAYLAND + else if (mTargetWaylandDragContext) { + ReplyToDragMotion(mTargetWaylandDragContext); + } +#endif + } + } + } + + if (task == eDragTaskDrop) { + gboolean success = DispatchDropEvent(); + + // Perhaps we should set the del parameter to TRUE when the drag + // action is move, but we don't know whether the data was successfully + // transferred. + if (mTargetDragContext) { + gtk_drag_finish(mTargetDragContext, success, + /* del = */ FALSE, mTargetTime); + } + + // This drag is over, so clear out our reference to the previous + // window. + mTargetWindow = nullptr; + // Make sure to end the drag session. If this drag started in a + // different app, we won't get a drag_end signal to end it from. + EndDragSession(true, GetCurrentModifiers()); + } + + // We're done with the drag context. + mTargetWidget = nullptr; + mTargetDragContext = nullptr; +#ifdef MOZ_WAYLAND + mTargetWaylandDragContext = nullptr; +#endif + + mCachedData.Clear(); + + // If we got another drag signal while running the sheduled task, that + // must have happened while running a nested event loop. Leave the task + // source on the event loop. + if (mScheduledTask != eDragTaskNone) return TRUE; + + // We have no task scheduled. + // Returning false removes the task source from the event loop. + mTaskSource = 0; + return FALSE; +} + +// This will update the drag action based on the information in the +// drag context. Gtk gets this from a combination of the key settings +// and what the source is offering. + +void nsDragService::UpdateDragAction() { + // This doesn't look right. dragSession.dragAction is used by + // nsContentUtils::SetDataTransferInEvent() to set the initial + // dataTransfer.dropEffect, so GdkDragContext::suggested_action would be + // more appropriate. GdkDragContext::actions should be used to set + // dataTransfer.effectAllowed, which doesn't currently happen with + // external sources. + + // default is to do nothing + int action = nsIDragService::DRAGDROP_ACTION_NONE; + GdkDragAction gdkAction = GDK_ACTION_DEFAULT; + if (mTargetDragContext) { + gdkAction = gdk_drag_context_get_actions(mTargetDragContext); + } +#ifdef MOZ_WAYLAND + else if (mTargetWaylandDragContext) { + gdkAction = mTargetWaylandDragContext->GetAvailableDragActions(); + } +#endif + + // set the default just in case nothing matches below + if (gdkAction & GDK_ACTION_DEFAULT) + action = nsIDragService::DRAGDROP_ACTION_MOVE; + + // first check to see if move is set + if (gdkAction & GDK_ACTION_MOVE) + action = nsIDragService::DRAGDROP_ACTION_MOVE; + + // then fall to the others + else if (gdkAction & GDK_ACTION_LINK) + action = nsIDragService::DRAGDROP_ACTION_LINK; + + // copy is ctrl + else if (gdkAction & GDK_ACTION_COPY) + action = nsIDragService::DRAGDROP_ACTION_COPY; + + // update the drag information + SetDragAction(action); +} + +NS_IMETHODIMP +nsDragService::UpdateDragEffect() { + if (mTargetDragContextForRemote) { + ReplyToDragMotion(mTargetDragContextForRemote); + mTargetDragContextForRemote = nullptr; + } +#ifdef MOZ_WAYLAND + else if (mTargetWaylandDragContextForRemote) { + ReplyToDragMotion(mTargetWaylandDragContextForRemote); + mTargetWaylandDragContextForRemote = nullptr; + } +#endif + return NS_OK; +} + +void nsDragService::DispatchMotionEvents() { + mCanDrop = false; + + FireDragEventAtSource(eDrag, GetCurrentModifiers()); + + mTargetWindow->DispatchDragEvent(eDragOver, mTargetWindowPoint, mTargetTime); +} + +// Returns true if the drop was successful +gboolean nsDragService::DispatchDropEvent() { + // We need to check IsDestroyed here because the nsRefPtr + // only protects this from being deleted, it does NOT protect + // against nsView::~nsView() calling Destroy() on it, bug 378273. + if (mTargetWindow->IsDestroyed()) return FALSE; + + EventMessage msg = mCanDrop ? eDrop : eDragExit; + + mTargetWindow->DispatchDragEvent(msg, mTargetWindowPoint, mTargetTime); + + return mCanDrop; +} + +/* static */ +uint32_t nsDragService::GetCurrentModifiers() { + return mozilla::widget::KeymapWrapper::ComputeCurrentKeyModifiers(); +} diff --git a/widget/gtk/nsDragService.h b/widget/gtk/nsDragService.h new file mode 100644 index 0000000000..6072824f49 --- /dev/null +++ b/widget/gtk/nsDragService.h @@ -0,0 +1,210 @@ +/* -*- 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/. */ + +#ifndef nsDragService_h__ +#define nsDragService_h__ + +#include "mozilla/RefPtr.h" +#include "nsBaseDragService.h" +#include "nsIObserver.h" +#include <gtk/gtk.h> + +class nsICookieJarSettings; +class nsWindow; +class nsWaylandDragContext; + +namespace mozilla { +namespace gfx { +class SourceSurface; +} +} // namespace mozilla + +/** + * Native GTK DragService wrapper + */ + +class nsDragService final : public nsBaseDragService, public nsIObserver { + public: + nsDragService(); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIOBSERVER + + // nsBaseDragService + MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl( + nsIArray* anArrayTransferables, + const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion, + uint32_t aActionType) override; + // nsIDragService + MOZ_CAN_RUN_SCRIPT NS_IMETHOD InvokeDragSession( + nsINode* aDOMNode, nsIPrincipal* aPrincipal, + nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings, + nsIArray* anArrayTransferables, uint32_t aActionType, + nsContentPolicyType aContentPolicyType) override; + NS_IMETHOD StartDragSession() override; + MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag, + uint32_t aKeyModifiers) override; + + // nsIDragSession + NS_IMETHOD SetCanDrop(bool aCanDrop) override; + NS_IMETHOD GetCanDrop(bool* aCanDrop) override; + NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override; + NS_IMETHOD GetData(nsITransferable* aTransferable, + uint32_t aItemIndex) override; + NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor, + bool* _retval) override; + + NS_IMETHOD UpdateDragEffect() override; + + // Methods called from nsWindow to handle responding to GTK drag + // destination signals + + static already_AddRefed<nsDragService> GetInstance(); + + void TargetDataReceived(GtkWidget* aWidget, GdkDragContext* aContext, gint aX, + gint aY, GtkSelectionData* aSelection_data, + guint aInfo, guint32 aTime); + + gboolean ScheduleMotionEvent(nsWindow* aWindow, GdkDragContext* aDragContext, + nsWaylandDragContext* aPendingWaylandDragContext, + mozilla::LayoutDeviceIntPoint aWindowPoint, + guint aTime); + void ScheduleLeaveEvent(); + gboolean ScheduleDropEvent(nsWindow* aWindow, GdkDragContext* aDragContext, + nsWaylandDragContext* aPendingWaylandDragContext, + mozilla::LayoutDeviceIntPoint aWindowPoint, + guint aTime); + + nsWindow* GetMostRecentDestWindow() { + return mScheduledTask == eDragTaskNone ? mTargetWindow : mPendingWindow; + } + + // END PUBLIC API + + // These methods are public only so that they can be called from functions + // with C calling conventions. They are called for drags started with the + // invisible widget. + void SourceEndDragSession(GdkDragContext* aContext, gint aResult); + void SourceDataGet(GtkWidget* widget, GdkDragContext* context, + GtkSelectionData* selection_data, guint32 aTime); + + void SourceBeginDrag(GdkDragContext* aContext); + + // set the drag icon during drag-begin + void SetDragIcon(GdkDragContext* aContext); + + protected: + virtual ~nsDragService(); + + private: + // mScheduledTask indicates what signal has been received from GTK and + // so what needs to be dispatched when the scheduled task is run. It is + // eDragTaskNone when there is no task scheduled (but the + // previous task may still not have finished running). + enum DragTask { + eDragTaskNone, + eDragTaskMotion, + eDragTaskLeave, + eDragTaskDrop, + eDragTaskSourceEnd + }; + DragTask mScheduledTask; + // mTaskSource is the GSource id for the task that is either scheduled + // or currently running. It is 0 if no task is scheduled or running. + guint mTaskSource; + + // target/destination side vars + // These variables keep track of the state of the current drag. + + // mPendingWindow, mPendingWindowPoint, mPendingDragContext, and + // mPendingTime, carry information from the GTK signal that will be used + // when the scheduled task is run. mPendingWindow and mPendingDragContext + // will be nullptr if the scheduled task is eDragTaskLeave. + RefPtr<nsWindow> mPendingWindow; + mozilla::LayoutDeviceIntPoint mPendingWindowPoint; + RefPtr<GdkDragContext> mPendingDragContext; + + // We cache all data for the current drag context, + // because waiting for the data in GetTargetDragData can be very slow. + nsDataHashtable<nsCStringHashKey, nsTArray<uint8_t>> mCachedData; + +#ifdef MOZ_WAYLAND + RefPtr<nsWaylandDragContext> mPendingWaylandDragContext; +#endif + guint mPendingTime; + + // mTargetWindow and mTargetWindowPoint record the position of the last + // eDragTaskMotion or eDragTaskDrop task that was run or is still running. + // mTargetWindow is cleared once the drag has completed or left. + RefPtr<nsWindow> mTargetWindow; + mozilla::LayoutDeviceIntPoint mTargetWindowPoint; + // mTargetWidget and mTargetDragContext are set only while dispatching + // motion or drop events. mTime records the corresponding timestamp. + RefPtr<GtkWidget> mTargetWidget; + RefPtr<GdkDragContext> mTargetDragContext; +#ifdef MOZ_WAYLAND + RefPtr<nsWaylandDragContext> mTargetWaylandDragContext; +#endif + // mTargetDragContextForRemote is set while waiting for a reply from + // a child process. + RefPtr<GdkDragContext> mTargetDragContextForRemote; +#ifdef MOZ_WAYLAND + RefPtr<nsWaylandDragContext> mTargetWaylandDragContextForRemote; +#endif + guint mTargetTime; + + // is it OK to drop on us? + bool mCanDrop; + + // have we received our drag data? + bool mTargetDragDataReceived; + // last data received and its length + void* mTargetDragData; + uint32_t mTargetDragDataLen; + // is the current target drag context contain a list? + bool IsTargetContextList(void); + // this will get the native data from the last target given a + // specific flavor + void GetTargetDragData(GdkAtom aFlavor); + // this will reset all of the target vars + void TargetResetData(void); + + // source side vars + + // the source of our drags + GtkWidget* mHiddenWidget; + // our source data items + nsCOMPtr<nsIArray> mSourceDataItems; + + // get a list of the sources in gtk's format + GtkTargetList* GetSourceList(void); + + // attempts to create a semi-transparent drag image. Returns TRUE if + // successful, FALSE if not + bool SetAlphaPixmap(SourceSurface* aPixbuf, GdkDragContext* aContext, + int32_t aXOffset, int32_t aYOffset, + const mozilla::LayoutDeviceIntRect& dragRect); + + gboolean Schedule(DragTask aTask, nsWindow* aWindow, + GdkDragContext* aDragContext, + nsWaylandDragContext* aPendingWaylandDragContext, + mozilla::LayoutDeviceIntPoint aWindowPoint, guint aTime); + + // Callback for g_idle_add_full() to run mScheduledTask. + MOZ_CAN_RUN_SCRIPT static gboolean TaskDispatchCallback(gpointer data); + MOZ_CAN_RUN_SCRIPT gboolean RunScheduledTask(); + void UpdateDragAction(); + MOZ_CAN_RUN_SCRIPT void DispatchMotionEvents(); + void ReplyToDragMotion(GdkDragContext* aDragContext); +#ifdef MOZ_WAYLAND + void ReplyToDragMotion(nsWaylandDragContext* aDragContext); +#endif + gboolean DispatchDropEvent(); + static uint32_t GetCurrentModifiers(); +}; + +#endif // nsDragService_h__ diff --git a/widget/gtk/nsFilePicker.cpp b/widget/gtk/nsFilePicker.cpp new file mode 100644 index 0000000000..b1f9ed3961 --- /dev/null +++ b/widget/gtk/nsFilePicker.cpp @@ -0,0 +1,646 @@ +/* -*- 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 <dlfcn.h> +#include <gtk/gtk.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "mozilla/Types.h" +#include "nsGtkUtils.h" +#include "nsIFileURL.h" +#include "nsIGIOService.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsIFile.h" +#include "mozilla/Preferences.h" + +#include "nsArrayEnumerator.h" +#include "nsMemory.h" +#include "nsEnumeratorUtils.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "MozContainer.h" +#include "gfxPlatformGtk.h" + +#include "nsFilePicker.h" + +using namespace mozilla; + +#define MAX_PREVIEW_SIZE 180 +// bug 1184009 +#define MAX_PREVIEW_SOURCE_SIZE 4096 + +nsIFile* nsFilePicker::mPrevDisplayDirectory = nullptr; + +void nsFilePicker::Shutdown() { NS_IF_RELEASE(mPrevDisplayDirectory); } + +static GtkFileChooserAction GetGtkFileChooserAction(int16_t aMode) { + GtkFileChooserAction action; + + switch (aMode) { + case nsIFilePicker::modeSave: + action = GTK_FILE_CHOOSER_ACTION_SAVE; + break; + + case nsIFilePicker::modeGetFolder: + action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; + break; + + case nsIFilePicker::modeOpen: + case nsIFilePicker::modeOpenMultiple: + action = GTK_FILE_CHOOSER_ACTION_OPEN; + break; + + default: + NS_WARNING("Unknown nsIFilePicker mode"); + action = GTK_FILE_CHOOSER_ACTION_OPEN; + break; + } + + return action; +} + +static void UpdateFilePreviewWidget(GtkFileChooser* file_chooser, + gpointer preview_widget_voidptr) { + GtkImage* preview_widget = GTK_IMAGE(preview_widget_voidptr); + char* image_filename = gtk_file_chooser_get_preview_filename(file_chooser); + struct stat st_buf; + + if (!image_filename) { + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); + return; + } + + gint preview_width = 0; + gint preview_height = 0; + /* check type of file + * if file is named pipe, Open is blocking which may lead to UI + * nonresponsiveness; if file is directory/socket, it also isn't + * likely to get preview */ + if (stat(image_filename, &st_buf) || (!S_ISREG(st_buf.st_mode))) { + g_free(image_filename); + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); + return; /* stat failed or file is not regular */ + } + + GdkPixbufFormat* preview_format = + gdk_pixbuf_get_file_info(image_filename, &preview_width, &preview_height); + if (!preview_format || preview_width <= 0 || preview_height <= 0 || + preview_width > MAX_PREVIEW_SOURCE_SIZE || + preview_height > MAX_PREVIEW_SOURCE_SIZE) { + g_free(image_filename); + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); + return; + } + + GdkPixbuf* preview_pixbuf = nullptr; + // Only scale down images that are too big + if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) { + preview_pixbuf = gdk_pixbuf_new_from_file_at_size( + image_filename, MAX_PREVIEW_SIZE, MAX_PREVIEW_SIZE, nullptr); + } else { + preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr); + } + + g_free(image_filename); + + if (!preview_pixbuf) { + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); + return; + } + + GdkPixbuf* preview_pixbuf_temp = preview_pixbuf; + preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp); + g_object_unref(preview_pixbuf_temp); + + // This is the easiest way to do center alignment without worrying about + // containers Minimum 3px padding each side (hence the 6) just to make things + // nice + gint x_padding = + (MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2; + gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0); + + gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf); + g_object_unref(preview_pixbuf); + gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE); +} + +static nsAutoCString MakeCaseInsensitiveShellGlob(const char* aPattern) { + // aPattern is UTF8 + nsAutoCString result; + unsigned int len = strlen(aPattern); + + for (unsigned int i = 0; i < len; i++) { + if (!g_ascii_isalpha(aPattern[i])) { + // non-ASCII characters will also trigger this path, so unicode + // is safely handled albeit case-sensitively + result.Append(aPattern[i]); + continue; + } + + // add the lowercase and uppercase version of a character to a bracket + // match, so it matches either the lowercase or uppercase char. + result.Append('['); + result.Append(g_ascii_tolower(aPattern[i])); + result.Append(g_ascii_toupper(aPattern[i])); + result.Append(']'); + } + + return result; +} + +NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) + +nsFilePicker::nsFilePicker() + : mSelectedType(0), + mRunning(false), + mAllowURLs(false), + mFileChooserDelegate(nullptr) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + // Due to Bug 1635718 always use portal for file dialog on Wayland. + if (gfxPlatformGtk::GetPlatform()->IsWaylandDisplay()) { + mUseNativeFileChooser = + Preferences::GetBool("widget.use-xdg-desktop-portal", true); + } else { + giovfs->ShouldUseFlatpakPortal(&mUseNativeFileChooser); + } +} + +nsFilePicker::~nsFilePicker() = default; + +void ReadMultipleFiles(gpointer filename, gpointer array) { + nsCOMPtr<nsIFile> localfile; + nsresult rv = + NS_NewNativeLocalFile(nsDependentCString(static_cast<char*>(filename)), + false, getter_AddRefs(localfile)); + if (NS_SUCCEEDED(rv)) { + nsCOMArray<nsIFile>& files = *static_cast<nsCOMArray<nsIFile>*>(array); + files.AppendObject(localfile); + } + + g_free(filename); +} + +void nsFilePicker::ReadValuesFromFileChooser(void* file_chooser) { + mFiles.Clear(); + + if (mMode == nsIFilePicker::modeOpenMultiple) { + mFileURL.Truncate(); + + GSList* list = + gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser)); + g_slist_foreach(list, ReadMultipleFiles, static_cast<gpointer>(&mFiles)); + g_slist_free(list); + } else { + gchar* filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser)); + mFileURL.Assign(filename); + g_free(filename); + } + + GtkFileFilter* filter = + gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser)); + GSList* filter_list = + gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser)); + + mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter)); + g_slist_free(filter_list); + + // Remember last used directory. + nsCOMPtr<nsIFile> file; + GetFile(getter_AddRefs(file)); + if (file) { + nsCOMPtr<nsIFile> dir; + file->GetParent(getter_AddRefs(dir)); + if (dir) { + dir.swap(mPrevDisplayDirectory); + } + } +} + +void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) { + mParentWidget = aParent; + mTitle.Assign(aTitle); +} + +NS_IMETHODIMP +nsFilePicker::AppendFilters(int32_t aFilterMask) { + mAllowURLs = !!(aFilterMask & filterAllowURLs); + return nsBaseFilePicker::AppendFilters(aFilterMask); +} + +NS_IMETHODIMP +nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) { + if (aFilter.EqualsLiteral("..apps")) { + // No platform specific thing we can do here, really.... + return NS_OK; + } + + nsAutoCString filter, name; + CopyUTF16toUTF8(aFilter, filter); + CopyUTF16toUTF8(aTitle, name); + + mFilters.AppendElement(filter); + mFilterNames.AppendElement(name); + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::SetDefaultString(const nsAString& aString) { + mDefault = aString; + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetDefaultString(nsAString& aString) { + // Per API... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsFilePicker::SetDefaultExtension(const nsAString& aExtension) { + mDefaultExtension = aExtension; + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetDefaultExtension(nsAString& aExtension) { + aExtension = mDefaultExtension; + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) { + *aFilterIndex = mSelectedType; + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::SetFilterIndex(int32_t aFilterIndex) { + mSelectedType = aFilterIndex; + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + + *aFile = nullptr; + nsCOMPtr<nsIURI> uri; + nsresult rv = GetFileURL(getter_AddRefs(uri)); + if (!uri) return rv; + + nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + file.forget(aFile); + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetFileURL(nsIURI** aFileURL) { + *aFileURL = nullptr; + return NS_NewURI(aFileURL, mFileURL); +} + +NS_IMETHODIMP +nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) { + NS_ENSURE_ARG_POINTER(aFiles); + + if (mMode == nsIFilePicker::modeOpenMultiple) { + return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile)); + } + + return NS_ERROR_FAILURE; +} + +nsresult nsFilePicker::Show(int16_t* aReturn) { + NS_ENSURE_ARG_POINTER(aReturn); + + nsresult rv = Open(nullptr); + if (NS_FAILED(rv)) return rv; + + while (mRunning) { + g_main_context_iteration(nullptr, TRUE); + } + + *aReturn = mResult; + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) { + // Can't show two dialogs concurrently with the same filepicker + if (mRunning) return NS_ERROR_NOT_AVAILABLE; + + NS_ConvertUTF16toUTF8 title(mTitle); + + GtkWindow* parent_widget = + GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET)); + + GtkFileChooserAction action = GetGtkFileChooserAction(mMode); + + const gchar* accept_button; + NS_ConvertUTF16toUTF8 buttonLabel(mOkButtonLabel); + if (!mOkButtonLabel.IsEmpty()) { + accept_button = buttonLabel.get(); + } else { + accept_button = nullptr; + } + + void* file_chooser = + GtkFileChooserNew(title.get(), parent_widget, action, accept_button); + + // If we have --enable-proxy-bypass-protection, then don't allow + // remote URLs to be used. +#ifndef MOZ_PROXY_BYPASS_PROTECTION + if (mAllowURLs) { + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE); + } +#endif + + if (action == GTK_FILE_CHOOSER_ACTION_OPEN || + action == GTK_FILE_CHOOSER_ACTION_SAVE) { + GtkWidget* img_preview = gtk_image_new(); + gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(file_chooser), + img_preview); + g_signal_connect(file_chooser, "update-preview", + G_CALLBACK(UpdateFilePreviewWidget), img_preview); + } + + GtkFileChooserSetModal(file_chooser, parent_widget, TRUE); + + NS_ConvertUTF16toUTF8 defaultName(mDefault); + switch (mMode) { + case nsIFilePicker::modeOpenMultiple: + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser), + TRUE); + break; + case nsIFilePicker::modeSave: + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser), + defaultName.get()); + break; + } + + nsCOMPtr<nsIFile> defaultPath; + if (mDisplayDirectory) { + mDisplayDirectory->Clone(getter_AddRefs(defaultPath)); + } else if (mPrevDisplayDirectory) { + mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath)); + } + + if (defaultPath) { + if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) { + // Try to select the intended file. Even if it doesn't exist, GTK still + // switches directories. + defaultPath->AppendNative(defaultName); + nsAutoCString path; + defaultPath->GetNativePath(path); + gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get()); + } else { + nsAutoCString directory; + defaultPath->GetNativePath(directory); + + // Workaround for problematic refcounting in GTK3 before 3.16. + // We need to keep a reference to the dialog's internal delegate. + // Otherwise, if our dialog gets destroyed, we'll lose the dialog's + // delegate by the time this gets processed in the event loop. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1166741 + if (GTK_IS_DIALOG(file_chooser)) { + GtkDialog* dialog = GTK_DIALOG(file_chooser); + GtkContainer* area = GTK_CONTAINER(gtk_dialog_get_content_area(dialog)); + gtk_container_forall( + area, + [](GtkWidget* widget, gpointer data) { + if (GTK_IS_FILE_CHOOSER_WIDGET(widget)) { + auto result = static_cast<GtkFileChooserWidget**>(data); + *result = GTK_FILE_CHOOSER_WIDGET(widget); + } + }, + &mFileChooserDelegate); + + if (mFileChooserDelegate != nullptr) { + g_object_ref(mFileChooserDelegate); + } + } + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser), + directory.get()); + } + } + + if (GTK_IS_DIALOG(file_chooser)) { + gtk_dialog_set_default_response(GTK_DIALOG(file_chooser), + GTK_RESPONSE_ACCEPT); + } + + int32_t count = mFilters.Length(); + for (int32_t i = 0; i < count; ++i) { + // This is fun... the GTK file picker does not accept a list of filters + // so we need to split out each string, and add it manually. + + char** patterns = g_strsplit(mFilters[i].get(), ";", -1); + if (!patterns) { + return NS_ERROR_OUT_OF_MEMORY; + } + + GtkFileFilter* filter = gtk_file_filter_new(); + for (int j = 0; patterns[j] != nullptr; ++j) { + nsAutoCString caseInsensitiveFilter = + MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j])); + gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get()); + } + + g_strfreev(patterns); + + if (!mFilterNames[i].IsEmpty()) { + // If we have a name for our filter, let's use that. + const char* filter_name = mFilterNames[i].get(); + gtk_file_filter_set_name(filter, filter_name); + } else { + // If we don't have a name, let's just use the filter pattern. + const char* filter_pattern = mFilters[i].get(); + gtk_file_filter_set_name(filter, filter_pattern); + } + + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter); + + // Set the initially selected filter + if (mSelectedType == i) { + gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter); + } + } + + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser), + TRUE); + + mRunning = true; + mCallback = aCallback; + NS_ADDREF_THIS(); + g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this); + GtkFileChooserShow(file_chooser); + + return NS_OK; +} + +/* static */ +void nsFilePicker::OnResponse(void* file_chooser, gint response_id, + gpointer user_data) { + static_cast<nsFilePicker*>(user_data)->Done(file_chooser, response_id); +} + +/* static */ +void nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) { + static_cast<nsFilePicker*>(user_data)->Done(file_chooser, + GTK_RESPONSE_CANCEL); +} + +void nsFilePicker::Done(void* file_chooser, gint response) { + mRunning = false; + + int16_t result; + switch (response) { + case GTK_RESPONSE_OK: + case GTK_RESPONSE_ACCEPT: + ReadValuesFromFileChooser(file_chooser); + result = nsIFilePicker::returnOK; + if (mMode == nsIFilePicker::modeSave) { + nsCOMPtr<nsIFile> file; + GetFile(getter_AddRefs(file)); + if (file) { + bool exists = false; + file->Exists(&exists); + if (exists) result = nsIFilePicker::returnReplace; + } + } + break; + + case GTK_RESPONSE_CANCEL: + case GTK_RESPONSE_CLOSE: + case GTK_RESPONSE_DELETE_EVENT: + result = nsIFilePicker::returnCancel; + break; + + default: + NS_WARNING("Unexpected response"); + result = nsIFilePicker::returnCancel; + break; + } + + // A "response" signal won't be sent again but "destroy" will be. + g_signal_handlers_disconnect_by_func(file_chooser, FuncToGpointer(OnDestroy), + this); + + // When response_id is GTK_RESPONSE_DELETE_EVENT or when called from + // OnDestroy, the widget would be destroyed anyway but it is fine if + // gtk_widget_destroy is called more than once. gtk_widget_destroy has + // requests that any remaining references be released, but the reference + // count will not be decremented again if GtkWindow's reference has already + // been released. + GtkFileChooserDestroy(file_chooser); + + if (mFileChooserDelegate) { + // Properly deref our acquired reference. We call this after + // gtk_widget_destroy() to try and ensure that pending file info + // queries caused by updating the current folder have been cancelled. + // However, we do not know for certain when the callback will run after + // cancelled. + g_idle_add( + [](gpointer data) -> gboolean { + g_object_unref(data); + return G_SOURCE_REMOVE; + }, + mFileChooserDelegate); + mFileChooserDelegate = nullptr; + } + + if (mCallback) { + mCallback->Done(result); + mCallback = nullptr; + } else { + mResult = result; + } + NS_RELEASE_THIS(); +} + +// All below functions available as of GTK 3.20+ +void* nsFilePicker::GtkFileChooserNew(const gchar* title, GtkWindow* parent, + GtkFileChooserAction action, + const gchar* accept_label) { + static auto sGtkFileChooserNativeNewPtr = + (void* (*)(const gchar*, GtkWindow*, GtkFileChooserAction, const gchar*, + const gchar*))dlsym(RTLD_DEFAULT, + "gtk_file_chooser_native_new"); + if (mUseNativeFileChooser && sGtkFileChooserNativeNewPtr != nullptr) { + return (*sGtkFileChooserNativeNewPtr)(title, parent, action, accept_label, + nullptr); + } + if (accept_label == nullptr) { + accept_label = (action == GTK_FILE_CHOOSER_ACTION_SAVE) ? GTK_STOCK_SAVE + : GTK_STOCK_OPEN; + } + GtkWidget* file_chooser = gtk_file_chooser_dialog_new( + title, parent, action, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + accept_label, GTK_RESPONSE_ACCEPT, nullptr); + gtk_dialog_set_alternative_button_order( + GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1); + return file_chooser; +} + +void nsFilePicker::GtkFileChooserShow(void* file_chooser) { + static auto sGtkNativeDialogShowPtr = + (void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_show"); + if (mUseNativeFileChooser && sGtkNativeDialogShowPtr != nullptr) { + const char* portalEnvString = g_getenv("GTK_USE_PORTAL"); + bool setPortalEnv = + (portalEnvString && atoi(portalEnvString) == 0) || !portalEnvString; + if (setPortalEnv) { + setenv("GTK_USE_PORTAL", "1", true); + } + (*sGtkNativeDialogShowPtr)(file_chooser); + if (setPortalEnv) { + unsetenv("GTK_USE_PORTAL"); + } + } else { + g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this); + gtk_widget_show(GTK_WIDGET(file_chooser)); + } +} + +void nsFilePicker::GtkFileChooserDestroy(void* file_chooser) { + static auto sGtkNativeDialogDestroyPtr = + (void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_destroy"); + if (mUseNativeFileChooser && sGtkNativeDialogDestroyPtr != nullptr) { + (*sGtkNativeDialogDestroyPtr)(file_chooser); + } else { + gtk_widget_destroy(GTK_WIDGET(file_chooser)); + } +} + +void nsFilePicker::GtkFileChooserSetModal(void* file_chooser, + GtkWindow* parent_widget, + gboolean modal) { + static auto sGtkNativeDialogSetModalPtr = (void (*)(void*, gboolean))dlsym( + RTLD_DEFAULT, "gtk_native_dialog_set_modal"); + if (mUseNativeFileChooser && sGtkNativeDialogSetModalPtr != nullptr) { + (*sGtkNativeDialogSetModalPtr)(file_chooser, modal); + } else { + GtkWindow* window = GTK_WINDOW(file_chooser); + gtk_window_set_modal(window, modal); + if (parent_widget != nullptr) { + gtk_window_set_destroy_with_parent(window, modal); + } + } +} diff --git a/widget/gtk/nsFilePicker.h b/widget/gtk/nsFilePicker.h new file mode 100644 index 0000000000..9b3110aa00 --- /dev/null +++ b/widget/gtk/nsFilePicker.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsFilePicker_h__ +#define nsFilePicker_h__ + +#include <gtk/gtk.h> + +#include "nsBaseFilePicker.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsCOMArray.h" + +class nsIWidget; +class nsIFile; + +class nsFilePicker : public nsBaseFilePicker { + public: + nsFilePicker(); + + NS_DECL_ISUPPORTS + + // nsIFilePicker (less what's in nsBaseFilePicker) + NS_IMETHOD Open(nsIFilePickerShownCallback* aCallback) override; + NS_IMETHOD AppendFilters(int32_t aFilterMask) override; + NS_IMETHOD AppendFilter(const nsAString& aTitle, + const nsAString& aFilter) override; + NS_IMETHOD SetDefaultString(const nsAString& aString) override; + NS_IMETHOD GetDefaultString(nsAString& aString) override; + NS_IMETHOD SetDefaultExtension(const nsAString& aExtension) override; + NS_IMETHOD GetDefaultExtension(nsAString& aExtension) override; + NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override; + NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override; + NS_IMETHOD GetFile(nsIFile** aFile) override; + NS_IMETHOD GetFileURL(nsIURI** aFileURL) override; + NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override; + + // nsBaseFilePicker + virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) override; + + static void Shutdown(); + + protected: + virtual ~nsFilePicker(); + + nsresult Show(int16_t* aReturn) override; + void ReadValuesFromFileChooser(void* file_chooser); + + static void OnResponse(void* file_chooser, gint response_id, + gpointer user_data); + static void OnDestroy(GtkWidget* file_chooser, gpointer user_data); + void Done(void* file_chooser, gint response_id); + + nsCOMPtr<nsIWidget> mParentWidget; + nsCOMPtr<nsIFilePickerShownCallback> mCallback; + nsCOMArray<nsIFile> mFiles; + + int16_t mSelectedType; + int16_t mResult; + bool mRunning; + bool mAllowURLs; + nsCString mFileURL; + nsString mTitle; + nsString mDefault; + nsString mDefaultExtension; + + nsTArray<nsCString> mFilters; + nsTArray<nsCString> mFilterNames; + + private: + static nsIFile* mPrevDisplayDirectory; + + void* GtkFileChooserNew(const gchar* title, GtkWindow* parent, + GtkFileChooserAction action, + const gchar* accept_label); + void GtkFileChooserShow(void* file_chooser); + void GtkFileChooserDestroy(void* file_chooser); + void GtkFileChooserSetModal(void* file_chooser, GtkWindow* parent_widget, + gboolean modal); + + GtkFileChooserWidget* mFileChooserDelegate; + bool mUseNativeFileChooser; +}; + +#endif diff --git a/widget/gtk/nsGTKToolkit.h b/widget/gtk/nsGTKToolkit.h new file mode 100644 index 0000000000..a4708d5466 --- /dev/null +++ b/widget/gtk/nsGTKToolkit.h @@ -0,0 +1,53 @@ +/* -*- 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 GTKTOOLKIT_H +#define GTKTOOLKIT_H + +#include "nsString.h" +#include <gtk/gtk.h> + +/** + * Wrapper around the thread running the message pump. + * The toolkit abstraction is necessary because the message pump must + * execute within the same thread that created the widget under Win32. + */ + +class nsGTKToolkit { + public: + nsGTKToolkit(); + + static nsGTKToolkit* GetToolkit(); + + static void Shutdown() { + delete gToolkit; + gToolkit = nullptr; + } + + /** + * Get/set our value of DESKTOP_STARTUP_ID. When non-empty, this is applied + * to the next toplevel window to be shown or focused (and then immediately + * cleared). + */ + void SetDesktopStartupID(const nsACString& aID) { mDesktopStartupID = aID; } + void GetDesktopStartupID(nsACString* aID) { *aID = mDesktopStartupID; } + + /** + * Get/set the timestamp value to be used, if non-zero, to focus the + * next top-level window to be shown or focused (upon which it is cleared). + */ + void SetFocusTimestamp(uint32_t aTimestamp) { mFocusTimestamp = aTimestamp; } + uint32_t GetFocusTimestamp() { return mFocusTimestamp; } + + private: + static nsGTKToolkit* gToolkit; + + nsCString mDesktopStartupID; + uint32_t mFocusTimestamp; +}; + +#endif // GTKTOOLKIT_H diff --git a/widget/gtk/nsGtkCursors.h b/widget/gtk/nsGtkCursors.h new file mode 100644 index 0000000000..f30ed593a9 --- /dev/null +++ b/widget/gtk/nsGtkCursors.h @@ -0,0 +1,416 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 nsGtkCursors_h__ +#define nsGtkCursors_h__ + +typedef struct { + const unsigned char* bits; + const unsigned char* mask_bits; + int hot_x; + int hot_y; + const char* hash; +} nsGtkCursor; + +/* MOZ_CURSOR_HAND_GRAB */ +static const unsigned char moz_hand_grab_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x60, 0x39, 0x00, 0x00, 0x90, 0x49, 0x00, 0x00, 0x90, 0x49, 0x01, 0x00, + 0x20, 0xc9, 0x02, 0x00, 0x20, 0x49, 0x02, 0x00, 0x58, 0x40, 0x02, 0x00, + 0x64, 0x00, 0x02, 0x00, 0x44, 0x00, 0x01, 0x00, 0x08, 0x00, 0x01, 0x00, + 0x10, 0x00, 0x01, 0x00, 0x10, 0x80, 0x00, 0x00, 0x20, 0x80, 0x00, 0x00, + 0x40, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_hand_grab_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x60, 0x3f, 0x00, 0x00, + 0xf0, 0x7f, 0x00, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf8, 0xff, 0x03, 0x00, + 0xf0, 0xff, 0x07, 0x00, 0xf8, 0xff, 0x07, 0x00, 0xfc, 0xff, 0x07, 0x00, + 0xfe, 0xff, 0x07, 0x00, 0xfe, 0xff, 0x03, 0x00, 0xfc, 0xff, 0x03, 0x00, + 0xf8, 0xff, 0x03, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x01, 0x00, + 0xe0, 0xff, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_HAND_GRABBING */ +static const unsigned char moz_hand_grabbing_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x36, 0x00, 0x00, 0x20, 0xc9, 0x00, 0x00, 0x20, 0x40, 0x01, 0x00, + 0x40, 0x00, 0x01, 0x00, 0x60, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, 0x00, + 0x10, 0x00, 0x01, 0x00, 0x10, 0x80, 0x00, 0x00, 0x20, 0x80, 0x00, 0x00, + 0x40, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_hand_grabbing_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x36, 0x00, 0x00, + 0xe0, 0xff, 0x00, 0x00, 0xf0, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x03, 0x00, + 0xe0, 0xff, 0x03, 0x00, 0xf0, 0xff, 0x03, 0x00, 0xf8, 0xff, 0x03, 0x00, + 0xf8, 0xff, 0x03, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x01, 0x00, + 0xe0, 0xff, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, 0xc0, 0x7f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_COPY */ +static const unsigned char moz_copy_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x7c, 0x30, 0x00, 0x00, 0x6c, 0x30, 0x00, 0x00, + 0xc4, 0xfc, 0x00, 0x00, 0xc0, 0xfc, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, + 0x80, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_copy_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, + 0xfe, 0x37, 0x00, 0x00, 0xfe, 0x7b, 0x00, 0x00, 0xfe, 0xfc, 0x00, 0x00, + 0xee, 0xff, 0x01, 0x00, 0xe4, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x00, 0x00, + 0xc0, 0x7b, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_ALIAS */ +static const unsigned char moz_alias_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x7c, 0xf0, 0x00, 0x00, 0x6c, 0xe0, 0x00, 0x00, + 0xc4, 0xf0, 0x00, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, + 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_alias_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, + 0xfe, 0xf7, 0x00, 0x00, 0xfe, 0xfb, 0x01, 0x00, 0xfe, 0xf0, 0x01, 0x00, + 0xee, 0xf9, 0x01, 0x00, 0xe4, 0xf9, 0x01, 0x00, 0xc0, 0xbf, 0x00, 0x00, + 0xc0, 0x3f, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_CONTEXT_MENU */ +static const unsigned char moz_menu_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0xfd, 0x00, 0x00, + 0xfc, 0xff, 0x00, 0x00, 0x7c, 0x84, 0x00, 0x00, 0x6c, 0xfc, 0x00, 0x00, + 0xc4, 0x84, 0x00, 0x00, 0xc0, 0xfc, 0x00, 0x00, 0x80, 0x85, 0x00, 0x00, + 0x80, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_menu_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0xfe, 0xfd, 0x00, 0x00, 0xfe, 0xff, 0x01, 0x00, + 0xfe, 0xff, 0x01, 0x00, 0xfe, 0xff, 0x01, 0x00, 0xfe, 0xfe, 0x01, 0x00, + 0xee, 0xff, 0x01, 0x00, 0xe4, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, + 0xc0, 0xff, 0x01, 0x00, 0x80, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_SPINNING */ +static const unsigned char moz_spinning_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, + 0xfc, 0x3b, 0x00, 0x00, 0x7c, 0x38, 0x00, 0x00, 0x6c, 0x54, 0x00, 0x00, + 0xc4, 0xdc, 0x00, 0x00, 0xc0, 0x44, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, + 0x80, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_spinning_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfe, 0x3b, 0x00, 0x00, + 0xfe, 0x7f, 0x00, 0x00, 0xfe, 0x7f, 0x00, 0x00, 0xfe, 0xfe, 0x00, 0x00, + 0xee, 0xff, 0x01, 0x00, 0xe4, 0xff, 0x00, 0x00, 0xc0, 0x7f, 0x00, 0x00, + 0xc0, 0x7f, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_ZOOM_IN */ +static const unsigned char moz_zoom_in_bits[] = { + 0xf0, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, + 0x62, 0x04, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0xf9, 0x09, 0x00, 0x00, + 0xf9, 0x09, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0x62, 0x04, 0x00, 0x00, + 0x02, 0x04, 0x00, 0x00, 0x0c, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_zoom_in_mask_bits[] = { + 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, + 0xfe, 0x07, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, + 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, + 0xfe, 0x07, 0x00, 0x00, 0xfc, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_ZOOM_OUT */ +static const unsigned char moz_zoom_out_bits[] = { + 0xf0, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, + 0x02, 0x04, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0xf9, 0x09, 0x00, 0x00, + 0xf9, 0x09, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, + 0x02, 0x04, 0x00, 0x00, 0x0c, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_zoom_out_mask_bits[] = { + 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, + 0xfe, 0x07, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, + 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, + 0xfe, 0x07, 0x00, 0x00, 0xfc, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_NOT_ALLOWED */ +static const unsigned char moz_not_allowed_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0xe0, 0x7f, 0x00, 0x00, + 0xf0, 0xf0, 0x00, 0x00, 0x38, 0xc0, 0x01, 0x00, 0x7c, 0x80, 0x03, 0x00, + 0xec, 0x00, 0x03, 0x00, 0xce, 0x01, 0x07, 0x00, 0x86, 0x03, 0x06, 0x00, + 0x06, 0x07, 0x06, 0x00, 0x06, 0x0e, 0x06, 0x00, 0x06, 0x1c, 0x06, 0x00, + 0x0e, 0x38, 0x07, 0x00, 0x0c, 0x70, 0x03, 0x00, 0x1c, 0xe0, 0x03, 0x00, + 0x38, 0xc0, 0x01, 0x00, 0xf0, 0xf0, 0x00, 0x00, 0xe0, 0x7f, 0x00, 0x00, + 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_not_allowed_mask_bits[] = { + 0x80, 0x1f, 0x00, 0x00, 0xe0, 0x7f, 0x00, 0x00, 0xf0, 0xff, 0x00, 0x00, + 0xf8, 0xff, 0x01, 0x00, 0xfc, 0xf0, 0x03, 0x00, 0xfe, 0xc0, 0x07, 0x00, + 0xfe, 0x81, 0x07, 0x00, 0xff, 0x83, 0x0f, 0x00, 0xcf, 0x07, 0x0f, 0x00, + 0x8f, 0x0f, 0x0f, 0x00, 0x0f, 0x1f, 0x0f, 0x00, 0x0f, 0x3e, 0x0f, 0x00, + 0x1f, 0xfc, 0x0f, 0x00, 0x1e, 0xf8, 0x07, 0x00, 0x3e, 0xf0, 0x07, 0x00, + 0xfc, 0xf0, 0x03, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x00, 0x00, + 0xe0, 0x7f, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_VERTICAL_TEXT */ +static const unsigned char moz_vertical_text_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, + 0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, + 0x02, 0x40, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_vertical_text_mask_bits[] = { + 0x07, 0xe0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0xfe, 0x7f, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, + 0x0f, 0xf0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_NESW_RESIZE */ +static const unsigned char moz_nesw_resize_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x00, 0xbe, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, + 0x02, 0xb4, 0x00, 0x00, 0x02, 0xa2, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00, + 0x8a, 0x80, 0x00, 0x00, 0x5a, 0x80, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0xfe, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_nesw_resize_mask_bits[] = { + 0xc0, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, + 0x00, 0xff, 0x01, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00, + 0x07, 0xfe, 0x01, 0x00, 0x07, 0xf7, 0x01, 0x00, 0x8f, 0xe3, 0x01, 0x00, + 0xdf, 0xc1, 0x01, 0x00, 0xff, 0xc0, 0x01, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xff, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, + 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_NWSE_RESIZE */ +static const unsigned char moz_nwse_resize_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x5a, 0x80, 0x00, 0x00, 0x8a, 0x80, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00, + 0x02, 0xa2, 0x00, 0x00, 0x02, 0xb4, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, + 0x00, 0xbc, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0xc0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_nwse_resize_mask_bits[] = { + 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xff, 0xc0, 0x01, 0x00, 0xdf, 0xc1, 0x01, 0x00, 0x8f, 0xe3, 0x01, 0x00, + 0x07, 0xf7, 0x01, 0x00, 0x07, 0xfe, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00, + 0x00, 0xfe, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, + 0xc0, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* MOZ_CURSOR_NONE */ +static const unsigned char moz_none_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const unsigned char moz_none_mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +enum { + MOZ_CURSOR_HAND_GRAB, + MOZ_CURSOR_HAND_GRABBING, + MOZ_CURSOR_COPY, + MOZ_CURSOR_ALIAS, + MOZ_CURSOR_CONTEXT_MENU, + MOZ_CURSOR_SPINNING, + MOZ_CURSOR_ZOOM_IN, + MOZ_CURSOR_ZOOM_OUT, + MOZ_CURSOR_NOT_ALLOWED, + MOZ_CURSOR_VERTICAL_TEXT, + MOZ_CURSOR_NESW_RESIZE, + MOZ_CURSOR_NWSE_RESIZE, + MOZ_CURSOR_NONE +}; + +// create custom pixmap cursor. The hash values must stay in sync with the +// bitmap data above. To see the hash function, have a look at XcursorImageHash +// in libXcursor +static const nsGtkCursor GtkCursors[] = { + {moz_hand_grab_bits, moz_hand_grab_mask_bits, 10, 10, + "5aca4d189052212118709018842178c0"}, + {moz_hand_grabbing_bits, moz_hand_grabbing_mask_bits, 10, 10, + "208530c400c041818281048008011002"}, + {moz_copy_bits, moz_copy_mask_bits, 2, 2, + "08ffe1cb5fe6fc01f906f1c063814ccf"}, + {moz_alias_bits, moz_alias_mask_bits, 2, 2, + "0876e1c15ff2fc01f906f1c363074c0f"}, + {moz_menu_bits, moz_menu_mask_bits, 2, 2, + "08ffe1e65f80fcfdf9fff11263e74c48"}, + {moz_spinning_bits, moz_spinning_mask_bits, 2, 2, + "08e8e1c95fe2fc01f976f1e063a24ccd"}, + {moz_zoom_in_bits, moz_zoom_in_mask_bits, 6, 6, + "f41c0e382c94c0958e07017e42b00462"}, + {moz_zoom_out_bits, moz_zoom_out_mask_bits, 6, 6, + "f41c0e382c97c0938e07017e42800402"}, + {moz_not_allowed_bits, moz_not_allowed_mask_bits, 9, 9, + "03b6e0fcb3499374a867d041f52298f0"}, + {moz_vertical_text_bits, moz_vertical_text_mask_bits, 8, 4, + "048008013003cff3c00c801001200000"}, + {moz_nesw_resize_bits, moz_nesw_resize_mask_bits, 8, 8, + "50585d75b494802d0151028115016902"}, + {moz_nwse_resize_bits, moz_nwse_resize_mask_bits, 8, 8, + "38c5dff7c7b8962045400281044508d2"}, + {moz_none_bits, moz_none_mask_bits, 0, 0, nullptr}}; + +#endif /* nsGtkCursors_h__ */ diff --git a/widget/gtk/nsGtkKeyUtils.cpp b/widget/gtk/nsGtkKeyUtils.cpp new file mode 100644 index 0000000000..41a679eab7 --- /dev/null +++ b/widget/gtk/nsGtkKeyUtils.cpp @@ -0,0 +1,2377 @@ +/* -*- 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 "mozilla/Logging.h" + +#include "nsGtkKeyUtils.h" + +#include <gdk/gdkkeysyms.h> +#include <algorithm> +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <dlfcn.h> +#include <gdk/gdkkeysyms-compat.h> +#include <X11/XKBlib.h> +#include "X11UndefineNone.h" +#include "IMContextWrapper.h" +#include "WidgetUtils.h" +#include "keysym2ucs.h" +#include "nsContentUtils.h" +#include "nsGtkUtils.h" +#include "nsIBidiKeyboard.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsWindow.h" +#include "gfxPlatformGtk.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" + +#ifdef MOZ_WAYLAND +# include <sys/mman.h> +# include "nsWaylandDisplay.h" +#endif + +namespace mozilla { +namespace widget { + +LazyLogModule gKeymapWrapperLog("KeymapWrapperWidgets"); + +#define IS_ASCII_ALPHABETICAL(key) \ + ((('a' <= key) && (key <= 'z')) || (('A' <= key) && (key <= 'Z'))) + +#define MOZ_MODIFIER_KEYS "MozKeymapWrapper" + +KeymapWrapper* KeymapWrapper::sInstance = nullptr; +guint KeymapWrapper::sLastRepeatableHardwareKeyCode = 0; +Time KeymapWrapper::sLastRepeatableKeyTime = 0; +KeymapWrapper::RepeatState KeymapWrapper::sRepeatState = + KeymapWrapper::NOT_PRESSED; + +static const char* GetBoolName(bool aBool) { return aBool ? "TRUE" : "FALSE"; } + +static const char* GetStatusName(nsEventStatus aStatus) { + switch (aStatus) { + case nsEventStatus_eConsumeDoDefault: + return "nsEventStatus_eConsumeDoDefault"; + case nsEventStatus_eConsumeNoDefault: + return "nsEventStatus_eConsumeNoDefault"; + case nsEventStatus_eIgnore: + return "nsEventStatus_eIgnore"; + case nsEventStatus_eSentinel: + return "nsEventStatus_eSentinel"; + default: + return "Illegal value"; + } +} + +static const nsCString GetKeyLocationName(uint32_t aLocation) { + switch (aLocation) { + case eKeyLocationLeft: + return "KEY_LOCATION_LEFT"_ns; + case eKeyLocationRight: + return "KEY_LOCATION_RIGHT"_ns; + case eKeyLocationStandard: + return "KEY_LOCATION_STANDARD"_ns; + case eKeyLocationNumpad: + return "KEY_LOCATION_NUMPAD"_ns; + default: + return nsPrintfCString("Unknown (0x%04X)", aLocation); + } +} + +static const nsCString GetCharacterCodeName(char16_t aChar) { + switch (aChar) { + case 0x0000: + return "NULL (0x0000)"_ns; + case 0x0008: + return "BACKSPACE (0x0008)"_ns; + case 0x0009: + return "CHARACTER TABULATION (0x0009)"_ns; + case 0x000A: + return "LINE FEED (0x000A)"_ns; + case 0x000B: + return "LINE TABULATION (0x000B)"_ns; + case 0x000C: + return "FORM FEED (0x000C)"_ns; + case 0x000D: + return "CARRIAGE RETURN (0x000D)"_ns; + case 0x0018: + return "CANCEL (0x0018)"_ns; + case 0x001B: + return "ESCAPE (0x001B)"_ns; + case 0x0020: + return "SPACE (0x0020)"_ns; + case 0x007F: + return "DELETE (0x007F)"_ns; + case 0x00A0: + return "NO-BREAK SPACE (0x00A0)"_ns; + case 0x00AD: + return "SOFT HYPHEN (0x00AD)"_ns; + case 0x2000: + return "EN QUAD (0x2000)"_ns; + case 0x2001: + return "EM QUAD (0x2001)"_ns; + case 0x2002: + return "EN SPACE (0x2002)"_ns; + case 0x2003: + return "EM SPACE (0x2003)"_ns; + case 0x2004: + return "THREE-PER-EM SPACE (0x2004)"_ns; + case 0x2005: + return "FOUR-PER-EM SPACE (0x2005)"_ns; + case 0x2006: + return "SIX-PER-EM SPACE (0x2006)"_ns; + case 0x2007: + return "FIGURE SPACE (0x2007)"_ns; + case 0x2008: + return "PUNCTUATION SPACE (0x2008)"_ns; + case 0x2009: + return "THIN SPACE (0x2009)"_ns; + case 0x200A: + return "HAIR SPACE (0x200A)"_ns; + case 0x200B: + return "ZERO WIDTH SPACE (0x200B)"_ns; + case 0x200C: + return "ZERO WIDTH NON-JOINER (0x200C)"_ns; + case 0x200D: + return "ZERO WIDTH JOINER (0x200D)"_ns; + case 0x200E: + return "LEFT-TO-RIGHT MARK (0x200E)"_ns; + case 0x200F: + return "RIGHT-TO-LEFT MARK (0x200F)"_ns; + case 0x2029: + return "PARAGRAPH SEPARATOR (0x2029)"_ns; + case 0x202A: + return "LEFT-TO-RIGHT EMBEDDING (0x202A)"_ns; + case 0x202B: + return "RIGHT-TO-LEFT EMBEDDING (0x202B)"_ns; + case 0x202D: + return "LEFT-TO-RIGHT OVERRIDE (0x202D)"_ns; + case 0x202E: + return "RIGHT-TO-LEFT OVERRIDE (0x202E)"_ns; + case 0x202F: + return "NARROW NO-BREAK SPACE (0x202F)"_ns; + case 0x205F: + return "MEDIUM MATHEMATICAL SPACE (0x205F)"_ns; + case 0x2060: + return "WORD JOINER (0x2060)"_ns; + case 0x2066: + return "LEFT-TO-RIGHT ISOLATE (0x2066)"_ns; + case 0x2067: + return "RIGHT-TO-LEFT ISOLATE (0x2067)"_ns; + case 0x3000: + return "IDEOGRAPHIC SPACE (0x3000)"_ns; + case 0xFEFF: + return "ZERO WIDTH NO-BREAK SPACE (0xFEFF)"_ns; + default: { + if (aChar < ' ' || (aChar >= 0x80 && aChar < 0xA0)) { + return nsPrintfCString("control (0x%04X)", aChar); + } + if (NS_IS_HIGH_SURROGATE(aChar)) { + return nsPrintfCString("high surrogate (0x%04X)", aChar); + } + if (NS_IS_LOW_SURROGATE(aChar)) { + return nsPrintfCString("low surrogate (0x%04X)", aChar); + } + return nsPrintfCString("'%s' (0x%04X)", + NS_ConvertUTF16toUTF8(nsAutoString(aChar)).get(), + aChar); + } + } +} + +static const nsCString GetCharacterCodeNames(const char16_t* aChars, + uint32_t aLength) { + if (!aLength) { + return "\"\""_ns; + } + nsCString result; + result.AssignLiteral("\""); + StringJoinAppend(result, ", "_ns, Span{aChars, aLength}, + [](nsACString& dest, const char16_t charValue) { + dest.Append(GetCharacterCodeName(charValue)); + }); + result.AppendLiteral("\""); + return result; +} + +static const nsCString GetCharacterCodeNames(const nsAString& aString) { + return GetCharacterCodeNames(aString.BeginReading(), aString.Length()); +} + +/* static */ const char* KeymapWrapper::GetModifierName(Modifier aModifier) { + switch (aModifier) { + case CAPS_LOCK: + return "CapsLock"; + case NUM_LOCK: + return "NumLock"; + case SCROLL_LOCK: + return "ScrollLock"; + case SHIFT: + return "Shift"; + case CTRL: + return "Ctrl"; + case ALT: + return "Alt"; + case SUPER: + return "Super"; + case HYPER: + return "Hyper"; + case META: + return "Meta"; + case LEVEL3: + return "Level3"; + case LEVEL5: + return "Level5"; + case NOT_MODIFIER: + return "NotModifier"; + default: + return "InvalidValue"; + } +} + +/* static */ KeymapWrapper::Modifier KeymapWrapper::GetModifierForGDKKeyval( + guint aGdkKeyval) { + switch (aGdkKeyval) { + case GDK_Caps_Lock: + return CAPS_LOCK; + case GDK_Num_Lock: + return NUM_LOCK; + case GDK_Scroll_Lock: + return SCROLL_LOCK; + case GDK_Shift_Lock: + case GDK_Shift_L: + case GDK_Shift_R: + return SHIFT; + case GDK_Control_L: + case GDK_Control_R: + return CTRL; + case GDK_Alt_L: + case GDK_Alt_R: + return ALT; + case GDK_Super_L: + case GDK_Super_R: + return SUPER; + case GDK_Hyper_L: + case GDK_Hyper_R: + return HYPER; + case GDK_Meta_L: + case GDK_Meta_R: + return META; + case GDK_ISO_Level3_Shift: + case GDK_Mode_switch: + return LEVEL3; + case GDK_ISO_Level5_Shift: + return LEVEL5; + default: + return NOT_MODIFIER; + } +} + +guint KeymapWrapper::GetModifierMask(Modifier aModifier) const { + switch (aModifier) { + case CAPS_LOCK: + return GDK_LOCK_MASK; + case NUM_LOCK: + return mModifierMasks[INDEX_NUM_LOCK]; + case SCROLL_LOCK: + return mModifierMasks[INDEX_SCROLL_LOCK]; + case SHIFT: + return GDK_SHIFT_MASK; + case CTRL: + return GDK_CONTROL_MASK; + case ALT: + return mModifierMasks[INDEX_ALT]; + case SUPER: + return mModifierMasks[INDEX_SUPER]; + case HYPER: + return mModifierMasks[INDEX_HYPER]; + case META: + return mModifierMasks[INDEX_META]; + case LEVEL3: + return mModifierMasks[INDEX_LEVEL3]; + case LEVEL5: + return mModifierMasks[INDEX_LEVEL5]; + default: + return 0; + } +} + +KeymapWrapper::ModifierKey* KeymapWrapper::GetModifierKey( + guint aHardwareKeycode) { + for (uint32_t i = 0; i < mModifierKeys.Length(); i++) { + ModifierKey& key = mModifierKeys[i]; + if (key.mHardwareKeycode == aHardwareKeycode) { + return &key; + } + } + return nullptr; +} + +/* static */ +KeymapWrapper* KeymapWrapper::GetInstance() { + if (sInstance) { + sInstance->Init(); + return sInstance; + } + + sInstance = new KeymapWrapper(); + return sInstance; +} + +/* static */ +void KeymapWrapper::Shutdown() { + if (sInstance) { + delete sInstance; + sInstance = nullptr; + } +} + +KeymapWrapper::KeymapWrapper() + : mInitialized(false), + mGdkKeymap(gdk_keymap_get_default()), + mXKBBaseEventCode(0), + mOnKeysChangedSignalHandle(0), + mOnDirectionChangedSignalHandle(0) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p Constructor, mGdkKeymap=%p", this, mGdkKeymap)); + + g_object_ref(mGdkKeymap); + + if (gfxPlatformGtk::GetPlatform()->IsX11Display()) { + InitXKBExtension(); + } + + Init(); +} + +void KeymapWrapper::Init() { + if (mInitialized) { + return; + } + mInitialized = true; + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p Init, mGdkKeymap=%p", this, mGdkKeymap)); + + mModifierKeys.Clear(); + memset(mModifierMasks, 0, sizeof(mModifierMasks)); + + if (gfxPlatformGtk::GetPlatform()->IsX11Display()) { + InitBySystemSettingsX11(); + } +#ifdef MOZ_WAYLAND + else { + InitBySystemSettingsWayland(); + } +#endif + + gdk_window_add_filter(nullptr, FilterEvents, this); + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p Init, CapsLock=0x%X, NumLock=0x%X, " + "ScrollLock=0x%X, Level3=0x%X, Level5=0x%X, " + "Shift=0x%X, Ctrl=0x%X, Alt=0x%X, Meta=0x%X, Super=0x%X, Hyper=0x%X", + this, GetModifierMask(CAPS_LOCK), GetModifierMask(NUM_LOCK), + GetModifierMask(SCROLL_LOCK), GetModifierMask(LEVEL3), + GetModifierMask(LEVEL5), GetModifierMask(SHIFT), + GetModifierMask(CTRL), GetModifierMask(ALT), GetModifierMask(META), + GetModifierMask(SUPER), GetModifierMask(HYPER))); +} + +void KeymapWrapper::InitXKBExtension() { + PodZero(&mKeyboardState); + + int xkbMajorVer = XkbMajorVersion; + int xkbMinorVer = XkbMinorVersion; + if (!XkbLibraryVersion(&xkbMajorVer, &xkbMinorVer)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XkbLibraryVersion()", + this)); + return; + } + + Display* display = gdk_x11_display_get_xdisplay(gdk_display_get_default()); + + // XkbLibraryVersion() set xkbMajorVer and xkbMinorVer to that of the + // library, which may be newer than what is required of the server in + // XkbQueryExtension(), so these variables should be reset to + // XkbMajorVersion and XkbMinorVersion before the XkbQueryExtension call. + xkbMajorVer = XkbMajorVersion; + xkbMinorVer = XkbMinorVersion; + int opcode, baseErrorCode; + if (!XkbQueryExtension(display, &opcode, &mXKBBaseEventCode, &baseErrorCode, + &xkbMajorVer, &xkbMinorVer)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XkbQueryExtension(), display=0x%p", + this, display)); + return; + } + + if (!XkbSelectEventDetails(display, XkbUseCoreKbd, XkbStateNotify, + XkbModifierStateMask, XkbModifierStateMask)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XkbSelectEventDetails() for XModifierStateMask, display=0x%p", + this, display)); + return; + } + + if (!XkbSelectEventDetails(display, XkbUseCoreKbd, XkbControlsNotify, + XkbPerKeyRepeatMask, XkbPerKeyRepeatMask)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XkbSelectEventDetails() for XkbControlsNotify, display=0x%p", + this, display)); + return; + } + + if (!XGetKeyboardControl(display, &mKeyboardState)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XGetKeyboardControl(), display=0x%p", + this, display)); + return; + } + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitXKBExtension, Succeeded", this)); +} + +void KeymapWrapper::InitBySystemSettingsX11() { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitBySystemSettingsX11, mGdkKeymap=%p", this, mGdkKeymap)); + + if (!mOnKeysChangedSignalHandle) { + mOnKeysChangedSignalHandle = g_signal_connect( + mGdkKeymap, "keys-changed", (GCallback)OnKeysChanged, this); + } + if (!mOnDirectionChangedSignalHandle) { + mOnDirectionChangedSignalHandle = g_signal_connect( + mGdkKeymap, "direction-changed", (GCallback)OnDirectionChanged, this); + } + + Display* display = gdk_x11_display_get_xdisplay(gdk_display_get_default()); + + int min_keycode = 0; + int max_keycode = 0; + XDisplayKeycodes(display, &min_keycode, &max_keycode); + + int keysyms_per_keycode = 0; + KeySym* xkeymap = + XGetKeyboardMapping(display, min_keycode, max_keycode - min_keycode + 1, + &keysyms_per_keycode); + if (!xkeymap) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitBySystemSettings, " + "Failed due to null xkeymap", + this)); + return; + } + + XModifierKeymap* xmodmap = XGetModifierMapping(display); + if (!xmodmap) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitBySystemSettings, " + "Failed due to null xmodmap", + this)); + XFree(xkeymap); + return; + } + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitBySystemSettings, min_keycode=%d, " + "max_keycode=%d, keysyms_per_keycode=%d, max_keypermod=%d", + this, min_keycode, max_keycode, keysyms_per_keycode, + xmodmap->max_keypermod)); + + // The modifiermap member of the XModifierKeymap structure contains 8 sets + // of max_keypermod KeyCodes, one for each modifier in the order Shift, + // Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5. + // Only nonzero KeyCodes have meaning in each set, and zero KeyCodes are + // ignored. + + // Note that two or more modifiers may use one modifier flag. E.g., + // on Ubuntu 10.10, Alt and Meta share the Mod1 in default settings. + // And also Super and Hyper share the Mod4. In such cases, we need to + // decide which modifier flag means one of DOM modifiers. + + // mod[0] is Modifier introduced by Mod1. + Modifier mod[5]; + int32_t foundLevel[5]; + for (uint32_t i = 0; i < ArrayLength(mod); i++) { + mod[i] = NOT_MODIFIER; + foundLevel[i] = INT32_MAX; + } + const uint32_t map_size = 8 * xmodmap->max_keypermod; + for (uint32_t i = 0; i < map_size; i++) { + KeyCode keycode = xmodmap->modifiermap[i]; + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitBySystemSettings, " + " i=%d, keycode=0x%08X", + this, i, keycode)); + if (!keycode || keycode < min_keycode || keycode > max_keycode) { + continue; + } + + ModifierKey* modifierKey = GetModifierKey(keycode); + if (!modifierKey) { + modifierKey = mModifierKeys.AppendElement(ModifierKey(keycode)); + } + + const KeySym* syms = + xkeymap + (keycode - min_keycode) * keysyms_per_keycode; + const uint32_t bit = i / xmodmap->max_keypermod; + modifierKey->mMask |= 1 << bit; + + // We need to know the meaning of Mod1, Mod2, Mod3, Mod4 and Mod5. + // Let's skip if current map is for others. + if (bit < 3) { + continue; + } + + const int32_t modIndex = bit - 3; + for (int32_t j = 0; j < keysyms_per_keycode; j++) { + Modifier modifier = GetModifierForGDKKeyval(syms[j]); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p InitBySystemSettings, " + " Mod%d, j=%d, syms[j]=%s(0x%lX), modifier=%s", + this, modIndex + 1, j, gdk_keyval_name(syms[j]), syms[j], + GetModifierName(modifier))); + + switch (modifier) { + case NOT_MODIFIER: + // Don't overwrite the stored information with + // NOT_MODIFIER. + break; + case CAPS_LOCK: + case SHIFT: + case CTRL: + // Ignore the modifiers defined in GDK spec. They shouldn't + // be mapped to Mod1-5 because they must not work on native + // GTK applications. + break; + default: + // If new modifier is found in higher level than stored + // value, we don't need to overwrite it. + if (j > foundLevel[modIndex]) { + break; + } + // If new modifier is more important than stored value, + // we should overwrite it with new modifier. + if (j == foundLevel[modIndex]) { + mod[modIndex] = std::min(modifier, mod[modIndex]); + break; + } + foundLevel[modIndex] = j; + mod[modIndex] = modifier; + break; + } + } + } + + for (uint32_t i = 0; i < COUNT_OF_MODIFIER_INDEX; i++) { + Modifier modifier; + switch (i) { + case INDEX_NUM_LOCK: + modifier = NUM_LOCK; + break; + case INDEX_SCROLL_LOCK: + modifier = SCROLL_LOCK; + break; + case INDEX_ALT: + modifier = ALT; + break; + case INDEX_META: + modifier = META; + break; + case INDEX_SUPER: + modifier = SUPER; + break; + case INDEX_HYPER: + modifier = HYPER; + break; + case INDEX_LEVEL3: + modifier = LEVEL3; + break; + case INDEX_LEVEL5: + modifier = LEVEL5; + break; + default: + MOZ_CRASH("All indexes must be handled here"); + } + for (uint32_t j = 0; j < ArrayLength(mod); j++) { + if (modifier == mod[j]) { + mModifierMasks[i] |= 1 << (j + 3); + } + } + } + + XFreeModifiermap(xmodmap); + XFree(xkeymap); +} + +#ifdef MOZ_WAYLAND +void KeymapWrapper::SetModifierMask(xkb_keymap* aKeymap, + ModifierIndex aModifierIndex, + const char* aModifierName) { + static auto sXkbKeymapModGetIndex = + (xkb_mod_index_t(*)(struct xkb_keymap*, const char*))dlsym( + RTLD_DEFAULT, "xkb_keymap_mod_get_index"); + + xkb_mod_index_t index = sXkbKeymapModGetIndex(aKeymap, aModifierName); + if (index != XKB_MOD_INVALID) { + mModifierMasks[aModifierIndex] = (1 << index); + } +} + +void KeymapWrapper::SetModifierMasks(xkb_keymap* aKeymap) { + KeymapWrapper* keymapWrapper = GetInstance(); + + // This mapping is derived from get_xkb_modifiers() at gdkkeys-wayland.c + keymapWrapper->SetModifierMask(aKeymap, INDEX_NUM_LOCK, XKB_MOD_NAME_NUM); + keymapWrapper->SetModifierMask(aKeymap, INDEX_ALT, XKB_MOD_NAME_ALT); + keymapWrapper->SetModifierMask(aKeymap, INDEX_META, "Meta"); + keymapWrapper->SetModifierMask(aKeymap, INDEX_SUPER, "Super"); + keymapWrapper->SetModifierMask(aKeymap, INDEX_HYPER, "Hyper"); + + keymapWrapper->SetModifierMask(aKeymap, INDEX_SCROLL_LOCK, "ScrollLock"); + keymapWrapper->SetModifierMask(aKeymap, INDEX_LEVEL3, "Level3"); + keymapWrapper->SetModifierMask(aKeymap, INDEX_LEVEL5, "Level5"); + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p KeymapWrapper::SetModifierMasks, CapsLock=0x%X, NumLock=0x%X, " + "ScrollLock=0x%X, Level3=0x%X, Level5=0x%X, " + "Shift=0x%X, Ctrl=0x%X, Alt=0x%X, Meta=0x%X, Super=0x%X, Hyper=0x%X", + keymapWrapper, keymapWrapper->GetModifierMask(CAPS_LOCK), + keymapWrapper->GetModifierMask(NUM_LOCK), + keymapWrapper->GetModifierMask(SCROLL_LOCK), + keymapWrapper->GetModifierMask(LEVEL3), + keymapWrapper->GetModifierMask(LEVEL5), + keymapWrapper->GetModifierMask(SHIFT), + keymapWrapper->GetModifierMask(CTRL), + keymapWrapper->GetModifierMask(ALT), + keymapWrapper->GetModifierMask(META), + keymapWrapper->GetModifierMask(SUPER), + keymapWrapper->GetModifierMask(HYPER))); +} + +/* This keymap routine is derived from weston-2.0.0/clients/simple-im.c + */ +static void keyboard_handle_keymap(void* data, struct wl_keyboard* wl_keyboard, + uint32_t format, int fd, uint32_t size) { + KeymapWrapper::ResetKeyboard(); + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + close(fd); + return; + } + + char* mapString = (char*)mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + if (mapString == MAP_FAILED) { + close(fd); + return; + } + + static auto sXkbContextNew = + (struct xkb_context * (*)(enum xkb_context_flags)) + dlsym(RTLD_DEFAULT, "xkb_context_new"); + static auto sXkbKeymapNewFromString = + (struct xkb_keymap * (*)(struct xkb_context*, const char*, + enum xkb_keymap_format, + enum xkb_keymap_compile_flags)) + dlsym(RTLD_DEFAULT, "xkb_keymap_new_from_string"); + + struct xkb_context* xkb_context = sXkbContextNew(XKB_CONTEXT_NO_FLAGS); + struct xkb_keymap* keymap = + sXkbKeymapNewFromString(xkb_context, mapString, XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + munmap(mapString, size); + close(fd); + + if (!keymap) { + NS_WARNING("keyboard_handle_keymap(): Failed to compile keymap!\n"); + return; + } + + KeymapWrapper::SetModifierMasks(keymap); + + static auto sXkbKeymapUnRef = + (void (*)(struct xkb_keymap*))dlsym(RTLD_DEFAULT, "xkb_keymap_unref"); + sXkbKeymapUnRef(keymap); + + static auto sXkbContextUnref = + (void (*)(struct xkb_context*))dlsym(RTLD_DEFAULT, "xkb_context_unref"); + sXkbContextUnref(xkb_context); +} + +static void keyboard_handle_enter(void* data, struct wl_keyboard* keyboard, + uint32_t serial, struct wl_surface* surface, + struct wl_array* keys) {} +static void keyboard_handle_leave(void* data, struct wl_keyboard* keyboard, + uint32_t serial, struct wl_surface* surface) { +} +static void keyboard_handle_key(void* data, struct wl_keyboard* keyboard, + uint32_t serial, uint32_t time, uint32_t key, + uint32_t state) {} +static void keyboard_handle_modifiers(void* data, struct wl_keyboard* keyboard, + uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, + uint32_t mods_locked, uint32_t group) {} + +static const struct wl_keyboard_listener keyboard_listener = { + keyboard_handle_keymap, keyboard_handle_enter, keyboard_handle_leave, + keyboard_handle_key, keyboard_handle_modifiers, +}; + +static void seat_handle_capabilities(void* data, struct wl_seat* seat, + unsigned int caps) { + static wl_keyboard* keyboard = nullptr; + + if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !keyboard) { + keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr); + } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && keyboard) { + wl_keyboard_destroy(keyboard); + keyboard = nullptr; + } +} + +static const struct wl_seat_listener seat_listener = { + seat_handle_capabilities, +}; + +static void gdk_registry_handle_global(void* data, struct wl_registry* registry, + uint32_t id, const char* interface, + uint32_t version) { + if (strcmp(interface, "wl_seat") == 0) { + auto* seat = + WaylandRegistryBind<wl_seat>(registry, id, &wl_seat_interface, 1); + wl_seat_add_listener(seat, &seat_listener, data); + } +} + +static void gdk_registry_handle_global_remove(void* data, + struct wl_registry* registry, + uint32_t id) {} + +static const struct wl_registry_listener keyboard_registry_listener = { + gdk_registry_handle_global, gdk_registry_handle_global_remove}; + +void KeymapWrapper::InitBySystemSettingsWayland() { + wl_display* display = WaylandDisplayGetWLDisplay(); + wl_registry_add_listener(wl_display_get_registry(display), + &keyboard_registry_listener, this); +} +#endif + +KeymapWrapper::~KeymapWrapper() { + gdk_window_remove_filter(nullptr, FilterEvents, this); + if (mOnKeysChangedSignalHandle) { + g_signal_handler_disconnect(mGdkKeymap, mOnKeysChangedSignalHandle); + } + if (mOnDirectionChangedSignalHandle) { + g_signal_handler_disconnect(mGdkKeymap, mOnDirectionChangedSignalHandle); + } + g_object_unref(mGdkKeymap); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, ("%p Destructor", this)); +} + +/* static */ +GdkFilterReturn KeymapWrapper::FilterEvents(GdkXEvent* aXEvent, + GdkEvent* aGdkEvent, + gpointer aData) { + XEvent* xEvent = static_cast<XEvent*>(aXEvent); + switch (xEvent->type) { + case KeyPress: { + // If the key doesn't support auto repeat, ignore the event because + // even if such key (e.g., Shift) is pressed during auto repeat of + // anoter key, it doesn't stop the auto repeat. + KeymapWrapper* self = static_cast<KeymapWrapper*>(aData); + if (!self->IsAutoRepeatableKey(xEvent->xkey.keycode)) { + break; + } + if (sRepeatState == NOT_PRESSED) { + sRepeatState = FIRST_PRESS; + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("FilterEvents(aXEvent={ type=KeyPress, " + "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, " + "aGdkEvent={ state=0x%08X }), " + "detected first keypress", + xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time, + reinterpret_cast<GdkEventKey*>(aGdkEvent)->state)); + } else if (sLastRepeatableHardwareKeyCode == xEvent->xkey.keycode) { + if (sLastRepeatableKeyTime == xEvent->xkey.time && + sLastRepeatableHardwareKeyCode == + IMContextWrapper:: + GetWaitingSynthesizedKeyPressHardwareKeyCode()) { + // On some environment, IM may generate duplicated KeyPress event + // without any special state flags. In such case, we shouldn't + // treat the event as "repeated". + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("FilterEvents(aXEvent={ type=KeyPress, " + "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, " + "aGdkEvent={ state=0x%08X }), " + "igored keypress since it must be synthesized by IME", + xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time, + reinterpret_cast<GdkEventKey*>(aGdkEvent)->state)); + break; + } + sRepeatState = REPEATING; + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("FilterEvents(aXEvent={ type=KeyPress, " + "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, " + "aGdkEvent={ state=0x%08X }), " + "detected repeating keypress", + xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time, + reinterpret_cast<GdkEventKey*>(aGdkEvent)->state)); + } else { + // If a different key is pressed while another key is pressed, + // auto repeat system repeats only the last pressed key. + // So, setting new keycode and setting repeat state as first key + // press should work fine. + sRepeatState = FIRST_PRESS; + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("FilterEvents(aXEvent={ type=KeyPress, " + "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, " + "aGdkEvent={ state=0x%08X }), " + "detected different keypress", + xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time, + reinterpret_cast<GdkEventKey*>(aGdkEvent)->state)); + } + sLastRepeatableHardwareKeyCode = xEvent->xkey.keycode; + sLastRepeatableKeyTime = xEvent->xkey.time; + break; + } + case KeyRelease: { + if (sLastRepeatableHardwareKeyCode != xEvent->xkey.keycode) { + // This case means the key release event is caused by + // a non-repeatable key such as Shift or a repeatable key that + // was pressed before sLastRepeatableHardwareKeyCode was + // pressed. + break; + } + sRepeatState = NOT_PRESSED; + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("FilterEvents(aXEvent={ type=KeyRelease, " + "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, " + "aGdkEvent={ state=0x%08X }), " + "detected key release", + xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time, + reinterpret_cast<GdkEventKey*>(aGdkEvent)->state)); + break; + } + case FocusOut: { + // At moving focus, we should reset keyboard repeat state. + // Strictly, this causes incorrect behavior. However, this + // correctness must be enough for web applications. + sRepeatState = NOT_PRESSED; + break; + } + default: { + KeymapWrapper* self = static_cast<KeymapWrapper*>(aData); + if (xEvent->type != self->mXKBBaseEventCode) { + break; + } + XkbEvent* xkbEvent = (XkbEvent*)xEvent; + if (xkbEvent->any.xkb_type != XkbControlsNotify || + !(xkbEvent->ctrls.changed_ctrls & XkbPerKeyRepeatMask)) { + break; + } + if (!XGetKeyboardControl(xkbEvent->any.display, &self->mKeyboardState)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p FilterEvents failed due to failure " + "of XGetKeyboardControl(), display=0x%p", + self, xkbEvent->any.display)); + } + break; + } + } + + return GDK_FILTER_CONTINUE; +} + +static void ResetBidiKeyboard() { + // Reset the bidi keyboard settings for the new GdkKeymap + nsCOMPtr<nsIBidiKeyboard> bidiKeyboard = nsContentUtils::GetBidiKeyboard(); + if (bidiKeyboard) { + bidiKeyboard->Reset(); + } + WidgetUtils::SendBidiKeyboardInfoToContent(); +} + +/* static */ +void KeymapWrapper::ResetKeyboard() { + sInstance->mInitialized = false; + ResetBidiKeyboard(); +} + +/* static */ +void KeymapWrapper::OnKeysChanged(GdkKeymap* aGdkKeymap, + KeymapWrapper* aKeymapWrapper) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("OnKeysChanged, aGdkKeymap=%p, aKeymapWrapper=%p", aGdkKeymap, + aKeymapWrapper)); + + MOZ_ASSERT(sInstance == aKeymapWrapper, + "This instance must be the singleton instance"); + + // We cannot reintialize here becasue we don't have GdkWindow which is using + // the GdkKeymap. We'll reinitialize it when next GetInstance() is called. + ResetKeyboard(); +} + +// static +void KeymapWrapper::OnDirectionChanged(GdkKeymap* aGdkKeymap, + KeymapWrapper* aKeymapWrapper) { + // XXX + // A lot of diretion-changed signal might be fired on switching bidi + // keyboard when using both ibus (with arabic layout) and fcitx (with IME). + // See https://github.com/fcitx/fcitx/issues/257 + // + // Also, when using ibus, switching to IM might not cause this signal. + // See https://github.com/ibus/ibus/issues/1848 + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("OnDirectionChanged, aGdkKeymap=%p, aKeymapWrapper=%p", aGdkKeymap, + aKeymapWrapper)); + + ResetBidiKeyboard(); +} + +/* static */ +guint KeymapWrapper::GetCurrentModifierState() { + GdkModifierType modifiers; + gdk_display_get_pointer(gdk_display_get_default(), nullptr, nullptr, nullptr, + &modifiers); + return static_cast<guint>(modifiers); +} + +/* static */ +bool KeymapWrapper::AreModifiersCurrentlyActive(Modifiers aModifiers) { + guint modifierState = GetCurrentModifierState(); + return AreModifiersActive(aModifiers, modifierState); +} + +/* static */ +bool KeymapWrapper::AreModifiersActive(Modifiers aModifiers, + guint aModifierState) { + NS_ENSURE_TRUE(aModifiers, false); + + KeymapWrapper* keymapWrapper = GetInstance(); + for (uint32_t i = 0; i < sizeof(Modifier) * 8 && aModifiers; i++) { + Modifier modifier = static_cast<Modifier>(1 << i); + if (!(aModifiers & modifier)) { + continue; + } + if (!(aModifierState & keymapWrapper->GetModifierMask(modifier))) { + return false; + } + aModifiers &= ~modifier; + } + return true; +} + +/* static */ +uint32_t KeymapWrapper::ComputeCurrentKeyModifiers() { + return ComputeKeyModifiers(GetCurrentModifierState()); +} + +/* static */ +uint32_t KeymapWrapper::ComputeKeyModifiers(guint aModifierState) { + KeymapWrapper* keymapWrapper = GetInstance(); + + uint32_t keyModifiers = 0; + // DOM Meta key should be TRUE only on Mac. We need to discuss this + // issue later. + if (keymapWrapper->AreModifiersActive(SHIFT, aModifierState)) { + keyModifiers |= MODIFIER_SHIFT; + } + if (keymapWrapper->AreModifiersActive(CTRL, aModifierState)) { + keyModifiers |= MODIFIER_CONTROL; + } + if (keymapWrapper->AreModifiersActive(ALT, aModifierState)) { + keyModifiers |= MODIFIER_ALT; + } + if (keymapWrapper->AreModifiersActive(META, aModifierState)) { + keyModifiers |= MODIFIER_META; + } + if (keymapWrapper->AreModifiersActive(SUPER, aModifierState) || + keymapWrapper->AreModifiersActive(HYPER, aModifierState)) { + keyModifiers |= MODIFIER_OS; + } + if (keymapWrapper->AreModifiersActive(LEVEL3, aModifierState) || + keymapWrapper->AreModifiersActive(LEVEL5, aModifierState)) { + keyModifiers |= MODIFIER_ALTGRAPH; + } + if (keymapWrapper->AreModifiersActive(CAPS_LOCK, aModifierState)) { + keyModifiers |= MODIFIER_CAPSLOCK; + } + if (keymapWrapper->AreModifiersActive(NUM_LOCK, aModifierState)) { + keyModifiers |= MODIFIER_NUMLOCK; + } + if (keymapWrapper->AreModifiersActive(SCROLL_LOCK, aModifierState)) { + keyModifiers |= MODIFIER_SCROLLLOCK; + } + return keyModifiers; +} + +/* static */ +void KeymapWrapper::InitInputEvent(WidgetInputEvent& aInputEvent, + guint aModifierState) { + KeymapWrapper* keymapWrapper = GetInstance(); + + aInputEvent.mModifiers = ComputeKeyModifiers(aModifierState); + + // Don't log this method for non-important events because e.g., eMouseMove is + // just noisy and there is no reason to log it. + bool doLog = aInputEvent.mMessage != eMouseMove; + if (doLog) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Debug, + ("%p InitInputEvent, aModifierState=0x%08X, " + "aInputEvent={ mMessage=%s, mModifiers=0x%04X (Shift: %s, " + "Control: %s, Alt: %s, " + "Meta: %s, OS: %s, AltGr: %s, " + "CapsLock: %s, NumLock: %s, ScrollLock: %s })", + keymapWrapper, aModifierState, ToChar(aInputEvent.mMessage), + aInputEvent.mModifiers, + GetBoolName(aInputEvent.mModifiers & MODIFIER_SHIFT), + GetBoolName(aInputEvent.mModifiers & MODIFIER_CONTROL), + GetBoolName(aInputEvent.mModifiers & MODIFIER_ALT), + GetBoolName(aInputEvent.mModifiers & MODIFIER_META), + GetBoolName(aInputEvent.mModifiers & MODIFIER_OS), + GetBoolName(aInputEvent.mModifiers & MODIFIER_ALTGRAPH), + GetBoolName(aInputEvent.mModifiers & MODIFIER_CAPSLOCK), + GetBoolName(aInputEvent.mModifiers & MODIFIER_NUMLOCK), + GetBoolName(aInputEvent.mModifiers & MODIFIER_SCROLLLOCK))); + } + + switch (aInputEvent.mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case eSimpleGestureEventClass: + break; + default: + return; + } + + WidgetMouseEventBase& mouseEvent = *aInputEvent.AsMouseEventBase(); + mouseEvent.mButtons = 0; + if (aModifierState & GDK_BUTTON1_MASK) { + mouseEvent.mButtons |= MouseButtonsFlag::ePrimaryFlag; + } + if (aModifierState & GDK_BUTTON3_MASK) { + mouseEvent.mButtons |= MouseButtonsFlag::eSecondaryFlag; + } + if (aModifierState & GDK_BUTTON2_MASK) { + mouseEvent.mButtons |= MouseButtonsFlag::eMiddleFlag; + } + + if (doLog) { + MOZ_LOG( + gKeymapWrapperLog, LogLevel::Debug, + ("%p InitInputEvent, aInputEvent has mButtons, " + "aInputEvent.mButtons=0x%04X (Left: %s, Right: %s, Middle: %s, " + "4th (BACK): %s, 5th (FORWARD): %s)", + keymapWrapper, mouseEvent.mButtons, + GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::ePrimaryFlag), + GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::eSecondaryFlag), + GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::eMiddleFlag), + GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::e4thFlag), + GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::e5thFlag))); + } +} + +/* static */ +uint32_t KeymapWrapper::ComputeDOMKeyCode(const GdkEventKey* aGdkKeyEvent) { + // If the keyval indicates it's a modifier key, we should use unshifted + // key's modifier keyval. + guint keyval = aGdkKeyEvent->keyval; + if (GetModifierForGDKKeyval(keyval)) { + // But if the keyval without modifiers isn't a modifier key, we + // shouldn't use it. E.g., Japanese keyboard layout's + // Shift + Eisu-Toggle key is CapsLock. This is an actual rare case, + // Windows uses different keycode for a physical key for different + // shift key state. + guint keyvalWithoutModifier = GetGDKKeyvalWithoutModifier(aGdkKeyEvent); + if (GetModifierForGDKKeyval(keyvalWithoutModifier)) { + keyval = keyvalWithoutModifier; + } + // Note that the modifier keycode and activating or deactivating + // modifier flag may be mismatched, but it's okay. If a DOM key + // event handler is testing a keydown event, it's more likely being + // used to test which key is being pressed than to test which + // modifier will become active. So, if we computed DOM keycode + // from modifier flag which were changing by the physical key, then + // there would be no other way for the user to generate the original + // keycode. + uint32_t DOMKeyCode = GetDOMKeyCodeFromKeyPairs(keyval); + NS_ASSERTION(DOMKeyCode, "All modifier keys must have a DOM keycode"); + return DOMKeyCode; + } + + // If the key isn't printable, let's look at the key pairs. + uint32_t charCode = GetCharCodeFor(aGdkKeyEvent); + if (!charCode) { + // Note that any key may be a function key because of some unusual keyboard + // layouts. I.e., even if the pressed key is a printable key of en-US + // keyboard layout, we should expose the function key's keyCode value to + // web apps because web apps should handle the keydown/keyup events as + // inputted by usual keyboard layout. For example, Hatchak keyboard + // maps Tab key to "Digit3" key and Level3 Shift makes it "Backspace". + // In this case, we should expose DOM_VK_BACK_SPACE (8). + uint32_t DOMKeyCode = GetDOMKeyCodeFromKeyPairs(keyval); + if (DOMKeyCode) { + // XXX If DOMKeyCode is a function key's keyCode value, it might be + // better to consume necessary modifiers. For example, if there is + // no Control Pad section on keyboard like notebook, Delete key is + // available only with Level3 Shift+"Backspace" key if using Hatchak. + // If web apps accept Delete key operation only when no modifiers are + // active, such users cannot use Delete key to do it. However, + // Chromium doesn't consume such necessary modifiers. So, our default + // behavior should keep not touching modifiers for compatibility, but + // it might be better to add a pref to consume necessary modifiers. + return DOMKeyCode; + } + // If aGdkKeyEvent cannot be mapped to a DOM keyCode value, we should + // refer keyCode value without modifiers because web apps should be + // able to identify the key as far as possible. + guint keyvalWithoutModifier = GetGDKKeyvalWithoutModifier(aGdkKeyEvent); + return GetDOMKeyCodeFromKeyPairs(keyvalWithoutModifier); + } + + // printable numpad keys should be resolved here. + switch (keyval) { + case GDK_KP_Multiply: + return NS_VK_MULTIPLY; + case GDK_KP_Add: + return NS_VK_ADD; + case GDK_KP_Separator: + return NS_VK_SEPARATOR; + case GDK_KP_Subtract: + return NS_VK_SUBTRACT; + case GDK_KP_Decimal: + return NS_VK_DECIMAL; + case GDK_KP_Divide: + return NS_VK_DIVIDE; + case GDK_KP_0: + return NS_VK_NUMPAD0; + case GDK_KP_1: + return NS_VK_NUMPAD1; + case GDK_KP_2: + return NS_VK_NUMPAD2; + case GDK_KP_3: + return NS_VK_NUMPAD3; + case GDK_KP_4: + return NS_VK_NUMPAD4; + case GDK_KP_5: + return NS_VK_NUMPAD5; + case GDK_KP_6: + return NS_VK_NUMPAD6; + case GDK_KP_7: + return NS_VK_NUMPAD7; + case GDK_KP_8: + return NS_VK_NUMPAD8; + case GDK_KP_9: + return NS_VK_NUMPAD9; + } + + KeymapWrapper* keymapWrapper = GetInstance(); + + // Ignore all modifier state except NumLock. + guint baseState = + (aGdkKeyEvent->state & keymapWrapper->GetModifierMask(NUM_LOCK)); + + // Basically, we should use unmodified character for deciding our keyCode. + uint32_t unmodifiedChar = keymapWrapper->GetCharCodeFor( + aGdkKeyEvent, baseState, aGdkKeyEvent->group); + if (IsBasicLatinLetterOrNumeral(unmodifiedChar)) { + // If the unmodified character is an ASCII alphabet or an ASCII + // numeric, it's the best hint for deciding our keyCode. + return WidgetUtils::ComputeKeyCodeFromChar(unmodifiedChar); + } + + // If the unmodified character is not an ASCII character, that means we + // couldn't find the hint. We should reset it. + if (!IsPrintableASCIICharacter(unmodifiedChar)) { + unmodifiedChar = 0; + } + + // Retry with shifted keycode. + guint shiftState = (baseState | keymapWrapper->GetModifierMask(SHIFT)); + uint32_t shiftedChar = keymapWrapper->GetCharCodeFor(aGdkKeyEvent, shiftState, + aGdkKeyEvent->group); + if (IsBasicLatinLetterOrNumeral(shiftedChar)) { + // A shifted character can be an ASCII alphabet on Hebrew keyboard + // layout. And also shifted character can be an ASCII numeric on + // AZERTY keyboad layout. Then, it's a good hint for deciding our + // keyCode. + return WidgetUtils::ComputeKeyCodeFromChar(shiftedChar); + } + + // If the shifted unmodified character isn't an ASCII character, we should + // discard it too. + if (!IsPrintableASCIICharacter(shiftedChar)) { + shiftedChar = 0; + } + + // If current keyboard layout isn't ASCII alphabet inputtable layout, + // look for ASCII alphabet inputtable keyboard layout. If the key + // inputs an ASCII alphabet or an ASCII numeric, we should use it + // for deciding our keyCode. + uint32_t unmodCharLatin = 0; + uint32_t shiftedCharLatin = 0; + if (!keymapWrapper->IsLatinGroup(aGdkKeyEvent->group)) { + gint minGroup = keymapWrapper->GetFirstLatinGroup(); + if (minGroup >= 0) { + unmodCharLatin = + keymapWrapper->GetCharCodeFor(aGdkKeyEvent, baseState, minGroup); + if (IsBasicLatinLetterOrNumeral(unmodCharLatin)) { + // If the unmodified character is an ASCII alphabet or + // an ASCII numeric, we should use it for the keyCode. + return WidgetUtils::ComputeKeyCodeFromChar(unmodCharLatin); + } + // If the unmodified character in the alternative ASCII capable + // keyboard layout isn't an ASCII character, that means we couldn't + // find the hint. We should reset it. + if (!IsPrintableASCIICharacter(unmodCharLatin)) { + unmodCharLatin = 0; + } + shiftedCharLatin = + keymapWrapper->GetCharCodeFor(aGdkKeyEvent, shiftState, minGroup); + if (IsBasicLatinLetterOrNumeral(shiftedCharLatin)) { + // If the shifted character is an ASCII alphabet or an ASCII + // numeric, we should use it for the keyCode. + return WidgetUtils::ComputeKeyCodeFromChar(shiftedCharLatin); + } + // If the shifted unmodified character in the alternative ASCII + // capable keyboard layout isn't an ASCII character, we should + // discard it too. + if (!IsPrintableASCIICharacter(shiftedCharLatin)) { + shiftedCharLatin = 0; + } + } + } + + // If the key itself or with Shift state on active keyboard layout produces + // an ASCII punctuation character, we should decide keyCode value with it. + if (unmodifiedChar || shiftedChar) { + return WidgetUtils::ComputeKeyCodeFromChar(unmodifiedChar ? unmodifiedChar + : shiftedChar); + } + + // If the key itself or with Shift state on alternative ASCII capable + // keyboard layout produces an ASCII punctuation character, we should + // decide keyCode value with it. Note that We've returned 0 for long + // time if keyCode isn't for an alphabet keys or a numeric key even in + // alternative ASCII capable keyboard layout because we decided that we + // should avoid setting same keyCode value to 2 or more keys since active + // keyboard layout may have a key to input the punctuation with different + // key. However, setting keyCode to 0 makes some web applications which + // are aware of neither KeyboardEvent.key nor KeyboardEvent.code not work + // with Firefox when user selects non-ASCII capable keyboard layout such + // as Russian and Thai. So, if alternative ASCII capable keyboard layout + // has keyCode value for the key, we should use it. In other words, this + // behavior means that non-ASCII capable keyboard layout overrides some + // keys' keyCode value only if the key produces ASCII character by itself + // or with Shift key. + if (unmodCharLatin || shiftedCharLatin) { + return WidgetUtils::ComputeKeyCodeFromChar( + unmodCharLatin ? unmodCharLatin : shiftedCharLatin); + } + + // Otherwise, let's decide keyCode value from the hardware_keycode + // value on major keyboard layout. + CodeNameIndex code = ComputeDOMCodeNameIndex(aGdkKeyEvent); + return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code); +} + +KeyNameIndex KeymapWrapper::ComputeDOMKeyNameIndex( + const GdkEventKey* aGdkKeyEvent) { + switch (aGdkKeyEvent->keyval) { +#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \ + case aNativeKey: \ + return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + default: + break; + } + + return KEY_NAME_INDEX_Unidentified; +} + +/* static */ +CodeNameIndex KeymapWrapper::ComputeDOMCodeNameIndex( + const GdkEventKey* aGdkKeyEvent) { + switch (aGdkKeyEvent->hardware_keycode) { +#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \ + case aNativeKey: \ + return aCodeNameIndex; + +#include "NativeKeyToDOMCodeName.h" + +#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX + + default: + break; + } + + return CODE_NAME_INDEX_UNKNOWN; +} + +/* static */ +bool KeymapWrapper::DispatchKeyDownOrKeyUpEvent(nsWindow* aWindow, + GdkEventKey* aGdkKeyEvent, + bool aIsProcessedByIME, + bool* aIsCancelled) { + MOZ_ASSERT(aIsCancelled, "aIsCancelled must not be nullptr"); + + *aIsCancelled = false; + + if (aGdkKeyEvent->type == GDK_KEY_PRESS && aGdkKeyEvent->keyval == GDK_Tab && + AreModifiersActive(CTRL | ALT, aGdkKeyEvent->state)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" DispatchKeyDownOrKeyUpEvent(), didn't dispatch keyboard events " + "because it's Ctrl + Alt + Tab")); + return false; + } + + EventMessage message = + aGdkKeyEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp; + WidgetKeyboardEvent keyEvent(true, message, aWindow); + KeymapWrapper::InitKeyEvent(keyEvent, aGdkKeyEvent, aIsProcessedByIME); + return DispatchKeyDownOrKeyUpEvent(aWindow, keyEvent, aIsCancelled); +} + +/* static */ +bool KeymapWrapper::DispatchKeyDownOrKeyUpEvent( + nsWindow* aWindow, WidgetKeyboardEvent& aKeyboardEvent, + bool* aIsCancelled) { + MOZ_ASSERT(aIsCancelled, "aIsCancelled must not be nullptr"); + + *aIsCancelled = false; + + RefPtr<TextEventDispatcher> dispatcher = aWindow->GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Error, + (" DispatchKeyDownOrKeyUpEvent(), stopped dispatching %s event " + "because of failed to initialize TextEventDispatcher", + ToChar(aKeyboardEvent.mMessage))); + return FALSE; + } + + nsEventStatus status = nsEventStatus_eIgnore; + bool dispatched = dispatcher->DispatchKeyboardEvent( + aKeyboardEvent.mMessage, aKeyboardEvent, status, nullptr); + *aIsCancelled = (status == nsEventStatus_eConsumeNoDefault); + return dispatched; +} + +/* static */ +bool KeymapWrapper::MaybeDispatchContextMenuEvent(nsWindow* aWindow, + const GdkEventKey* aEvent) { + KeyNameIndex keyNameIndex = ComputeDOMKeyNameIndex(aEvent); + + // Shift+F10 and ContextMenu should cause eContextMenu event. + if (keyNameIndex != KEY_NAME_INDEX_F10 && + keyNameIndex != KEY_NAME_INDEX_ContextMenu) { + return false; + } + + WidgetMouseEvent contextMenuEvent(true, eContextMenu, aWindow, + WidgetMouseEvent::eReal, + WidgetMouseEvent::eContextMenuKey); + + contextMenuEvent.mRefPoint = LayoutDeviceIntPoint(0, 0); + contextMenuEvent.AssignEventTime(aWindow->GetWidgetEventTime(aEvent->time)); + contextMenuEvent.mClickCount = 1; + KeymapWrapper::InitInputEvent(contextMenuEvent, aEvent->state); + + if (contextMenuEvent.IsControl() || contextMenuEvent.IsMeta() || + contextMenuEvent.IsAlt()) { + return false; + } + + // If the key is ContextMenu, then an eContextMenu mouse event is + // dispatched regardless of the state of the Shift modifier. When it is + // pressed without the Shift modifier, a web page can prevent the default + // context menu action. When pressed with the Shift modifier, the web page + // cannot prevent the default context menu action. + // (PresShell::HandleEventInternal() sets mOnlyChromeDispatch to true.) + + // If the key is F10, it needs Shift state because Shift+F10 is well-known + // shortcut key on Linux. However, eContextMenu with Shift state is + // special. It won't fire "contextmenu" event in the web content for + // blocking web page to prevent its default. Therefore, this combination + // should work same as ContextMenu key. + // XXX Should we allow to block web page to prevent its default with + // Ctrl+Shift+F10 or Alt+Shift+F10 instead? + if (keyNameIndex == KEY_NAME_INDEX_F10) { + if (!contextMenuEvent.IsShift()) { + return false; + } + contextMenuEvent.mModifiers &= ~MODIFIER_SHIFT; + } + + aWindow->DispatchInputEvent(&contextMenuEvent); + return true; +} + +/* static*/ +void KeymapWrapper::HandleKeyPressEvent(nsWindow* aWindow, + GdkEventKey* aGdkKeyEvent) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("HandleKeyPressEvent(aWindow=%p, aGdkKeyEvent={ type=%s, " + "keyval=%s(0x%X), state=0x%08X, hardware_keycode=0x%08X, " + "time=%u, is_modifier=%s })", + aWindow, + ((aGdkKeyEvent->type == GDK_KEY_PRESS) ? "GDK_KEY_PRESS" + : "GDK_KEY_RELEASE"), + gdk_keyval_name(aGdkKeyEvent->keyval), aGdkKeyEvent->keyval, + aGdkKeyEvent->state, aGdkKeyEvent->hardware_keycode, + aGdkKeyEvent->time, GetBoolName(aGdkKeyEvent->is_modifier))); + + // if we are in the middle of composing text, XIM gets to see it + // before mozilla does. + // FYI: Don't dispatch keydown event before notifying IME of the event + // because IME may send a key event synchronously and consume the + // original event. + bool IMEWasEnabled = false; + KeyHandlingState handlingState = KeyHandlingState::eNotHandled; + RefPtr<IMContextWrapper> imContext = aWindow->GetIMContext(); + if (imContext) { + IMEWasEnabled = imContext->IsEnabled(); + handlingState = imContext->OnKeyEvent(aWindow, aGdkKeyEvent); + if (handlingState == KeyHandlingState::eHandled) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), the event was handled by " + "IMContextWrapper")); + return; + } + } + + // work around for annoying things. + if (aGdkKeyEvent->keyval == GDK_Tab && + AreModifiersActive(CTRL | ALT, aGdkKeyEvent->state)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), didn't dispatch keyboard events " + "because it's Ctrl + Alt + Tab")); + return; + } + + // Dispatch keydown event always. At auto repeating, we should send + // KEYDOWN -> KEYPRESS -> KEYDOWN -> KEYPRESS ... -> KEYUP + // However, old distributions (e.g., Ubuntu 9.10) sent native key + // release event, so, on such platform, the DOM events will be: + // KEYDOWN -> KEYPRESS -> KEYUP -> KEYDOWN -> KEYPRESS -> KEYUP... + + bool isKeyDownCancelled = false; + if (handlingState == KeyHandlingState::eNotHandled) { + if (DispatchKeyDownOrKeyUpEvent(aWindow, aGdkKeyEvent, false, + &isKeyDownCancelled) && + (MOZ_UNLIKELY(aWindow->IsDestroyed()) || isKeyDownCancelled)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched eKeyDown event and " + "stopped handling the event because %s", + aWindow->IsDestroyed() ? "the window has been destroyed" + : "the event was consumed")); + return; + } + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched eKeyDown event and " + "it wasn't consumed")); + handlingState = KeyHandlingState::eNotHandledButEventDispatched; + } + + // If a keydown event handler causes to enable IME, i.e., it moves + // focus from IME unusable content to IME usable editor, we should + // send the native key event to IME for the first input on the editor. + imContext = aWindow->GetIMContext(); + if (!IMEWasEnabled && imContext && imContext->IsEnabled()) { + // Notice our keydown event was already dispatched. This prevents + // unnecessary DOM keydown event in the editor. + handlingState = imContext->OnKeyEvent(aWindow, aGdkKeyEvent, true); + if (handlingState == KeyHandlingState::eHandled) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), the event was handled by " + "IMContextWrapper which was enabled by the preceding eKeyDown " + "event")); + return; + } + } + + // Look for specialized app-command keys + switch (aGdkKeyEvent->keyval) { + case GDK_Back: + aWindow->DispatchCommandEvent(nsGkAtoms::Back); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Back\" command event")); + return; + case GDK_Forward: + aWindow->DispatchCommandEvent(nsGkAtoms::Forward); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Forward\" command " + "event")); + return; + case GDK_Reload: + case GDK_Refresh: + aWindow->DispatchCommandEvent(nsGkAtoms::Reload); + return; + case GDK_Stop: + aWindow->DispatchCommandEvent(nsGkAtoms::Stop); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Stop\" command event")); + return; + case GDK_Search: + aWindow->DispatchCommandEvent(nsGkAtoms::Search); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Search\" command event")); + return; + case GDK_Favorites: + aWindow->DispatchCommandEvent(nsGkAtoms::Bookmarks); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Bookmarks\" command " + "event")); + return; + case GDK_HomePage: + aWindow->DispatchCommandEvent(nsGkAtoms::Home); + return; + case GDK_Copy: + case GDK_F16: // F16, F20, F18, F14 are old keysyms for Copy Cut Paste Undo + aWindow->DispatchContentCommandEvent(eContentCommandCopy); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Copy\" content command " + "event")); + return; + case GDK_Cut: + case GDK_F20: + aWindow->DispatchContentCommandEvent(eContentCommandCut); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Cut\" content command " + "event")); + return; + case GDK_Paste: + case GDK_F18: + aWindow->DispatchContentCommandEvent(eContentCommandPaste); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Paste\" content command " + "event")); + return; + case GDK_Redo: + aWindow->DispatchContentCommandEvent(eContentCommandRedo); + return; + case GDK_Undo: + case GDK_F14: + aWindow->DispatchContentCommandEvent(eContentCommandUndo); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Undo\" content command " + "event")); + return; + default: + break; + } + + // before we dispatch a key, check if it's the context menu key. + // If so, send a context menu key event instead. + if (MaybeDispatchContextMenuEvent(aWindow, aGdkKeyEvent)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), stopped dispatching eKeyPress event " + "because eContextMenu event was dispatched")); + return; + } + + RefPtr<TextEventDispatcher> textEventDispatcher = + aWindow->GetTextEventDispatcher(); + nsresult rv = textEventDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Error, + (" HandleKeyPressEvent(), stopped dispatching eKeyPress event " + "because of failed to initialize TextEventDispatcher")); + return; + } + + // If the character code is in the BMP, send the key press event. + // Otherwise, send a compositionchange event with the equivalent UTF-16 + // string. + // TODO: Investigate other browser's behavior in this case because + // this hack is odd for UI Events. + WidgetKeyboardEvent keypressEvent(true, eKeyPress, aWindow); + KeymapWrapper::InitKeyEvent(keypressEvent, aGdkKeyEvent, false); + nsEventStatus status = nsEventStatus_eIgnore; + if (keypressEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING || + keypressEvent.mKeyValue.Length() == 1) { + if (textEventDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status, + aGdkKeyEvent)) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched eKeyPress event " + "(status=%s)", + GetStatusName(status))); + } else { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), didn't dispatch eKeyPress event " + "(status=%s)", + GetStatusName(status))); + } + } else { + WidgetEventTime eventTime = aWindow->GetWidgetEventTime(aGdkKeyEvent->time); + textEventDispatcher->CommitComposition(status, &keypressEvent.mKeyValue, + &eventTime); + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched a set of composition " + "events")); + } +} + +/* static */ +bool KeymapWrapper::HandleKeyReleaseEvent(nsWindow* aWindow, + GdkEventKey* aGdkKeyEvent) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("HandleKeyReleaseEvent(aWindow=%p, aGdkKeyEvent={ type=%s, " + "keyval=%s(0x%X), state=0x%08X, hardware_keycode=0x%08X, " + "time=%u, is_modifier=%s })", + aWindow, + ((aGdkKeyEvent->type == GDK_KEY_PRESS) ? "GDK_KEY_PRESS" + : "GDK_KEY_RELEASE"), + gdk_keyval_name(aGdkKeyEvent->keyval), aGdkKeyEvent->keyval, + aGdkKeyEvent->state, aGdkKeyEvent->hardware_keycode, + aGdkKeyEvent->time, GetBoolName(aGdkKeyEvent->is_modifier))); + + RefPtr<IMContextWrapper> imContext = aWindow->GetIMContext(); + if (imContext) { + KeyHandlingState handlingState = + imContext->OnKeyEvent(aWindow, aGdkKeyEvent); + if (handlingState != KeyHandlingState::eNotHandled) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyReleaseEvent(), the event was handled by " + "IMContextWrapper")); + return true; + } + } + + bool isCancelled = false; + if (NS_WARN_IF(!DispatchKeyDownOrKeyUpEvent(aWindow, aGdkKeyEvent, false, + &isCancelled))) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Error, + (" HandleKeyReleaseEvent(), didn't dispatch eKeyUp event")); + return false; + } + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + (" HandleKeyReleaseEvent(), dispatched eKeyUp event " + "(isCancelled=%s)", + GetBoolName(isCancelled))); + return true; +} + +/* static */ +void KeymapWrapper::InitKeyEvent(WidgetKeyboardEvent& aKeyEvent, + GdkEventKey* aGdkKeyEvent, + bool aIsProcessedByIME) { + MOZ_ASSERT( + !aIsProcessedByIME || aKeyEvent.mMessage != eKeyPress, + "If the key event is handled by IME, keypress event shouldn't be fired"); + + KeymapWrapper* keymapWrapper = GetInstance(); + + aKeyEvent.mCodeNameIndex = ComputeDOMCodeNameIndex(aGdkKeyEvent); + MOZ_ASSERT(aKeyEvent.mCodeNameIndex != CODE_NAME_INDEX_USE_STRING); + aKeyEvent.mKeyNameIndex = + aIsProcessedByIME ? KEY_NAME_INDEX_Process + : keymapWrapper->ComputeDOMKeyNameIndex(aGdkKeyEvent); + if (aKeyEvent.mKeyNameIndex == KEY_NAME_INDEX_Unidentified) { + uint32_t charCode = GetCharCodeFor(aGdkKeyEvent); + if (!charCode) { + charCode = keymapWrapper->GetUnmodifiedCharCodeFor(aGdkKeyEvent); + } + if (charCode) { + aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + MOZ_ASSERT(aKeyEvent.mKeyValue.IsEmpty(), + "Uninitialized mKeyValue must be empty"); + AppendUCS4ToUTF16(charCode, aKeyEvent.mKeyValue); + } + } + + if (aIsProcessedByIME) { + aKeyEvent.mKeyCode = NS_VK_PROCESSKEY; + } else if (aKeyEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING || + aKeyEvent.mMessage != eKeyPress) { + aKeyEvent.mKeyCode = ComputeDOMKeyCode(aGdkKeyEvent); + } else { + aKeyEvent.mKeyCode = 0; + } + + // NOTE: The state of given key event indicates adjacent state of + // modifier keys. E.g., even if the event is Shift key press event, + // the bit for Shift is still false. By the same token, even if the + // event is Shift key release event, the bit for Shift is still true. + // Unfortunately, gdk_keyboard_get_modifiers() returns current modifier + // state. It means if there're some pending modifier key press or + // key release events, the result isn't what we want. + guint modifierState = aGdkKeyEvent->state; + GdkDisplay* gdkDisplay = gdk_display_get_default(); + if (aGdkKeyEvent->is_modifier && GDK_IS_X11_DISPLAY(gdkDisplay)) { + Display* display = gdk_x11_display_get_xdisplay(gdkDisplay); + if (XEventsQueued(display, QueuedAfterReading)) { + XEvent nextEvent; + XPeekEvent(display, &nextEvent); + if (nextEvent.type == keymapWrapper->mXKBBaseEventCode) { + XkbEvent* XKBEvent = (XkbEvent*)&nextEvent; + if (XKBEvent->any.xkb_type == XkbStateNotify) { + XkbStateNotifyEvent* stateNotifyEvent = + (XkbStateNotifyEvent*)XKBEvent; + modifierState &= ~0xFF; + modifierState |= stateNotifyEvent->lookup_mods; + } + } + } + } + InitInputEvent(aKeyEvent, modifierState); + + switch (aGdkKeyEvent->keyval) { + case GDK_Shift_L: + case GDK_Control_L: + case GDK_Alt_L: + case GDK_Super_L: + case GDK_Hyper_L: + case GDK_Meta_L: + aKeyEvent.mLocation = eKeyLocationLeft; + break; + + case GDK_Shift_R: + case GDK_Control_R: + case GDK_Alt_R: + case GDK_Super_R: + case GDK_Hyper_R: + case GDK_Meta_R: + aKeyEvent.mLocation = eKeyLocationRight; + break; + + case GDK_KP_0: + case GDK_KP_1: + case GDK_KP_2: + case GDK_KP_3: + case GDK_KP_4: + case GDK_KP_5: + case GDK_KP_6: + case GDK_KP_7: + case GDK_KP_8: + case GDK_KP_9: + case GDK_KP_Space: + case GDK_KP_Tab: + case GDK_KP_Enter: + case GDK_KP_F1: + case GDK_KP_F2: + case GDK_KP_F3: + case GDK_KP_F4: + case GDK_KP_Home: + case GDK_KP_Left: + case GDK_KP_Up: + case GDK_KP_Right: + case GDK_KP_Down: + case GDK_KP_Prior: // same as GDK_KP_Page_Up + case GDK_KP_Next: // same as GDK_KP_Page_Down + case GDK_KP_End: + case GDK_KP_Begin: + case GDK_KP_Insert: + case GDK_KP_Delete: + case GDK_KP_Equal: + case GDK_KP_Multiply: + case GDK_KP_Add: + case GDK_KP_Separator: + case GDK_KP_Subtract: + case GDK_KP_Decimal: + case GDK_KP_Divide: + aKeyEvent.mLocation = eKeyLocationNumpad; + break; + + default: + aKeyEvent.mLocation = eKeyLocationStandard; + break; + } + + // The transformations above and in gdk for the keyval are not invertible + // so link to the GdkEvent (which will vanish soon after return from the + // event callback) to give plugins access to hardware_keycode and state. + // (An XEvent would be nice but the GdkEvent is good enough.) + aKeyEvent.mPluginEvent.Copy(*aGdkKeyEvent); + aKeyEvent.mTime = aGdkKeyEvent->time; + aKeyEvent.mNativeKeyEvent = static_cast<void*>(aGdkKeyEvent); + aKeyEvent.mIsRepeat = + sRepeatState == REPEATING && + aGdkKeyEvent->hardware_keycode == sLastRepeatableHardwareKeyCode; + + MOZ_LOG( + gKeymapWrapperLog, LogLevel::Info, + ("%p InitKeyEvent, modifierState=0x%08X " + "aKeyEvent={ mMessage=%s, isShift=%s, isControl=%s, " + "isAlt=%s, isMeta=%s , mKeyCode=0x%02X, mCharCode=%s, " + "mKeyNameIndex=%s, mKeyValue=%s, mCodeNameIndex=%s, mCodeValue=%s, " + "mLocation=%s, mIsRepeat=%s }", + keymapWrapper, modifierState, ToChar(aKeyEvent.mMessage), + GetBoolName(aKeyEvent.IsShift()), GetBoolName(aKeyEvent.IsControl()), + GetBoolName(aKeyEvent.IsAlt()), GetBoolName(aKeyEvent.IsMeta()), + aKeyEvent.mKeyCode, + GetCharacterCodeName(static_cast<char16_t>(aKeyEvent.mCharCode)).get(), + ToString(aKeyEvent.mKeyNameIndex).get(), + GetCharacterCodeNames(aKeyEvent.mKeyValue).get(), + ToString(aKeyEvent.mCodeNameIndex).get(), + GetCharacterCodeNames(aKeyEvent.mCodeValue).get(), + GetKeyLocationName(aKeyEvent.mLocation).get(), + GetBoolName(aKeyEvent.mIsRepeat))); +} + +/* static */ +uint32_t KeymapWrapper::GetCharCodeFor(const GdkEventKey* aGdkKeyEvent) { + // Anything above 0xf000 is considered a non-printable + // Exception: directly encoded UCS characters + if (aGdkKeyEvent->keyval > 0xf000 && + (aGdkKeyEvent->keyval & 0xff000000) != 0x01000000) { + // Keypad keys are an exception: they return a value different + // from their non-keypad equivalents, but mozilla doesn't distinguish. + switch (aGdkKeyEvent->keyval) { + case GDK_KP_Space: + return ' '; + case GDK_KP_Equal: + return '='; + case GDK_KP_Multiply: + return '*'; + case GDK_KP_Add: + return '+'; + case GDK_KP_Separator: + return ','; + case GDK_KP_Subtract: + return '-'; + case GDK_KP_Decimal: + return '.'; + case GDK_KP_Divide: + return '/'; + case GDK_KP_0: + return '0'; + case GDK_KP_1: + return '1'; + case GDK_KP_2: + return '2'; + case GDK_KP_3: + return '3'; + case GDK_KP_4: + return '4'; + case GDK_KP_5: + return '5'; + case GDK_KP_6: + return '6'; + case GDK_KP_7: + return '7'; + case GDK_KP_8: + return '8'; + case GDK_KP_9: + return '9'; + default: + return 0; // non-printables + } + } + + static const long MAX_UNICODE = 0x10FFFF; + + // we're supposedly printable, let's try to convert + long ucs = keysym2ucs(aGdkKeyEvent->keyval); + if ((ucs != -1) && (ucs < MAX_UNICODE)) { + return ucs; + } + + // I guess we couldn't convert + return 0; +} + +uint32_t KeymapWrapper::GetCharCodeFor(const GdkEventKey* aGdkKeyEvent, + guint aModifierState, gint aGroup) { + guint keyval; + if (!gdk_keymap_translate_keyboard_state( + mGdkKeymap, aGdkKeyEvent->hardware_keycode, + GdkModifierType(aModifierState), aGroup, &keyval, nullptr, nullptr, + nullptr)) { + return 0; + } + GdkEventKey tmpEvent = *aGdkKeyEvent; + tmpEvent.state = aModifierState; + tmpEvent.keyval = keyval; + tmpEvent.group = aGroup; + return GetCharCodeFor(&tmpEvent); +} + +uint32_t KeymapWrapper::GetUnmodifiedCharCodeFor( + const GdkEventKey* aGdkKeyEvent) { + guint state = aGdkKeyEvent->state & + (GetModifierMask(SHIFT) | GetModifierMask(CAPS_LOCK) | + GetModifierMask(NUM_LOCK) | GetModifierMask(SCROLL_LOCK) | + GetModifierMask(LEVEL3) | GetModifierMask(LEVEL5)); + uint32_t charCode = + GetCharCodeFor(aGdkKeyEvent, GdkModifierType(state), aGdkKeyEvent->group); + if (charCode) { + return charCode; + } + // If no character is mapped to the key when Level3 Shift or Level5 Shift + // is active, let's return a character which is inputted by the key without + // Level3 nor Level5 Shift. + guint stateWithoutAltGraph = + state & ~(GetModifierMask(LEVEL3) | GetModifierMask(LEVEL5)); + if (state == stateWithoutAltGraph) { + return 0; + } + return GetCharCodeFor(aGdkKeyEvent, GdkModifierType(stateWithoutAltGraph), + aGdkKeyEvent->group); +} + +gint KeymapWrapper::GetKeyLevel(GdkEventKey* aGdkKeyEvent) { + gint level; + if (!gdk_keymap_translate_keyboard_state( + mGdkKeymap, aGdkKeyEvent->hardware_keycode, + GdkModifierType(aGdkKeyEvent->state), aGdkKeyEvent->group, nullptr, + nullptr, &level, nullptr)) { + return -1; + } + return level; +} + +gint KeymapWrapper::GetFirstLatinGroup() { + GdkKeymapKey* keys; + gint count; + gint minGroup = -1; + if (gdk_keymap_get_entries_for_keyval(mGdkKeymap, GDK_a, &keys, &count)) { + // find the minimum number group for latin inputtable layout + for (gint i = 0; i < count && minGroup != 0; ++i) { + if (keys[i].level != 0 && keys[i].level != 1) { + continue; + } + if (minGroup >= 0 && keys[i].group > minGroup) { + continue; + } + minGroup = keys[i].group; + } + g_free(keys); + } + return minGroup; +} + +bool KeymapWrapper::IsLatinGroup(guint8 aGroup) { + GdkKeymapKey* keys; + gint count; + bool result = false; + if (gdk_keymap_get_entries_for_keyval(mGdkKeymap, GDK_a, &keys, &count)) { + for (gint i = 0; i < count; ++i) { + if (keys[i].level != 0 && keys[i].level != 1) { + continue; + } + if (keys[i].group == aGroup) { + result = true; + break; + } + } + g_free(keys); + } + return result; +} + +bool KeymapWrapper::IsAutoRepeatableKey(guint aHardwareKeyCode) { + uint8_t indexOfArray = aHardwareKeyCode / 8; + MOZ_ASSERT(indexOfArray < ArrayLength(mKeyboardState.auto_repeats), + "invalid index"); + char bitMask = 1 << (aHardwareKeyCode % 8); + return (mKeyboardState.auto_repeats[indexOfArray] & bitMask) != 0; +} + +/* static */ +bool KeymapWrapper::IsBasicLatinLetterOrNumeral(uint32_t aCharCode) { + return (aCharCode >= 'a' && aCharCode <= 'z') || + (aCharCode >= 'A' && aCharCode <= 'Z') || + (aCharCode >= '0' && aCharCode <= '9'); +} + +/* static */ +guint KeymapWrapper::GetGDKKeyvalWithoutModifier( + const GdkEventKey* aGdkKeyEvent) { + KeymapWrapper* keymapWrapper = GetInstance(); + guint state = + (aGdkKeyEvent->state & keymapWrapper->GetModifierMask(NUM_LOCK)); + guint keyval; + if (!gdk_keymap_translate_keyboard_state( + keymapWrapper->mGdkKeymap, aGdkKeyEvent->hardware_keycode, + GdkModifierType(state), aGdkKeyEvent->group, &keyval, nullptr, + nullptr, nullptr)) { + return 0; + } + return keyval; +} + +/* static */ +uint32_t KeymapWrapper::GetDOMKeyCodeFromKeyPairs(guint aGdkKeyval) { + switch (aGdkKeyval) { + case GDK_Cancel: + return NS_VK_CANCEL; + case GDK_BackSpace: + return NS_VK_BACK; + case GDK_Tab: + case GDK_ISO_Left_Tab: + return NS_VK_TAB; + case GDK_Clear: + return NS_VK_CLEAR; + case GDK_Return: + return NS_VK_RETURN; + case GDK_Shift_L: + case GDK_Shift_R: + case GDK_Shift_Lock: + return NS_VK_SHIFT; + case GDK_Control_L: + case GDK_Control_R: + return NS_VK_CONTROL; + case GDK_Alt_L: + case GDK_Alt_R: + return NS_VK_ALT; + case GDK_Meta_L: + case GDK_Meta_R: + return NS_VK_META; + + // Assume that Super or Hyper is always mapped to physical Win key. + case GDK_Super_L: + case GDK_Super_R: + case GDK_Hyper_L: + case GDK_Hyper_R: + return NS_VK_WIN; + + // GTK's AltGraph key is similar to Mac's Option (Alt) key. However, + // unfortunately, browsers on Mac are using NS_VK_ALT for it even though + // it's really different from Alt key on Windows. + // On the other hand, GTK's AltGrapsh keys are really different from + // Alt key. However, there is no AltGrapsh key on Windows. On Windows, + // both Ctrl and Alt keys are pressed internally when AltGr key is + // pressed. For some languages' users, AltGraph key is important, so, + // web applications on such locale may want to know AltGraph key press. + // Therefore, we should map AltGr keycode for them only on GTK. + case GDK_ISO_Level3_Shift: + case GDK_ISO_Level5_Shift: + // We assume that Mode_switch is always used for level3 shift. + case GDK_Mode_switch: + return NS_VK_ALTGR; + + case GDK_Pause: + return NS_VK_PAUSE; + case GDK_Caps_Lock: + return NS_VK_CAPS_LOCK; + case GDK_Kana_Lock: + case GDK_Kana_Shift: + return NS_VK_KANA; + case GDK_Hangul: + return NS_VK_HANGUL; + // case GDK_XXX: return NS_VK_JUNJA; + // case GDK_XXX: return NS_VK_FINAL; + case GDK_Hangul_Hanja: + return NS_VK_HANJA; + case GDK_Kanji: + return NS_VK_KANJI; + case GDK_Escape: + return NS_VK_ESCAPE; + case GDK_Henkan: + return NS_VK_CONVERT; + case GDK_Muhenkan: + return NS_VK_NONCONVERT; + // case GDK_XXX: return NS_VK_ACCEPT; + // case GDK_XXX: return NS_VK_MODECHANGE; + case GDK_Page_Up: + return NS_VK_PAGE_UP; + case GDK_Page_Down: + return NS_VK_PAGE_DOWN; + case GDK_End: + return NS_VK_END; + case GDK_Home: + return NS_VK_HOME; + case GDK_Left: + return NS_VK_LEFT; + case GDK_Up: + return NS_VK_UP; + case GDK_Right: + return NS_VK_RIGHT; + case GDK_Down: + return NS_VK_DOWN; + case GDK_Select: + return NS_VK_SELECT; + case GDK_Print: + return NS_VK_PRINT; + case GDK_Execute: + return NS_VK_EXECUTE; + case GDK_Insert: + return NS_VK_INSERT; + case GDK_Delete: + return NS_VK_DELETE; + case GDK_Help: + return NS_VK_HELP; + + // keypad keys + case GDK_KP_Left: + return NS_VK_LEFT; + case GDK_KP_Right: + return NS_VK_RIGHT; + case GDK_KP_Up: + return NS_VK_UP; + case GDK_KP_Down: + return NS_VK_DOWN; + case GDK_KP_Page_Up: + return NS_VK_PAGE_UP; + // Not sure what these are + // case GDK_KP_Prior: return NS_VK_; + // case GDK_KP_Next: return NS_VK_; + case GDK_KP_Begin: + return NS_VK_CLEAR; // Num-unlocked 5 + case GDK_KP_Page_Down: + return NS_VK_PAGE_DOWN; + case GDK_KP_Home: + return NS_VK_HOME; + case GDK_KP_End: + return NS_VK_END; + case GDK_KP_Insert: + return NS_VK_INSERT; + case GDK_KP_Delete: + return NS_VK_DELETE; + case GDK_KP_Enter: + return NS_VK_RETURN; + + case GDK_Num_Lock: + return NS_VK_NUM_LOCK; + case GDK_Scroll_Lock: + return NS_VK_SCROLL_LOCK; + + // Function keys + case GDK_F1: + return NS_VK_F1; + case GDK_F2: + return NS_VK_F2; + case GDK_F3: + return NS_VK_F3; + case GDK_F4: + return NS_VK_F4; + case GDK_F5: + return NS_VK_F5; + case GDK_F6: + return NS_VK_F6; + case GDK_F7: + return NS_VK_F7; + case GDK_F8: + return NS_VK_F8; + case GDK_F9: + return NS_VK_F9; + case GDK_F10: + return NS_VK_F10; + case GDK_F11: + return NS_VK_F11; + case GDK_F12: + return NS_VK_F12; + case GDK_F13: + return NS_VK_F13; + case GDK_F14: + return NS_VK_F14; + case GDK_F15: + return NS_VK_F15; + case GDK_F16: + return NS_VK_F16; + case GDK_F17: + return NS_VK_F17; + case GDK_F18: + return NS_VK_F18; + case GDK_F19: + return NS_VK_F19; + case GDK_F20: + return NS_VK_F20; + case GDK_F21: + return NS_VK_F21; + case GDK_F22: + return NS_VK_F22; + case GDK_F23: + return NS_VK_F23; + case GDK_F24: + return NS_VK_F24; + + // context menu key, keysym 0xff67, typically keycode 117 on 105-key + // (Microsoft) x86 keyboards, located between right 'Windows' key and + // right Ctrl key + case GDK_Menu: + return NS_VK_CONTEXT_MENU; + case GDK_Sleep: + return NS_VK_SLEEP; + + case GDK_3270_Attn: + return NS_VK_ATTN; + case GDK_3270_CursorSelect: + return NS_VK_CRSEL; + case GDK_3270_ExSelect: + return NS_VK_EXSEL; + case GDK_3270_EraseEOF: + return NS_VK_EREOF; + case GDK_3270_Play: + return NS_VK_PLAY; + // case GDK_XXX: return NS_VK_ZOOM; + case GDK_3270_PA1: + return NS_VK_PA1; + + // map Sun Keyboard special keysyms on to NS_VK keys + + // Sun F11 key generates SunF36(0x1005ff10) keysym + case 0x1005ff10: + return NS_VK_F11; + // Sun F12 key generates SunF37(0x1005ff11) keysym + case 0x1005ff11: + return NS_VK_F12; + default: + return 0; + } +} + +void KeymapWrapper::WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyEvent, + GdkEventKey* aGdkKeyEvent) { + GetInstance()->WillDispatchKeyboardEventInternal(aKeyEvent, aGdkKeyEvent); +} + +void KeymapWrapper::WillDispatchKeyboardEventInternal( + WidgetKeyboardEvent& aKeyEvent, GdkEventKey* aGdkKeyEvent) { + if (!aGdkKeyEvent) { + // If aGdkKeyEvent is nullptr, we're trying to dispatch a fake keyboard + // event in such case, we don't need to set alternative char codes. + // So, we don't need to do nothing here. This case is typically we're + // dispatching eKeyDown or eKeyUp event during composition. + return; + } + + uint32_t charCode = GetCharCodeFor(aGdkKeyEvent); + if (!charCode) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p WillDispatchKeyboardEventInternal, " + "mKeyCode=0x%02X, charCode=0x%08X", + this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode)); + return; + } + + // The mCharCode was set from mKeyValue. However, for example, when Ctrl key + // is pressed, its value should indicate an ASCII character for backward + // compatibility rather than inputting character without the modifiers. + // Therefore, we need to modify mCharCode value here. + aKeyEvent.SetCharCode(charCode); + + gint level = GetKeyLevel(aGdkKeyEvent); + if (level != 0 && level != 1) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p WillDispatchKeyboardEventInternal, " + "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d", + this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level)); + return; + } + + guint baseState = + aGdkKeyEvent->state & ~(GetModifierMask(SHIFT) | GetModifierMask(CTRL) | + GetModifierMask(ALT) | GetModifierMask(META) | + GetModifierMask(SUPER) | GetModifierMask(HYPER)); + + // We shold send both shifted char and unshifted char, all keyboard layout + // users can use all keys. Don't change event.mCharCode. On some keyboard + // layouts, Ctrl/Alt/Meta keys are used for inputting some characters. + AlternativeCharCode altCharCodes(0, 0); + // unshifted charcode of current keyboard layout. + altCharCodes.mUnshiftedCharCode = + GetCharCodeFor(aGdkKeyEvent, baseState, aGdkKeyEvent->group); + bool isLatin = (altCharCodes.mUnshiftedCharCode <= 0xFF); + // shifted charcode of current keyboard layout. + altCharCodes.mShiftedCharCode = GetCharCodeFor( + aGdkKeyEvent, baseState | GetModifierMask(SHIFT), aGdkKeyEvent->group); + isLatin = isLatin && (altCharCodes.mShiftedCharCode <= 0xFF); + if (altCharCodes.mUnshiftedCharCode || altCharCodes.mShiftedCharCode) { + aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes); + } + + bool needLatinKeyCodes = !isLatin; + if (!needLatinKeyCodes) { + needLatinKeyCodes = + (IS_ASCII_ALPHABETICAL(altCharCodes.mUnshiftedCharCode) != + IS_ASCII_ALPHABETICAL(altCharCodes.mShiftedCharCode)); + } + + // If current keyboard layout can input Latin characters, we don't need + // more information. + if (!needLatinKeyCodes) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p WillDispatchKeyboardEventInternal, " + "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d, altCharCodes={ " + "mUnshiftedCharCode=0x%08X, mShiftedCharCode=0x%08X }", + this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level, + altCharCodes.mUnshiftedCharCode, altCharCodes.mShiftedCharCode)); + return; + } + + // Next, find Latin inputtable keyboard layout. + gint minGroup = GetFirstLatinGroup(); + if (minGroup < 0) { + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p WillDispatchKeyboardEventInternal, " + "Latin keyboard layout isn't found: " + "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d, " + "altCharCodes={ mUnshiftedCharCode=0x%08X, " + "mShiftedCharCode=0x%08X }", + this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level, + altCharCodes.mUnshiftedCharCode, altCharCodes.mShiftedCharCode)); + return; + } + + AlternativeCharCode altLatinCharCodes(0, 0); + uint32_t unmodifiedCh = aKeyEvent.IsShift() ? altCharCodes.mShiftedCharCode + : altCharCodes.mUnshiftedCharCode; + + // unshifted charcode of found keyboard layout. + uint32_t ch = GetCharCodeFor(aGdkKeyEvent, baseState, minGroup); + altLatinCharCodes.mUnshiftedCharCode = + IsBasicLatinLetterOrNumeral(ch) ? ch : 0; + // shifted charcode of found keyboard layout. + ch = GetCharCodeFor(aGdkKeyEvent, baseState | GetModifierMask(SHIFT), + minGroup); + altLatinCharCodes.mShiftedCharCode = IsBasicLatinLetterOrNumeral(ch) ? ch : 0; + if (altLatinCharCodes.mUnshiftedCharCode || + altLatinCharCodes.mShiftedCharCode) { + aKeyEvent.mAlternativeCharCodes.AppendElement(altLatinCharCodes); + } + // If the mCharCode is not Latin, and the level is 0 or 1, we should + // replace the mCharCode to Latin char if Alt and Meta keys are not + // pressed. (Alt should be sent the localized char for accesskey + // like handling of Web Applications.) + ch = aKeyEvent.IsShift() ? altLatinCharCodes.mShiftedCharCode + : altLatinCharCodes.mUnshiftedCharCode; + if (ch && !(aKeyEvent.IsAlt() || aKeyEvent.IsMeta()) && + charCode == unmodifiedCh) { + aKeyEvent.SetCharCode(ch); + } + + MOZ_LOG(gKeymapWrapperLog, LogLevel::Info, + ("%p WillDispatchKeyboardEventInternal, " + "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d, minGroup=%d, " + "altCharCodes={ mUnshiftedCharCode=0x%08X, " + "mShiftedCharCode=0x%08X } " + "altLatinCharCodes={ mUnshiftedCharCode=0x%08X, " + "mShiftedCharCode=0x%08X }", + this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level, minGroup, + altCharCodes.mUnshiftedCharCode, altCharCodes.mShiftedCharCode, + altLatinCharCodes.mUnshiftedCharCode, + altLatinCharCodes.mShiftedCharCode)); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/nsGtkKeyUtils.h b/widget/gtk/nsGtkKeyUtils.h new file mode 100644 index 0000000000..3354cf2fee --- /dev/null +++ b/widget/gtk/nsGtkKeyUtils.h @@ -0,0 +1,467 @@ +/* -*- 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 __nsGdkKeyUtils_h__ +#define __nsGdkKeyUtils_h__ + +#include "nsTArray.h" +#include "mozilla/EventForwards.h" + +#include <gdk/gdk.h> +#include <X11/XKBlib.h> +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +# include <xkbcommon/xkbcommon.h> +#endif + +class nsWindow; + +namespace mozilla { +namespace widget { + +/** + * KeymapWrapper is a wrapper class of GdkKeymap. GdkKeymap doesn't support + * all our needs, therefore, we need to access lower level APIs. + * But such code is usually complex and might be slow. Against such issues, + * we should cache some information. + * + * This class provides only static methods. The methods is using internal + * singleton instance which is initialized by default GdkKeymap. When the + * GdkKeymap is destroyed, the singleton instance will be destroyed. + */ + +class KeymapWrapper { + public: + /** + * Compute an our DOM keycode from a GDK keyval. + */ + static uint32_t ComputeDOMKeyCode(const GdkEventKey* aGdkKeyEvent); + + /** + * Compute a DOM key name index from aGdkKeyEvent. + */ + static KeyNameIndex ComputeDOMKeyNameIndex(const GdkEventKey* aGdkKeyEvent); + + /** + * Compute a DOM code name index from aGdkKeyEvent. + */ + static CodeNameIndex ComputeDOMCodeNameIndex(const GdkEventKey* aGdkKeyEvent); + + /** + * Modifier is list of modifiers which we support in widget level. + */ + enum Modifier { + NOT_MODIFIER = 0x0000, + CAPS_LOCK = 0x0001, + NUM_LOCK = 0x0002, + SCROLL_LOCK = 0x0004, + SHIFT = 0x0008, + CTRL = 0x0010, + ALT = 0x0020, + META = 0x0040, + SUPER = 0x0080, + HYPER = 0x0100, + LEVEL3 = 0x0200, + LEVEL5 = 0x0400 + }; + + /** + * Modifiers is used for combination of Modifier. + * E.g., |Modifiers modifiers = (SHIFT | CTRL);| means Shift and Ctrl. + */ + typedef uint32_t Modifiers; + + /** + * GetCurrentModifierState() returns current modifier key state. + * The "current" means actual state of hardware keyboard when this is + * called. I.e., if some key events are not still dispatched by GDK, + * the state may mismatch with GdkEventKey::state. + * + * @return Current modifier key state. + */ + static guint GetCurrentModifierState(); + + /** + * AreModifiersCurrentlyActive() checks the "current" modifier state + * on aGdkWindow with the keymap of the singleton instance. + * + * @param aModifiers One or more of Modifier values except + * NOT_MODIFIER. + * @return TRUE if all of modifieres in aModifiers are + * active. Otherwise, FALSE. + */ + static bool AreModifiersCurrentlyActive(Modifiers aModifiers); + + /** + * Utility function to compute current keyboard modifiers for + * WidgetInputEvent + */ + static uint32_t ComputeCurrentKeyModifiers(); + + /** + * Utility function to covert platform modifier state to keyboard modifiers + * of WidgetInputEvent + */ + static uint32_t ComputeKeyModifiers(guint aModifierState); + + /** + * InitInputEvent() initializes the aInputEvent with aModifierState. + */ + static void InitInputEvent(WidgetInputEvent& aInputEvent, + guint aModifierState); + + /** + * InitKeyEvent() intializes aKeyEvent's modifier key related members + * and keycode related values. + * + * @param aKeyEvent It's an WidgetKeyboardEvent which needs to be + * initialized. + * @param aGdkKeyEvent A native GDK key event. + * @param aIsProcessedByIME true if aGdkKeyEvent is handled by IME. + */ + static void InitKeyEvent(WidgetKeyboardEvent& aKeyEvent, + GdkEventKey* aGdkKeyEvent, bool aIsProcessedByIME); + + /** + * DispatchKeyDownOrKeyUpEvent() dispatches eKeyDown or eKeyUp event. + * + * @param aWindow The window to dispatch a keyboard event. + * @param aGdkKeyEvent A native GDK_KEY_PRESS or GDK_KEY_RELEASE + * event. + * @param aIsProcessedByIME true if the event is handled by IME. + * @param aIsCancelled [Out] true if the default is prevented. + * @return true if eKeyDown event is actually dispatched. + * Otherwise, false. + */ + static bool DispatchKeyDownOrKeyUpEvent(nsWindow* aWindow, + GdkEventKey* aGdkKeyEvent, + bool aIsProcessedByIME, + bool* aIsCancelled); + + /** + * DispatchKeyDownOrKeyUpEvent() dispatches eKeyDown or eKeyUp event. + * + * @param aWindow The window to dispatch aKeyboardEvent. + * @param aKeyboardEvent An eKeyDown or eKeyUp event. This will be + * dispatched as is. + * @param aIsCancelled [Out] true if the default is prevented. + * @return true if eKeyDown event is actually dispatched. + * Otherwise, false. + */ + static bool DispatchKeyDownOrKeyUpEvent(nsWindow* aWindow, + WidgetKeyboardEvent& aKeyboardEvent, + bool* aIsCancelled); + + /** + * GDK_KEY_PRESS event handler. + * + * @param aWindow The window to dispatch eKeyDown event (and maybe + * eKeyPress events). + * @param aGdkKeyEvent Receivied GDK_KEY_PRESS event. + */ + static void HandleKeyPressEvent(nsWindow* aWindow, GdkEventKey* aGdkKeyEvent); + + /** + * GDK_KEY_RELEASE event handler. + * + * @param aWindow The window to dispatch eKeyUp event. + * @param aGdkKeyEvent Receivied GDK_KEY_RELEASE event. + * @return true if an event is dispatched. Otherwise, false. + */ + static bool HandleKeyReleaseEvent(nsWindow* aWindow, + GdkEventKey* aGdkKeyEvent); + + /** + * WillDispatchKeyboardEvent() is called via + * TextEventDispatcherListener::WillDispatchKeyboardEvent(). + * + * @param aKeyEvent An instance of KeyboardEvent which will be + * dispatched. This method should set charCode + * and alternative char codes if it's necessary. + * @param aGdkKeyEvent A GdkEventKey instance which caused the + * aKeyEvent. + */ + static void WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyEvent, + GdkEventKey* aGdkKeyEvent); + +#ifdef MOZ_WAYLAND + /** + * Utility function to set all supported modifier masks + * from xkb_keymap. We call that from Wayland backend routines. + */ + static void SetModifierMasks(xkb_keymap* aKeymap); +#endif + + /** + * ResetKeyboard is called on keymap changes from OnKeysChanged and + * keyboard_handle_keymap to prepare for keymap changes. + */ + static void ResetKeyboard(); + + /** + * Destroys the singleton KeymapWrapper instance, if it exists. + */ + static void Shutdown(); + + protected: + /** + * GetInstance() returns a KeymapWrapper instance. + * + * @return A singleton instance of KeymapWrapper. + */ + static KeymapWrapper* GetInstance(); + + KeymapWrapper(); + ~KeymapWrapper(); + + bool mInitialized; + + /** + * Initializing methods. + */ + void Init(); + void InitXKBExtension(); + void InitBySystemSettingsX11(); +#ifdef MOZ_WAYLAND + void InitBySystemSettingsWayland(); +#endif + + /** + * mModifierKeys stores each hardware key information. + */ + struct ModifierKey { + guint mHardwareKeycode; + guint mMask; + + explicit ModifierKey(guint aHardwareKeycode) + : mHardwareKeycode(aHardwareKeycode), mMask(0) {} + }; + nsTArray<ModifierKey> mModifierKeys; + + /** + * GetModifierKey() returns modifier key information of the hardware + * keycode. If the key isn't a modifier key, returns nullptr. + */ + ModifierKey* GetModifierKey(guint aHardwareKeycode); + + /** + * mModifierMasks is bit masks for each modifier. The index should be one + * of ModifierIndex values. + */ + enum ModifierIndex { + INDEX_NUM_LOCK, + INDEX_SCROLL_LOCK, + INDEX_ALT, + INDEX_META, + INDEX_SUPER, + INDEX_HYPER, + INDEX_LEVEL3, + INDEX_LEVEL5, + COUNT_OF_MODIFIER_INDEX + }; + guint mModifierMasks[COUNT_OF_MODIFIER_INDEX]; + + guint GetModifierMask(Modifier aModifier) const; + + /** + * @param aGdkKeyval A GDK defined modifier key value such as + * GDK_Shift_L. + * @return Returns Modifier values for aGdkKeyval. + * If the given key code isn't a modifier key, + * returns NOT_MODIFIER. + */ + static Modifier GetModifierForGDKKeyval(guint aGdkKeyval); + + static const char* GetModifierName(Modifier aModifier); + + /** + * AreModifiersActive() just checks whether aModifierState indicates + * all modifiers in aModifiers are active or not. + * + * @param aModifiers One or more of Modifier values except + * NOT_MODIFIER. + * @param aModifierState GDK's modifier states. + * @return TRUE if aGdkModifierType indecates all of + * modifiers in aModifier are active. + * Otherwise, FALSE. + */ + static bool AreModifiersActive(Modifiers aModifiers, guint aModifierState); + + /** + * mGdkKeymap is a wrapped instance by this class. + */ + GdkKeymap* mGdkKeymap; + + /** + * The base event code of XKB extension. + */ + int mXKBBaseEventCode; + + /** + * Only auto_repeats[] stores valid value. If you need to use other + * members, you need to listen notification events for them. + * See a call of XkbSelectEventDetails() with XkbControlsNotify in + * InitXKBExtension(). + */ + XKeyboardState mKeyboardState; + + /** + * Pointer of the singleton instance. + */ + static KeymapWrapper* sInstance; + + /** + * Auto key repeat management. + */ + static guint sLastRepeatableHardwareKeyCode; + static Time sLastRepeatableKeyTime; + enum RepeatState { NOT_PRESSED, FIRST_PRESS, REPEATING }; + static RepeatState sRepeatState; + + /** + * IsAutoRepeatableKey() returns true if the key supports auto repeat. + * Otherwise, false. + */ + bool IsAutoRepeatableKey(guint aHardwareKeyCode); + + /** + * Signal handlers. + */ + static void OnKeysChanged(GdkKeymap* aKeymap, KeymapWrapper* aKeymapWrapper); + static void OnDirectionChanged(GdkKeymap* aGdkKeymap, + KeymapWrapper* aKeymapWrapper); + + gulong mOnKeysChangedSignalHandle; + gulong mOnDirectionChangedSignalHandle; + + /** + * GetCharCodeFor() Computes what character is inputted by the key event + * with aModifierState and aGroup. + * + * @param aGdkKeyEvent Native key event, must not be nullptr. + * @param aModifierState Combination of GdkModifierType which you + * want to test with aGdkKeyEvent. + * @param aGroup Set group in the mGdkKeymap. + * @return charCode which is inputted by aGdkKeyEvent. + * If failed, this returns 0. + */ + static uint32_t GetCharCodeFor(const GdkEventKey* aGdkKeyEvent); + uint32_t GetCharCodeFor(const GdkEventKey* aGdkKeyEvent, guint aModifierState, + gint aGroup); + + /** + * GetUnmodifiedCharCodeFor() computes what character is inputted by the + * key event without Ctrl/Alt/Meta/Super/Hyper modifiers. + * If Level3 or Level5 Shift causes no character input, this also ignores + * them. + * + * @param aGdkKeyEvent Native key event, must not be nullptr. + * @return charCode which is computed without modifiers + * which prevent text input. + */ + uint32_t GetUnmodifiedCharCodeFor(const GdkEventKey* aGdkKeyEvent); + + /** + * GetKeyLevel() returns level of the aGdkKeyEvent in mGdkKeymap. + * + * @param aGdkKeyEvent Native key event, must not be nullptr. + * @return Using level. Typically, this is 0 or 1. + * If failed, this returns -1. + */ + gint GetKeyLevel(GdkEventKey* aGdkKeyEvent); + + /** + * GetFirstLatinGroup() returns group of mGdkKeymap which can input an + * ASCII character by GDK_A. + * + * @return group value of GdkEventKey. + */ + gint GetFirstLatinGroup(); + + /** + * IsLatinGroup() checkes whether the keyboard layout of aGroup is + * ASCII alphabet inputtable or not. + * + * @param aGroup The group value of GdkEventKey. + * @return TRUE if the keyboard layout can input + * ASCII alphabet. Otherwise, FALSE. + */ + bool IsLatinGroup(guint8 aGroup); + + /** + * IsBasicLatinLetterOrNumeral() Checks whether the aCharCode is an + * alphabet or a numeric character in ASCII. + * + * @param aCharCode Charcode which you want to test. + * @return TRUE if aCharCode is an alphabet or a numeric + * in ASCII range. Otherwise, FALSE. + */ + static bool IsBasicLatinLetterOrNumeral(uint32_t aCharCode); + + /** + * IsPrintableASCIICharacter() checks whether the aCharCode is a printable + * ASCII character. I.e., returns false if aCharCode is a control + * character even in an ASCII character. + */ + static bool IsPrintableASCIICharacter(uint32_t aCharCode) { + return aCharCode >= 0x20 && aCharCode <= 0x7E; + } + + /** + * GetGDKKeyvalWithoutModifier() returns the keyval for aGdkKeyEvent when + * ignoring the modifier state except NumLock. (NumLock is a key to change + * some key's meaning.) + */ + static guint GetGDKKeyvalWithoutModifier(const GdkEventKey* aGdkKeyEvent); + + /** + * GetDOMKeyCodeFromKeyPairs() returns DOM keycode for aGdkKeyval if + * it's in KeyPair table. + */ + static uint32_t GetDOMKeyCodeFromKeyPairs(guint aGdkKeyval); + + /** + * FilterEvents() listens all events on all our windows. + * Be careful, this may make damage to performance if you add expensive + * code in this method. + */ + static GdkFilterReturn FilterEvents(GdkXEvent* aXEvent, GdkEvent* aGdkEvent, + gpointer aData); + + /** + * MaybeDispatchContextMenuEvent() may dispatch eContextMenu event if + * the given key combination should cause opening context menu. + * + * @param aWindow The window to dispatch a contextmenu event. + * @param aEvent The native key event. + * @return true if this method dispatched eContextMenu + * event. Otherwise, false. + * Be aware, when this returns true, the + * widget may have been destroyed. + */ + static bool MaybeDispatchContextMenuEvent(nsWindow* aWindow, + const GdkEventKey* aEvent); + + /** + * See the document of WillDispatchKeyboardEvent(). + */ + void WillDispatchKeyboardEventInternal(WidgetKeyboardEvent& aKeyEvent, + GdkEventKey* aGdkKeyEvent); + +#ifdef MOZ_WAYLAND + /** + * Utility function to set Xkb modifier key mask. + */ + void SetModifierMask(xkb_keymap* aKeymap, ModifierIndex aModifierIndex, + const char* aModifierName); +#endif +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __nsGdkKeyUtils_h__ */ diff --git a/widget/gtk/nsGtkUtils.h b/widget/gtk/nsGtkUtils.h new file mode 100644 index 0000000000..58cf2eed16 --- /dev/null +++ b/widget/gtk/nsGtkUtils.h @@ -0,0 +1,23 @@ +/* -*- 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 nsGtkUtils_h__ +#define nsGtkUtils_h__ + +#include <glib.h> + +// Some gobject functions expect functions for gpointer arguments. +// gpointer is void* but C++ doesn't like casting functions to void*. +template <class T> +static inline gpointer FuncToGpointer(T aFunction) { + return reinterpret_cast<gpointer>( + reinterpret_cast<uintptr_t> + // This cast just provides a warning if T is not a function. + (reinterpret_cast<void (*)()>(aFunction))); +} + +#endif // nsGtkUtils_h__ diff --git a/widget/gtk/nsIImageToPixbuf.h b/widget/gtk/nsIImageToPixbuf.h new file mode 100644 index 0000000000..396d1b98b5 --- /dev/null +++ b/widget/gtk/nsIImageToPixbuf.h @@ -0,0 +1,38 @@ +/* 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 NSIIMAGETOPIXBUF_H_ +#define NSIIMAGETOPIXBUF_H_ + +#include "nsISupports.h" + +// dfa4ac93-83f2-4ab8-9b2a-0ff7022aebe2 +#define NSIIMAGETOPIXBUF_IID \ + { \ + 0xdfa4ac93, 0x83f2, 0x4ab8, { \ + 0x9b, 0x2a, 0x0f, 0xf7, 0x02, 0x2a, 0xeb, 0xe2 \ + } \ + } + +class imgIContainer; +typedef struct _GdkPixbuf GdkPixbuf; + +/** + * An interface that allows converting the current frame of an imgIContainer to + * a GdkPixbuf*. + */ +class nsIImageToPixbuf : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NSIIMAGETOPIXBUF_IID) + + /** + * The return value, if not null, should be released as needed + * by the caller using g_object_unref. + */ + NS_IMETHOD_(GdkPixbuf*) ConvertImageToPixbuf(imgIContainer* aImage) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIImageToPixbuf, NSIIMAGETOPIXBUF_IID) + +#endif diff --git a/widget/gtk/nsImageToPixbuf.cpp b/widget/gtk/nsImageToPixbuf.cpp new file mode 100644 index 0000000000..c70e589576 --- /dev/null +++ b/widget/gtk/nsImageToPixbuf.cpp @@ -0,0 +1,108 @@ +/* vim:set sw=2 sts=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include "nsImageToPixbuf.h" + +#include "imgIContainer.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" + +using mozilla::gfx::DataSourceSurface; +using mozilla::gfx::SurfaceFormat; + +NS_IMPL_ISUPPORTS(nsImageToPixbuf, nsIImageToPixbuf) + +inline unsigned char unpremultiply(unsigned char color, unsigned char alpha) { + if (alpha == 0) return 0; + // plus alpha/2 to round instead of truncate + return (color * 255 + alpha / 2) / alpha; +} + +NS_IMETHODIMP_(GdkPixbuf*) +nsImageToPixbuf::ConvertImageToPixbuf(imgIContainer* aImage) { + return ImageToPixbuf(aImage); +} + +GdkPixbuf* nsImageToPixbuf::ImageToPixbuf(imgIContainer* aImage) { + RefPtr<SourceSurface> surface = aImage->GetFrame( + imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + + // If the last call failed, it was probably because our call stack originates + // in an imgINotificationObserver event, meaning that we're not allowed + // request a sync decode. Presumably the originating event is something + // sensible like OnStopFrame(), so we can just retry the call without a sync + // decode. + if (!surface) + surface = aImage->GetFrame(imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_NONE); + + NS_ENSURE_TRUE(surface, nullptr); + + return SourceSurfaceToPixbuf(surface, surface->GetSize().width, + surface->GetSize().height); +} + +GdkPixbuf* nsImageToPixbuf::SourceSurfaceToPixbuf(SourceSurface* aSurface, + int32_t aWidth, + int32_t aHeight) { + MOZ_ASSERT(aWidth <= aSurface->GetSize().width && + aHeight <= aSurface->GetSize().height, + "Requested rect is bigger than the supplied surface"); + + GdkPixbuf* pixbuf = + gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, aWidth, aHeight); + if (!pixbuf) return nullptr; + + uint32_t destStride = gdk_pixbuf_get_rowstride(pixbuf); + guchar* destPixels = gdk_pixbuf_get_pixels(pixbuf); + + RefPtr<DataSourceSurface> dataSurface = aSurface->GetDataSurface(); + DataSourceSurface::MappedSurface map; + if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) return nullptr; + + uint8_t* srcData = map.mData; + int32_t srcStride = map.mStride; + + SurfaceFormat format = dataSurface->GetFormat(); + + for (int32_t row = 0; row < aHeight; ++row) { + for (int32_t col = 0; col < aWidth; ++col) { + guchar* destPixel = destPixels + row * destStride + 4 * col; + + uint32_t* srcPixel = + reinterpret_cast<uint32_t*>((srcData + row * srcStride + 4 * col)); + + if (format == SurfaceFormat::B8G8R8A8) { + const uint8_t a = (*srcPixel >> 24) & 0xFF; + const uint8_t r = unpremultiply((*srcPixel >> 16) & 0xFF, a); + const uint8_t g = unpremultiply((*srcPixel >> 8) & 0xFF, a); + const uint8_t b = unpremultiply((*srcPixel >> 0) & 0xFF, a); + + *destPixel++ = r; + *destPixel++ = g; + *destPixel++ = b; + *destPixel++ = a; + } else { + MOZ_ASSERT(format == SurfaceFormat::B8G8R8X8); + + const uint8_t r = (*srcPixel >> 16) & 0xFF; + const uint8_t g = (*srcPixel >> 8) & 0xFF; + const uint8_t b = (*srcPixel >> 0) & 0xFF; + + *destPixel++ = r; + *destPixel++ = g; + *destPixel++ = b; + *destPixel++ = 0xFF; // A + } + } + } + + dataSurface->Unmap(); + + return pixbuf; +} diff --git a/widget/gtk/nsImageToPixbuf.h b/widget/gtk/nsImageToPixbuf.h new file mode 100644 index 0000000000..3f30201ac5 --- /dev/null +++ b/widget/gtk/nsImageToPixbuf.h @@ -0,0 +1,47 @@ +/* vim:set sw=2 sts=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSIMAGETOPIXBUF_H_ +#define NSIMAGETOPIXBUF_H_ + +#include "nsIImageToPixbuf.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace gfx { +class SourceSurface; +} +} // namespace mozilla + +class nsImageToPixbuf final : public nsIImageToPixbuf { + typedef mozilla::gfx::SourceSurface SourceSurface; + + public: + NS_DECL_ISUPPORTS + NS_IMETHOD_(GdkPixbuf*) ConvertImageToPixbuf(imgIContainer* aImage) override; + + // Friendlier version of ConvertImageToPixbuf for callers inside of + // widget + /** + * The return value of all these, if not null, should be + * released as needed by the caller using g_object_unref. + */ + static GdkPixbuf* ImageToPixbuf(imgIContainer* aImage); + static GdkPixbuf* SourceSurfaceToPixbuf(SourceSurface* aSurface, + int32_t aWidth, int32_t aHeight); + + private: + ~nsImageToPixbuf() = default; +}; + +// fc2389b8-c650-4093-9e42-b05e5f0685b7 +#define NS_IMAGE_TO_PIXBUF_CID \ + { \ + 0xfc2389b8, 0xc650, 0x4093, { \ + 0x9e, 0x42, 0xb0, 0x5e, 0x5f, 0x06, 0x85, 0xb7 \ + } \ + } + +#endif diff --git a/widget/gtk/nsLookAndFeel.cpp b/widget/gtk/nsLookAndFeel.cpp new file mode 100644 index 0000000000..0fdc7748ce --- /dev/null +++ b/widget/gtk/nsLookAndFeel.cpp @@ -0,0 +1,1536 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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/. */ + +// for strtod() +#include <stdlib.h> + +#include "nsLookAndFeel.h" + +#include <gtk/gtk.h> +#include <gdk/gdk.h> + +#include <pango/pango.h> +#include <pango/pango-fontmap.h> + +#include <fontconfig/fontconfig.h> +#include "gfxPlatformGtk.h" +#include "mozilla/FontPropertyTypes.h" +#include "mozilla/RelativeLuminanceUtils.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/Telemetry.h" +#include "ScreenHelperGTK.h" +#include "nsNativeBasicThemeGTK.h" + +#include "gtkdrawing.h" +#include "nsStyleConsts.h" +#include "gfxFontConstants.h" +#include "WidgetUtils.h" +#include "nsWindow.h" + +#include "mozilla/gfx/2D.h" + +#include <cairo-gobject.h> +#include "WidgetStyleCache.h" +#include "prenv.h" +#include "nsCSSColorUtils.h" + +using namespace mozilla; +using mozilla::LookAndFeel; + +#undef LOG +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetLog; +# define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args) +#else +# define LOG(args) +#endif /* MOZ_LOGGING */ + +#define GDK_COLOR_TO_NS_RGB(c) \ + ((nscolor)NS_RGB(c.red >> 8, c.green >> 8, c.blue >> 8)) +#define GDK_RGBA_TO_NS_RGBA(c) \ + ((nscolor)NS_RGBA((int)((c).red * 255), (int)((c).green * 255), \ + (int)((c).blue * 255), (int)((c).alpha * 255))) + +nsLookAndFeel::nsLookAndFeel(const LookAndFeelCache* aCache) { + if (aCache) { + DoSetCache(*aCache); + } +} + +nsLookAndFeel::~nsLookAndFeel() = default; + +// Modifies color |*aDest| as if a pattern of color |aSource| was painted with +// CAIRO_OPERATOR_OVER to a surface with color |*aDest|. +static void ApplyColorOver(const GdkRGBA& aSource, GdkRGBA* aDest) { + gdouble sourceCoef = aSource.alpha; + gdouble destCoef = aDest->alpha * (1.0 - sourceCoef); + gdouble resultAlpha = sourceCoef + destCoef; + if (resultAlpha != 0.0) { // don't divide by zero + destCoef /= resultAlpha; + sourceCoef /= resultAlpha; + aDest->red = sourceCoef * aSource.red + destCoef * aDest->red; + aDest->green = sourceCoef * aSource.green + destCoef * aDest->green; + aDest->blue = sourceCoef * aSource.blue + destCoef * aDest->blue; + aDest->alpha = resultAlpha; + } +} + +static void GetLightAndDarkness(const GdkRGBA& aColor, double* aLightness, + double* aDarkness) { + double sum = aColor.red + aColor.green + aColor.blue; + *aLightness = sum * aColor.alpha; + *aDarkness = (3.0 - sum) * aColor.alpha; +} + +static bool GetGradientColors(const GValue* aValue, GdkRGBA* aLightColor, + GdkRGBA* aDarkColor) { + if (!G_TYPE_CHECK_VALUE_TYPE(aValue, CAIRO_GOBJECT_TYPE_PATTERN)) + return false; + + auto pattern = static_cast<cairo_pattern_t*>(g_value_get_boxed(aValue)); + if (!pattern) return false; + + // Just picking the lightest and darkest colors as simple samples rather + // than trying to blend, which could get messy if there are many stops. + if (CAIRO_STATUS_SUCCESS != + cairo_pattern_get_color_stop_rgba(pattern, 0, nullptr, &aDarkColor->red, + &aDarkColor->green, &aDarkColor->blue, + &aDarkColor->alpha)) + return false; + + double maxLightness, maxDarkness; + GetLightAndDarkness(*aDarkColor, &maxLightness, &maxDarkness); + *aLightColor = *aDarkColor; + + GdkRGBA stop; + for (int index = 1; + CAIRO_STATUS_SUCCESS == + cairo_pattern_get_color_stop_rgba(pattern, index, nullptr, &stop.red, + &stop.green, &stop.blue, &stop.alpha); + ++index) { + double lightness, darkness; + GetLightAndDarkness(stop, &lightness, &darkness); + if (lightness > maxLightness) { + maxLightness = lightness; + *aLightColor = stop; + } + if (darkness > maxDarkness) { + maxDarkness = darkness; + *aDarkColor = stop; + } + } + + return true; +} + +static bool GetUnicoBorderGradientColors(GtkStyleContext* aContext, + GdkRGBA* aLightColor, + GdkRGBA* aDarkColor) { + // Ubuntu 12.04 has GTK engine Unico-1.0.2, which overrides render_frame, + // providing its own border code. Ubuntu 14.04 has + // Unico-1.0.3+14.04.20140109, which does not override render_frame, and + // so does not need special attention. The earlier Unico can be detected + // by the -unico-border-gradient style property it registers. + // gtk_style_properties_lookup_property() is checked first to avoid the + // warning from gtk_style_context_get_property() when the property does + // not exist. (gtk_render_frame() of GTK+ 3.16 no longer uses the + // engine.) + const char* propertyName = "-unico-border-gradient"; + if (!gtk_style_properties_lookup_property(propertyName, nullptr, nullptr)) + return false; + + // -unico-border-gradient is used only when the CSS node's engine is Unico. + GtkThemingEngine* engine; + GtkStateFlags state = gtk_style_context_get_state(aContext); + gtk_style_context_get(aContext, state, "engine", &engine, nullptr); + if (strcmp(g_type_name(G_TYPE_FROM_INSTANCE(engine)), "UnicoEngine") != 0) + return false; + + // draw_border() of Unico engine uses -unico-border-gradient + // in preference to border-color. + GValue value = G_VALUE_INIT; + gtk_style_context_get_property(aContext, propertyName, state, &value); + + bool result = GetGradientColors(&value, aLightColor, aDarkColor); + + g_value_unset(&value); + return result; +} + +// Sets |aLightColor| and |aDarkColor| to colors from |aContext|. Returns +// true if |aContext| uses these colors to render a visible border. +// If returning false, then the colors returned are a fallback from the +// border-color value even though |aContext| does not use these colors to +// render a border. +static bool GetBorderColors(GtkStyleContext* aContext, GdkRGBA* aLightColor, + GdkRGBA* aDarkColor) { + // Determine whether the border on this style context is visible. + GtkStateFlags state = gtk_style_context_get_state(aContext); + GtkBorderStyle borderStyle; + gtk_style_context_get(aContext, state, GTK_STYLE_PROPERTY_BORDER_STYLE, + &borderStyle, nullptr); + bool visible = borderStyle != GTK_BORDER_STYLE_NONE && + borderStyle != GTK_BORDER_STYLE_HIDDEN; + if (visible) { + // GTK has an initial value of zero for border-widths, and so themes + // need to explicitly set border-widths to make borders visible. + GtkBorder border; + gtk_style_context_get_border(aContext, state, &border); + visible = border.top != 0 || border.right != 0 || border.bottom != 0 || + border.left != 0; + } + + if (visible && + GetUnicoBorderGradientColors(aContext, aLightColor, aDarkColor)) + return true; + + // The initial value for the border-color is the foreground color, and so + // this will usually return a color distinct from the background even if + // there is no visible border detected. + gtk_style_context_get_border_color(aContext, state, aDarkColor); + // TODO GTK3 - update aLightColor + // for GTK_BORDER_STYLE_INSET/OUTSET/GROVE/RIDGE border styles. + // https://bugzilla.mozilla.org/show_bug.cgi?id=978172#c25 + *aLightColor = *aDarkColor; + return visible; +} + +static bool GetBorderColors(GtkStyleContext* aContext, nscolor* aLightColor, + nscolor* aDarkColor) { + GdkRGBA lightColor, darkColor; + bool ret = GetBorderColors(aContext, &lightColor, &darkColor); + *aLightColor = GDK_RGBA_TO_NS_RGBA(lightColor); + *aDarkColor = GDK_RGBA_TO_NS_RGBA(darkColor); + return ret; +} + +// Finds ideal cell highlight colors used for unfocused+selected cells distinct +// from both Highlight, used as focused+selected background, and the listbox +// background which is assumed to be similar to -moz-field +nsresult nsLookAndFeel::InitCellHighlightColors() { + // NS_SUFFICIENT_LUMINOSITY_DIFFERENCE is the a11y standard for text + // on a background. Use 20% of that standard since we have a background + // on top of another background + int32_t minLuminosityDifference = NS_SUFFICIENT_LUMINOSITY_DIFFERENCE / 5; + int32_t backLuminosityDifference = + NS_LUMINOSITY_DIFFERENCE(mMozWindowBackground, mFieldBackground); + if (backLuminosityDifference >= minLuminosityDifference) { + mMozCellHighlightBackground = mMozWindowBackground; + mMozCellHighlightText = mMozWindowText; + return NS_OK; + } + + uint16_t hue, sat, luminance; + uint8_t alpha; + mMozCellHighlightBackground = mFieldBackground; + mMozCellHighlightText = mFieldText; + + NS_RGB2HSV(mMozCellHighlightBackground, hue, sat, luminance, alpha); + + uint16_t step = 30; + // Lighten the color if the color is very dark + if (luminance <= step) { + luminance += step; + } + // Darken it if it is very light + else if (luminance >= 255 - step) { + luminance -= step; + } + // Otherwise, compute what works best depending on the text luminance. + else { + uint16_t textHue, textSat, textLuminance; + uint8_t textAlpha; + NS_RGB2HSV(mMozCellHighlightText, textHue, textSat, textLuminance, + textAlpha); + // Text is darker than background, use a lighter shade + if (textLuminance < luminance) { + luminance += step; + } + // Otherwise, use a darker shade + else { + luminance -= step; + } + } + NS_HSV2RGB(mMozCellHighlightBackground, hue, sat, luminance, alpha); + return NS_OK; +} + +void nsLookAndFeel::NativeInit() { EnsureInit(); } + +void nsLookAndFeel::RefreshImpl() { + nsXPLookAndFeel::RefreshImpl(); + moz_gtk_refresh(); + + mInitialized = false; +} + +widget::LookAndFeelCache nsLookAndFeel::GetCacheImpl() { + LookAndFeelCache cache = nsXPLookAndFeel::GetCacheImpl(); + + constexpr IntID kIntIdsToCache[] = {IntID::SystemUsesDarkTheme, + IntID::PrefersReducedMotion, + IntID::UseAccessibilityTheme}; + + constexpr ColorID kColorIdsToCache[] = { + ColorID::ThemedScrollbar, + ColorID::ThemedScrollbarInactive, + ColorID::ThemedScrollbarThumb, + ColorID::ThemedScrollbarThumbHover, + ColorID::ThemedScrollbarThumbActive, + ColorID::ThemedScrollbarThumbInactive}; + + for (IntID id : kIntIdsToCache) { + cache.mInts().AppendElement(LookAndFeelInt(id, GetInt(id))); + } + + for (ColorID id : kColorIdsToCache) { + cache.mColors().AppendElement(LookAndFeelColor(id, GetColor(id))); + } + + return cache; +} + +void nsLookAndFeel::SetCacheImpl(const LookAndFeelCache& aCache) { + DoSetCache(aCache); +} + +void nsLookAndFeel::DoSetCache(const LookAndFeelCache& aCache) { + for (const auto& entry : aCache.mInts()) { + switch (entry.id()) { + case IntID::SystemUsesDarkTheme: + mSystemUsesDarkTheme = entry.value(); + break; + case IntID::PrefersReducedMotion: + mPrefersReducedMotion = entry.value(); + break; + case IntID::UseAccessibilityTheme: + mHighContrast = entry.value(); + break; + default: + MOZ_ASSERT_UNREACHABLE("Bogus Int ID in cache"); + break; + } + } + for (const auto& entry : aCache.mColors()) { + switch (entry.id()) { + case ColorID::ThemedScrollbar: + mThemedScrollbar = entry.color(); + break; + case ColorID::ThemedScrollbarInactive: + mThemedScrollbarInactive = entry.color(); + break; + case ColorID::ThemedScrollbarThumb: + mThemedScrollbarThumb = entry.color(); + break; + case ColorID::ThemedScrollbarThumbHover: + mThemedScrollbarThumbHover = entry.color(); + break; + case ColorID::ThemedScrollbarThumbActive: + mThemedScrollbarThumbActive = entry.color(); + break; + case ColorID::ThemedScrollbarThumbInactive: + mThemedScrollbarThumbInactive = entry.color(); + break; + default: + MOZ_ASSERT_UNREACHABLE("Bogus Color ID in cache"); + break; + } + } +} + +nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { + EnsureInit(); + + nsresult res = NS_OK; + + switch (aID) { + // These colors don't seem to be used for anything anymore in Mozilla + // (except here at least TextSelectBackground and TextSelectForeground) + // The CSS2 colors below are used. + case ColorID::WindowBackground: + case ColorID::WidgetBackground: + case ColorID::TextBackground: + case ColorID::Activecaption: // active window caption background + case ColorID::Appworkspace: // MDI background color + case ColorID::Background: // desktop background + case ColorID::Window: + case ColorID::Windowframe: + case ColorID::MozDialog: + case ColorID::MozCombobox: + aColor = mMozWindowBackground; + break; + case ColorID::WindowForeground: + case ColorID::WidgetForeground: + case ColorID::TextForeground: + case ColorID::Captiontext: // text in active window caption, size box, and + // scrollbar arrow box (!) + case ColorID::Windowtext: + case ColorID::MozDialogtext: + aColor = mMozWindowText; + break; + case ColorID::WidgetSelectBackground: + case ColorID::TextSelectBackground: + case ColorID::IMESelectedRawTextBackground: + case ColorID::IMESelectedConvertedTextBackground: + case ColorID::MozDragtargetzone: + case ColorID::MozHtmlCellhighlight: + case ColorID::Highlight: // preference selected item, + aColor = mTextSelectedBackground; + break; + case ColorID::WidgetSelectForeground: + case ColorID::TextSelectForeground: + case ColorID::IMESelectedRawTextForeground: + case ColorID::IMESelectedConvertedTextForeground: + case ColorID::Highlighttext: + case ColorID::MozHtmlCellhighlighttext: + aColor = mTextSelectedText; + break; + case ColorID::MozCellhighlight: + aColor = mMozCellHighlightBackground; + break; + case ColorID::MozCellhighlighttext: + aColor = mMozCellHighlightText; + break; + case ColorID::Widget3DHighlight: + aColor = NS_RGB(0xa0, 0xa0, 0xa0); + break; + case ColorID::Widget3DShadow: + aColor = NS_RGB(0x40, 0x40, 0x40); + break; + case ColorID::IMERawInputBackground: + case ColorID::IMEConvertedTextBackground: + aColor = NS_TRANSPARENT; + break; + case ColorID::IMERawInputForeground: + case ColorID::IMEConvertedTextForeground: + aColor = NS_SAME_AS_FOREGROUND_COLOR; + break; + case ColorID::IMERawInputUnderline: + case ColorID::IMEConvertedTextUnderline: + aColor = NS_SAME_AS_FOREGROUND_COLOR; + break; + case ColorID::IMESelectedRawTextUnderline: + case ColorID::IMESelectedConvertedTextUnderline: + aColor = NS_TRANSPARENT; + break; + case ColorID::SpellCheckerUnderline: + aColor = NS_RGB(0xff, 0, 0); + break; + case ColorID::ThemedScrollbar: + aColor = mThemedScrollbar; + break; + case ColorID::ThemedScrollbarInactive: + aColor = mThemedScrollbarInactive; + break; + case ColorID::ThemedScrollbarThumb: + aColor = mThemedScrollbarThumb; + break; + case ColorID::ThemedScrollbarThumbHover: + aColor = mThemedScrollbarThumbHover; + break; + case ColorID::ThemedScrollbarThumbActive: + aColor = mThemedScrollbarThumbActive; + break; + case ColorID::ThemedScrollbarThumbInactive: + aColor = mThemedScrollbarThumbInactive; + break; + + // css2 http://www.w3.org/TR/REC-CSS2/ui.html#system-colors + case ColorID::Activeborder: + // active window border + aColor = mMozWindowActiveBorder; + break; + case ColorID::Inactiveborder: + // inactive window border + aColor = mMozWindowInactiveBorder; + break; + case ColorID::Graytext: // disabled text in windows, menus, etc. + case ColorID::Inactivecaptiontext: // text in inactive window caption + aColor = mMenuTextInactive; + break; + case ColorID::Inactivecaption: + // inactive window caption + aColor = mMozWindowInactiveCaption; + break; + case ColorID::Infobackground: + // tooltip background color + aColor = mInfoBackground; + break; + case ColorID::Infotext: + // tooltip text color + aColor = mInfoText; + break; + case ColorID::Menu: + // menu background + aColor = mMenuBackground; + break; + case ColorID::Menutext: + // menu text + aColor = mMenuText; + break; + case ColorID::Scrollbar: + // scrollbar gray area + aColor = mMozScrollbar; + break; + + case ColorID::Threedface: + case ColorID::Buttonface: + // 3-D face color + aColor = mMozWindowBackground; + break; + + case ColorID::Buttontext: + // text on push buttons + aColor = mButtonText; + break; + + case ColorID::Buttonhighlight: + // 3-D highlighted edge color + case ColorID::Threedhighlight: + // 3-D highlighted outer edge color + aColor = mFrameOuterLightBorder; + break; + + case ColorID::Buttonshadow: + // 3-D shadow edge color + case ColorID::Threedshadow: + // 3-D shadow inner edge color + aColor = mFrameInnerDarkBorder; + break; + + case ColorID::Threedlightshadow: + aColor = NS_RGB(0xE0, 0xE0, 0xE0); + break; + case ColorID::Threeddarkshadow: + aColor = NS_RGB(0xDC, 0xDC, 0xDC); + break; + + case ColorID::MozEventreerow: + case ColorID::Field: + aColor = mFieldBackground; + break; + case ColorID::Fieldtext: + aColor = mFieldText; + break; + case ColorID::MozButtondefault: + // default button border color + aColor = mButtonDefault; + break; + case ColorID::MozButtonhoverface: + aColor = mButtonHoverFace; + break; + case ColorID::MozButtonhovertext: + aColor = mButtonHoverText; + break; + case ColorID::MozGtkButtonactivetext: + aColor = mButtonActiveText; + break; + case ColorID::MozMenuhover: + aColor = mMenuHover; + break; + case ColorID::MozMenuhovertext: + aColor = mMenuHoverText; + break; + case ColorID::MozOddtreerow: + aColor = mOddCellBackground; + break; + case ColorID::MozNativehyperlinktext: + aColor = mNativeHyperLinkText; + break; + case ColorID::MozComboboxtext: + aColor = mComboBoxText; + break; + case ColorID::MozMenubartext: + aColor = mMenuBarText; + break; + case ColorID::MozMenubarhovertext: + aColor = mMenuBarHoverText; + break; + case ColorID::MozGtkInfoBarText: + aColor = mInfoBarText; + break; + case ColorID::MozColheadertext: + aColor = mMozColHeaderText; + break; + case ColorID::MozColheaderhovertext: + aColor = mMozColHeaderHoverText; + break; + default: + /* default color is BLACK */ + aColor = 0; + res = NS_ERROR_FAILURE; + break; + } + + return res; +} + +static int32_t CheckWidgetStyle(GtkWidget* aWidget, const char* aStyle, + int32_t aResult) { + gboolean value = FALSE; + gtk_widget_style_get(aWidget, aStyle, &value, nullptr); + return value ? aResult : 0; +} + +static int32_t ConvertGTKStepperStyleToMozillaScrollArrowStyle( + GtkWidget* aWidget) { + if (!aWidget) return mozilla::LookAndFeel::eScrollArrowStyle_Single; + + return CheckWidgetStyle(aWidget, "has-backward-stepper", + mozilla::LookAndFeel::eScrollArrow_StartBackward) | + CheckWidgetStyle(aWidget, "has-forward-stepper", + mozilla::LookAndFeel::eScrollArrow_EndForward) | + CheckWidgetStyle(aWidget, "has-secondary-backward-stepper", + mozilla::LookAndFeel::eScrollArrow_EndBackward) | + CheckWidgetStyle(aWidget, "has-secondary-forward-stepper", + mozilla::LookAndFeel::eScrollArrow_StartForward); +} + +nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) { + nsresult res = NS_OK; + + // We use delayed initialization by EnsureInit() here + // to make sure mozilla::Preferences is available (Bug 115807). + // IntID::UseAccessibilityTheme is requested before user preferences + // are read, and so EnsureInit(), which depends on preference values, + // is deliberately delayed until required. + switch (aID) { + case IntID::ScrollButtonLeftMouseButtonAction: + aResult = 0; + break; + case IntID::ScrollButtonMiddleMouseButtonAction: + aResult = 1; + break; + case IntID::ScrollButtonRightMouseButtonAction: + aResult = 2; + break; + case IntID::CaretBlinkTime: + EnsureInit(); + aResult = mCaretBlinkTime; + break; + case IntID::CaretWidth: + aResult = 1; + break; + case IntID::ShowCaretDuringSelection: + aResult = 0; + break; + case IntID::SelectTextfieldsOnKeyFocus: { + GtkSettings* settings; + gboolean select_on_focus; + + settings = gtk_settings_get_default(); + g_object_get(settings, "gtk-entry-select-on-focus", &select_on_focus, + nullptr); + + if (select_on_focus) + aResult = 1; + else + aResult = 0; + + } break; + case IntID::ScrollToClick: { + GtkSettings* settings; + gboolean warps_slider = FALSE; + + settings = gtk_settings_get_default(); + if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings), + "gtk-primary-button-warps-slider")) { + g_object_get(settings, "gtk-primary-button-warps-slider", &warps_slider, + nullptr); + } + + if (warps_slider) + aResult = 1; + else + aResult = 0; + } break; + case IntID::SubmenuDelay: { + GtkSettings* settings; + gint delay; + + settings = gtk_settings_get_default(); + g_object_get(settings, "gtk-menu-popup-delay", &delay, nullptr); + aResult = (int32_t)delay; + break; + } + case IntID::TooltipDelay: { + aResult = 500; + break; + } + case IntID::MenusCanOverlapOSBar: + // we want XUL popups to be able to overlap the task bar. + aResult = 1; + break; + case IntID::SkipNavigatingDisabledMenuItem: + aResult = 1; + break; + case IntID::DragThresholdX: + case IntID::DragThresholdY: { + gint threshold = 0; + g_object_get(gtk_settings_get_default(), "gtk-dnd-drag-threshold", + &threshold, nullptr); + + aResult = threshold; + } break; + case IntID::ScrollArrowStyle: { + GtkWidget* scrollbar = GetWidget(MOZ_GTK_SCROLLBAR_HORIZONTAL); + aResult = ConvertGTKStepperStyleToMozillaScrollArrowStyle(scrollbar); + break; + } + case IntID::ScrollSliderStyle: + aResult = eScrollThumbStyle_Proportional; + break; + case IntID::TreeOpenDelay: + aResult = 1000; + break; + case IntID::TreeCloseDelay: + aResult = 1000; + break; + case IntID::TreeLazyScrollDelay: + aResult = 150; + break; + case IntID::TreeScrollDelay: + aResult = 100; + break; + case IntID::TreeScrollLinesMax: + aResult = 3; + break; + case IntID::DWMCompositor: + case IntID::WindowsClassic: + case IntID::WindowsDefaultTheme: + case IntID::WindowsThemeIdentifier: + case IntID::OperatingSystemVersionIdentifier: + aResult = 0; + res = NS_ERROR_NOT_IMPLEMENTED; + break; + case IntID::TouchEnabled: + aResult = mozilla::widget::WidgetUtils::IsTouchDeviceSupportPresent(); + break; + case IntID::MacGraphiteTheme: + aResult = 0; + res = NS_ERROR_NOT_IMPLEMENTED; + break; + case IntID::AlertNotificationOrigin: + aResult = NS_ALERT_TOP; + break; + case IntID::IMERawInputUnderlineStyle: + case IntID::IMEConvertedTextUnderlineStyle: + aResult = NS_STYLE_TEXT_DECORATION_STYLE_SOLID; + break; + case IntID::IMESelectedRawTextUnderlineStyle: + case IntID::IMESelectedConvertedTextUnderline: + aResult = NS_STYLE_TEXT_DECORATION_STYLE_NONE; + break; + case IntID::SpellCheckerUnderlineStyle: + aResult = NS_STYLE_TEXT_DECORATION_STYLE_WAVY; + break; + case IntID::MenuBarDrag: + EnsureInit(); + aResult = mMenuSupportsDrag; + break; + case IntID::ScrollbarButtonAutoRepeatBehavior: + aResult = 1; + break; + case IntID::SwipeAnimationEnabled: + aResult = 0; + break; + case IntID::ContextMenuOffsetVertical: + case IntID::ContextMenuOffsetHorizontal: + aResult = 2; + break; + case IntID::GTKCSDAvailable: + EnsureInit(); + aResult = mCSDAvailable; + break; + case IntID::GTKCSDHideTitlebarByDefault: + EnsureInit(); + aResult = mCSDHideTitlebarByDefault; + break; + case IntID::GTKCSDMaximizeButton: + EnsureInit(); + aResult = mCSDMaximizeButton; + break; + case IntID::GTKCSDMinimizeButton: + EnsureInit(); + aResult = mCSDMinimizeButton; + break; + case IntID::GTKCSDCloseButton: + EnsureInit(); + aResult = mCSDCloseButton; + break; + case IntID::GTKCSDTransparentBackground: { + // Enable transparent titlebar corners for titlebar mode. + GdkScreen* screen = gdk_screen_get_default(); + aResult = gdk_screen_is_composited(screen) + ? (nsWindow::GetSystemCSDSupportLevel() != + nsWindow::CSD_SUPPORT_NONE) + : false; + break; + } + case IntID::GTKCSDReversedPlacement: + EnsureInit(); + aResult = mCSDReversedPlacement; + break; + case IntID::PrefersReducedMotion: { + aResult = mPrefersReducedMotion; + break; + } + case IntID::SystemUsesDarkTheme: { + EnsureInit(); + aResult = mSystemUsesDarkTheme; + break; + } + case IntID::GTKCSDMaximizeButtonPosition: + aResult = mCSDMaximizeButtonPosition; + break; + case IntID::GTKCSDMinimizeButtonPosition: + aResult = mCSDMinimizeButtonPosition; + break; + case IntID::GTKCSDCloseButtonPosition: + aResult = mCSDCloseButtonPosition; + break; + case IntID::UseAccessibilityTheme: { + EnsureInit(); + aResult = mHighContrast; + break; + } + default: + aResult = 0; + res = NS_ERROR_FAILURE; + } + + return res; +} + +nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) { + nsresult rv = NS_OK; + switch (aID) { + case FloatID::IMEUnderlineRelativeSize: + aResult = 1.0f; + break; + case FloatID::SpellCheckerUnderlineRelativeSize: + aResult = 1.0f; + break; + case FloatID::CaretAspectRatio: + EnsureInit(); + aResult = mCaretRatio; + break; + default: + aResult = -1.0; + rv = NS_ERROR_FAILURE; + } + return rv; +} + +static void GetSystemFontInfo(GtkStyleContext* aStyle, nsString* aFontName, + gfxFontStyle* aFontStyle) { + aFontStyle->style = FontSlantStyle::Normal(); + + // As in + // https://git.gnome.org/browse/gtk+/tree/gtk/gtkwidget.c?h=3.22.19#n10333 + PangoFontDescription* desc; + gtk_style_context_get(aStyle, gtk_style_context_get_state(aStyle), "font", + &desc, nullptr); + + aFontStyle->systemFont = true; + + constexpr auto quote = u"\""_ns; + NS_ConvertUTF8toUTF16 family(pango_font_description_get_family(desc)); + *aFontName = quote + family + quote; + + aFontStyle->weight = FontWeight(pango_font_description_get_weight(desc)); + + // FIXME: Set aFontStyle->stretch correctly! + aFontStyle->stretch = FontStretch::Normal(); + + float size = float(pango_font_description_get_size(desc)) / PANGO_SCALE; + + // |size| is now either pixels or pango-points (not Mozilla-points!) + + if (!pango_font_description_get_size_is_absolute(desc)) { + // |size| is in pango-points, so convert to pixels. + size *= float(gfxPlatformGtk::GetFontScaleDPI()) / POINTS_PER_INCH_FLOAT; + } + + // |size| is now pixels but not scaled for the hidpi displays, + aFontStyle->size = size; + + pango_font_description_free(desc); +} + +bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName, + gfxFontStyle& aFontStyle) { + switch (aID) { + case FontID::Menu: // css2 + case FontID::PullDownMenu: // css3 + aFontName = mMenuFontName; + aFontStyle = mMenuFontStyle; + break; + + case FontID::Field: // css3 + case FontID::List: // css3 + aFontName = mFieldFontName; + aFontStyle = mFieldFontStyle; + break; + + case FontID::Button: // css3 + aFontName = mButtonFontName; + aFontStyle = mButtonFontStyle; + break; + + case FontID::Caption: // css2 + case FontID::Icon: // css2 + case FontID::MessageBox: // css2 + case FontID::SmallCaption: // css2 + case FontID::StatusBar: // css2 + case FontID::Window: // css3 + case FontID::Document: // css3 + case FontID::Workspace: // css3 + case FontID::Desktop: // css3 + case FontID::Info: // css3 + case FontID::Dialog: // css3 + case FontID::Tooltips: // moz + case FontID::Widget: // moz + default: + aFontName = mDefaultFontName; + aFontStyle = mDefaultFontStyle; + break; + } + + // Scale the font for the current monitor + double scaleFactor = StaticPrefs::layout_css_devPixelsPerPx(); + if (scaleFactor > 0) { + aFontStyle.size *= + widget::ScreenHelperGTK::GetGTKMonitorScaleFactor() / scaleFactor; + } else { + // Convert gdk pixels to CSS pixels. + aFontStyle.size /= gfxPlatformGtk::GetFontScaleFactor(); + } + + return true; +} + +// Check color contrast according to +// https://www.w3.org/TR/AERT/#color-contrast +static bool HasGoodContrastVisibility(GdkRGBA& aColor1, GdkRGBA& aColor2) { + int32_t luminosityDifference = NS_LUMINOSITY_DIFFERENCE( + GDK_RGBA_TO_NS_RGBA(aColor1), GDK_RGBA_TO_NS_RGBA(aColor2)); + if (luminosityDifference < NS_SUFFICIENT_LUMINOSITY_DIFFERENCE) { + return false; + } + + double colorDifference = std::abs(aColor1.red - aColor2.red) + + std::abs(aColor1.green - aColor2.green) + + std::abs(aColor1.blue - aColor2.blue); + return (colorDifference * 255.0 > 500.0); +} + +// Check if the foreground/background colors match with default white/black +// html page colors. +static bool IsGtkThemeCompatibleWithHTMLColors() { + GdkRGBA white = {1.0, 1.0, 1.0}; + GdkRGBA black = {0.0, 0.0, 0.0}; + + GtkStyleContext* style = GetStyleContext(MOZ_GTK_WINDOW); + + GdkRGBA textColor; + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &textColor); + + // Theme text color and default white html page background + if (!HasGoodContrastVisibility(textColor, white)) { + return false; + } + + GdkRGBA backgroundColor; + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, + &backgroundColor); + + // Theme background color and default white html page background + if (HasGoodContrastVisibility(backgroundColor, white)) { + return false; + } + + // Theme background color and default black text color + return HasGoodContrastVisibility(backgroundColor, black); +} + +static nsCString GetGtkTheme() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + nsCString ret; + GtkSettings* settings = gtk_settings_get_default(); + char* themeName = nullptr; + g_object_get(settings, "gtk-theme-name", &themeName, nullptr); + if (themeName) { + ret.Assign(themeName); + g_free(themeName); + } + return ret; +} + +static bool GetPreferDarkTheme() { + GtkSettings* settings = gtk_settings_get_default(); + gboolean preferDarkTheme = FALSE; + g_object_get(settings, "gtk-application-prefer-dark-theme", &preferDarkTheme, + nullptr); + return preferDarkTheme == TRUE; +} + +void nsLookAndFeel::ConfigureTheme(const LookAndFeelTheme& aTheme) { + MOZ_ASSERT(XRE_IsContentProcess()); + GtkSettings* settings = gtk_settings_get_default(); + g_object_set(settings, "gtk-theme-name", aTheme.themeName().get(), + "gtk-application-prefer-dark-theme", + aTheme.preferDarkTheme() ? TRUE : FALSE, nullptr); +} + +void nsLookAndFeel::WithThemeConfiguredForContent( + const std::function<void(const LookAndFeelTheme& aTheme)>& aFn) { + nsWindow::WithSettingsChangesIgnored([&]() { + // Available on Gtk 3.20+. + static auto sGtkSettingsResetProperty = + (void (*)(GtkSettings*, const gchar*))dlsym( + RTLD_DEFAULT, "gtk_settings_reset_property"); + + nsCString themeName; + bool preferDarkTheme = false; + + if (!sGtkSettingsResetProperty) { + // When gtk_settings_reset_property is not available, we instead + // record the current theme name and variant and explicitly restore + // them afterwards. This means we won't respond to any subsequent + // theme settings changes, which is unfortunate. (It's possible we + // could listen to xsettings changes and update the GtkSettings object + // ourselves in response, if we wanted to fix this.) + themeName = GetGtkTheme(); + preferDarkTheme = GetPreferDarkTheme(); + } + + bool changed = ConfigureContentGtkTheme(); + if (changed) { + RefreshImpl(); + } + + LookAndFeelTheme theme; + theme.themeName() = GetGtkTheme(); + theme.preferDarkTheme() = GetPreferDarkTheme(); + + aFn(theme); + + if (changed) { + GtkSettings* settings = gtk_settings_get_default(); + if (sGtkSettingsResetProperty) { + sGtkSettingsResetProperty(settings, "gtk-theme-name"); + sGtkSettingsResetProperty(settings, + "gtk-application-prefer-dark-theme"); + } else { + g_object_set(settings, "gtk-theme-name", themeName.get(), + "gtk-application-prefer-dark-theme", + preferDarkTheme ? TRUE : FALSE, nullptr); + } + RefreshImpl(); + } + }); +} + +bool nsLookAndFeel::ConfigureContentGtkTheme() { + bool changed = false; + + GtkSettings* settings = gtk_settings_get_default(); + + nsAutoCString themeOverride; + mozilla::Preferences::GetCString("widget.content.gtk-theme-override", + themeOverride); + if (!themeOverride.IsEmpty()) { + g_object_set(settings, "gtk-theme-name", themeOverride.get(), nullptr); + changed = true; + LOG(("ConfigureContentGtkTheme(%s)\n", themeOverride.get())); + } else { + LOG(("ConfigureContentGtkTheme(%s)\n", GetGtkTheme().get())); + } + + // Dark theme is active but user explicitly enables it, or we're on + // high-contrast (in which case we prevent content to mess up with the colors + // of the page), so we're done now. + if (!themeOverride.IsEmpty() || mHighContrast || + StaticPrefs::widget_content_allow_gtk_dark_theme()) { + return changed; + } + + // Try to select the light variant of the current theme first... + if (GetPreferDarkTheme()) { + LOG((" disabling gtk-application-prefer-dark-theme\n")); + g_object_set(settings, "gtk-application-prefer-dark-theme", FALSE, nullptr); + changed = true; + } + + // ...and use a default Gtk theme as a fallback. + if (!IsGtkThemeCompatibleWithHTMLColors()) { + LOG((" Non-compatible dark theme, default to Adwaita\n")); + g_object_set(settings, "gtk-theme-name", "Adwaita", nullptr); + changed = true; + } + + return changed; +} + +void nsLookAndFeel::EnsureInit() { + if (mInitialized) { + return; + } + + // Gtk manages a screen's CSS in the settings object so we + // ask Gtk to create it explicitly. Otherwise we may end up + // with wrong color theme, see Bug 972382 + GtkSettings* settings = gtk_settings_get_default(); + if (MOZ_UNLIKELY(!settings)) { + NS_WARNING("EnsureInit: No settings"); + return; + } + + mInitialized = true; + + // gtk does non threadsafe refcounting + MOZ_ASSERT(NS_IsMainThread()); + + GtkStyleContext* style; + GdkRGBA color; + + if (XRE_IsContentProcess()) { + LOG(("nsLookAndFeel::EnsureInit() [%p] Content process\n", (void*)this)); + // Dark themes interacts poorly with widget styling (see bug 1216658). + // We disable dark themes by default for web content + // but allow user to overide it by prefs. + ConfigureContentGtkTheme(); + } else { + // It seems GTK doesn't have an API to query if the current theme is + // "light" or "dark", so we synthesize it from the CSS2 Window/WindowText + // colors instead, by comparing their luminosity. + GdkRGBA bg, fg; + style = GetStyleContext(MOZ_GTK_WINDOW); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &bg); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &fg); + LOG(("nsLookAndFeel::EnsureInit() [%p] Chrome process\n", (void*)this)); + // Update mSystemUsesDarkTheme only in the parent process since in the child + // processes we forcibly set gtk-theme-name so that we can't get correct + // results. Instead mSystemUsesDarkTheme in the child processes is updated + // via our caching machinery. + mSystemUsesDarkTheme = + (RelativeLuminanceUtils::Compute(GDK_RGBA_TO_NS_RGBA(bg)) < + RelativeLuminanceUtils::Compute(GDK_RGBA_TO_NS_RGBA(fg))); + + mHighContrast = StaticPrefs::widget_content_gtk_high_contrast_enabled() && + GetGtkTheme().Find("HighContrast"_ns) >= 0; + + gboolean enableAnimations = false; + g_object_get(settings, "gtk-enable-animations", &enableAnimations, nullptr); + mPrefersReducedMotion = !enableAnimations; + + // Colors that we pass to content processes through the LookAndFeelCache. + if (ShouldHonorThemeScrollbarColors()) { + // Some themes style the <trough>, while others style the <scrollbar> + // itself, so we look at both and compose the colors. + style = GetStyleContext(MOZ_GTK_SCROLLBAR_VERTICAL); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, + &color); + mThemedScrollbar = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_BACKDROP, + &color); + mThemedScrollbarInactive = GDK_RGBA_TO_NS_RGBA(color); + + style = GetStyleContext(MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, + &color); + mThemedScrollbar = + NS_ComposeColors(mThemedScrollbar, GDK_RGBA_TO_NS_RGBA(color)); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_BACKDROP, + &color); + mThemedScrollbarInactive = NS_ComposeColors(mThemedScrollbarInactive, + GDK_RGBA_TO_NS_RGBA(color)); + + mMozScrollbar = mThemedScrollbar; + + style = GetStyleContext(MOZ_GTK_SCROLLBAR_THUMB_VERTICAL); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, + &color); + mThemedScrollbarThumb = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_PRELIGHT, + &color); + mThemedScrollbarThumbHover = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_background_color( + style, GtkStateFlags(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE), + &color); + mThemedScrollbarThumbActive = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_BACKDROP, + &color); + mThemedScrollbarThumbInactive = GDK_RGBA_TO_NS_RGBA(color); + } else { + mMozScrollbar = mThemedScrollbar = widget::sScrollbarColor.ToABGR(); + mThemedScrollbarInactive = widget::sScrollbarColor.ToABGR(); + mThemedScrollbarThumb = widget::sScrollbarThumbColor.ToABGR(); + mThemedScrollbarThumbHover = widget::sScrollbarThumbColorHover.ToABGR(); + mThemedScrollbarThumbActive = widget::sScrollbarThumbColorActive.ToABGR(); + mThemedScrollbarThumbInactive = widget::sScrollbarThumbColor.ToABGR(); + } + } + + // The label is not added to a parent widget, but shared for constructing + // different style contexts. The node hierarchy is constructed only on + // the label style context. + GtkWidget* labelWidget = gtk_label_new("M"); + g_object_ref_sink(labelWidget); + + // Window colors + style = GetStyleContext(MOZ_GTK_WINDOW); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMozWindowBackground = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMozWindowText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_border_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMozWindowActiveBorder = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_border_color(style, GTK_STATE_FLAG_INSENSITIVE, &color); + mMozWindowInactiveBorder = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_INSENSITIVE, + &color); + mMozWindowInactiveCaption = GDK_RGBA_TO_NS_RGBA(color); + + style = GetStyleContext(MOZ_GTK_WINDOW_CONTAINER); + { + GtkStyleContext* labelStyle = CreateStyleForWidget(labelWidget, style); + GetSystemFontInfo(labelStyle, &mDefaultFontName, &mDefaultFontStyle); + g_object_unref(labelStyle); + } + + // tooltip foreground and background + style = GetStyleContext(MOZ_GTK_TOOLTIP); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color); + mInfoBackground = GDK_RGBA_TO_NS_RGBA(color); + + style = GetStyleContext(MOZ_GTK_TOOLTIP_BOX_LABEL); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mInfoText = GDK_RGBA_TO_NS_RGBA(color); + + style = GetStyleContext(MOZ_GTK_MENUITEM); + { + GtkStyleContext* accelStyle = + CreateStyleForWidget(gtk_accel_label_new("M"), style); + + GetSystemFontInfo(accelStyle, &mMenuFontName, &mMenuFontStyle); + + gtk_style_context_get_color(accelStyle, GTK_STATE_FLAG_NORMAL, &color); + mMenuText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(accelStyle, GTK_STATE_FLAG_INSENSITIVE, &color); + mMenuTextInactive = GDK_RGBA_TO_NS_RGBA(color); + g_object_unref(accelStyle); + } + + style = GetStyleContext(MOZ_GTK_MENUPOPUP); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMenuBackground = GDK_RGBA_TO_NS_RGBA(color); + + style = GetStyleContext(MOZ_GTK_MENUITEM); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_PRELIGHT, + &color); + mMenuHover = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_PRELIGHT, &color); + mMenuHoverText = GDK_RGBA_TO_NS_RGBA(color); + + GtkWidget* parent = gtk_fixed_new(); + GtkWidget* window = gtk_window_new(GTK_WINDOW_POPUP); + GtkWidget* treeView = gtk_tree_view_new(); + GtkWidget* linkButton = gtk_link_button_new("http://example.com/"); + GtkWidget* menuBar = gtk_menu_bar_new(); + GtkWidget* menuBarItem = gtk_menu_item_new(); + GtkWidget* entry = gtk_entry_new(); + GtkWidget* textView = gtk_text_view_new(); + + gtk_container_add(GTK_CONTAINER(parent), treeView); + gtk_container_add(GTK_CONTAINER(parent), linkButton); + gtk_container_add(GTK_CONTAINER(parent), menuBar); + gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), menuBarItem); + gtk_container_add(GTK_CONTAINER(window), parent); + gtk_container_add(GTK_CONTAINER(parent), entry); + gtk_container_add(GTK_CONTAINER(parent), textView); + + // Text colors + GdkRGBA bgColor; + // If the text window background is translucent, then the background of + // the textview root node is visible. + style = GetStyleContext(MOZ_GTK_TEXT_VIEW); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, + &bgColor); + + style = GetStyleContext(MOZ_GTK_TEXT_VIEW_TEXT); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color); + ApplyColorOver(color, &bgColor); + mFieldBackground = GDK_RGBA_TO_NS_RGBA(bgColor); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mFieldText = GDK_RGBA_TO_NS_RGBA(color); + + // Selected text and background + { + GtkStyleContext* selectionStyle = + GetStyleContext(MOZ_GTK_TEXT_VIEW_TEXT_SELECTION); + auto GrabSelectionColors = [&](GtkStyleContext* style) { + gtk_style_context_get_background_color( + style, + static_cast<GtkStateFlags>(GTK_STATE_FLAG_FOCUSED | + GTK_STATE_FLAG_SELECTED), + &color); + mTextSelectedBackground = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color( + style, + static_cast<GtkStateFlags>(GTK_STATE_FLAG_FOCUSED | + GTK_STATE_FLAG_SELECTED), + &color); + mTextSelectedText = GDK_RGBA_TO_NS_RGBA(color); + }; + GrabSelectionColors(selectionStyle); + if (mTextSelectedBackground == mTextSelectedText) { + // Some old distros/themes don't properly use the .selection style, so + // fall back to the regular text view style. + GrabSelectionColors(style); + } + } + + // Button text color + style = GetStyleContext(MOZ_GTK_BUTTON); + { + GtkStyleContext* labelStyle = CreateStyleForWidget(labelWidget, style); + GetSystemFontInfo(labelStyle, &mButtonFontName, &mButtonFontStyle); + g_object_unref(labelStyle); + } + + gtk_style_context_get_border_color(style, GTK_STATE_FLAG_NORMAL, &color); + mButtonDefault = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mButtonText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_PRELIGHT, &color); + mButtonHoverText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_ACTIVE, &color); + mButtonActiveText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_PRELIGHT, + &color); + mButtonHoverFace = GDK_RGBA_TO_NS_RGBA(color); + + // Combobox text color + style = GetStyleContext(MOZ_GTK_COMBOBOX_ENTRY_TEXTAREA); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mComboBoxText = GDK_RGBA_TO_NS_RGBA(color); + + // Menubar text and hover text colors + style = GetStyleContext(MOZ_GTK_MENUBARITEM); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMenuBarText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_PRELIGHT, &color); + mMenuBarHoverText = GDK_RGBA_TO_NS_RGBA(color); + + // GTK's guide to fancy odd row background colors: + // 1) Check if a theme explicitly defines an odd row color + // 2) If not, check if it defines an even row color, and darken it + // slightly by a hardcoded value (gtkstyle.c) + // 3) If neither are defined, take the base background color and + // darken that by a hardcoded value + style = GetStyleContext(MOZ_GTK_TREEVIEW); + + // Get odd row background color + gtk_style_context_save(style); + gtk_style_context_add_region(style, GTK_STYLE_REGION_ROW, GTK_REGION_ODD); + gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color); + mOddCellBackground = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_restore(style); + + // Column header colors + style = GetStyleContext(MOZ_GTK_TREE_HEADER_CELL); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMozColHeaderText = GDK_RGBA_TO_NS_RGBA(color); + gtk_style_context_get_color(style, GTK_STATE_FLAG_PRELIGHT, &color); + mMozColHeaderHoverText = GDK_RGBA_TO_NS_RGBA(color); + + // Compute cell highlight colors + InitCellHighlightColors(); + + // GtkFrame has a "border" subnode on which Adwaita draws the border. + // Some themes do not draw on this node but draw a border on the widget + // root node, so check the root node if no border is found on the border + // node. + style = GetStyleContext(MOZ_GTK_FRAME_BORDER); + bool themeUsesColors = + GetBorderColors(style, &mFrameOuterLightBorder, &mFrameInnerDarkBorder); + if (!themeUsesColors) { + style = GetStyleContext(MOZ_GTK_FRAME); + GetBorderColors(style, &mFrameOuterLightBorder, &mFrameInnerDarkBorder); + } + + // GtkInfoBar + // TODO - Use WidgetCache for it? + GtkWidget* infoBar = gtk_info_bar_new(); + GtkWidget* infoBarContent = + gtk_info_bar_get_content_area(GTK_INFO_BAR(infoBar)); + GtkWidget* infoBarLabel = gtk_label_new(nullptr); + gtk_container_add(GTK_CONTAINER(parent), infoBar); + gtk_container_add(GTK_CONTAINER(infoBarContent), infoBarLabel); + style = gtk_widget_get_style_context(infoBarLabel); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_INFO); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mInfoBarText = GDK_RGBA_TO_NS_RGBA(color); + // Some themes have a unified menu bar, and support window dragging on it + gboolean supports_menubar_drag = FALSE; + GParamSpec* param_spec = gtk_widget_class_find_style_property( + GTK_WIDGET_GET_CLASS(menuBar), "window-dragging"); + if (param_spec) { + if (g_type_is_a(G_PARAM_SPEC_VALUE_TYPE(param_spec), G_TYPE_BOOLEAN)) { + gtk_widget_style_get(menuBar, "window-dragging", &supports_menubar_drag, + nullptr); + } + } + mMenuSupportsDrag = supports_menubar_drag; + + // TODO: It returns wrong color for themes which + // sets link color for GtkLabel only as we query + // GtkLinkButton style here. + style = gtk_widget_get_style_context(linkButton); + gtk_style_context_get_color(style, GTK_STATE_FLAG_LINK, &color); + mNativeHyperLinkText = GDK_RGBA_TO_NS_RGBA(color); + + // invisible character styles + guint value; + g_object_get(entry, "invisible-char", &value, nullptr); + mInvisibleCharacter = char16_t(value); + + // caret styles + gtk_widget_style_get(entry, "cursor-aspect-ratio", &mCaretRatio, nullptr); + + gint blink_time; + gboolean blink; + g_object_get(settings, "gtk-cursor-blink-time", &blink_time, + "gtk-cursor-blink", &blink, nullptr); + mCaretBlinkTime = blink ? (int32_t)blink_time : 0; + + GetSystemFontInfo(gtk_widget_get_style_context(entry), &mFieldFontName, + &mFieldFontStyle); + + gtk_widget_destroy(window); + g_object_unref(labelWidget); + + mCSDAvailable = + nsWindow::GetSystemCSDSupportLevel() != nsWindow::CSD_SUPPORT_NONE; + mCSDHideTitlebarByDefault = nsWindow::HideTitlebarByDefault(); + + mCSDCloseButton = false; + mCSDMinimizeButton = false; + mCSDMaximizeButton = false; + mCSDCloseButtonPosition = 0; + mCSDMinimizeButtonPosition = 0; + mCSDMaximizeButtonPosition = 0; + + // We need to initialize whole CSD config explicitly because it's queried + // as -moz-gtk* media features. + ButtonLayout buttonLayout[TOOLBAR_BUTTONS]; + + size_t activeButtons = + GetGtkHeaderBarButtonLayout(Span(buttonLayout), &mCSDReversedPlacement); + for (size_t i = 0; i < activeButtons; i++) { + // We check if a button is represented on the right side of the tabbar. + // Then we assign it a value from 3 to 5, instead of 0 to 2 when it is on + // the left side. + const ButtonLayout& layout = buttonLayout[i]; + int32_t* pos = nullptr; + switch (layout.mType) { + case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE: + mCSDMinimizeButton = true; + pos = &mCSDMinimizeButtonPosition; + break; + case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE: + mCSDMaximizeButton = true; + pos = &mCSDMaximizeButtonPosition; + break; + case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE: + mCSDCloseButton = true; + pos = &mCSDCloseButtonPosition; + break; + default: + break; + } + + if (pos) { + *pos = i; + if (layout.mAtRight) { + *pos += TOOLBAR_BUTTONS; + } + } + } + + RecordTelemetry(); +} + +// virtual +char16_t nsLookAndFeel::GetPasswordCharacterImpl() { + EnsureInit(); + return mInvisibleCharacter; +} + +bool nsLookAndFeel::GetEchoPasswordImpl() { return false; } + +bool nsLookAndFeel::WidgetUsesImage(WidgetNodeType aNodeType) { + static constexpr GtkStateFlags sFlagsToCheck[]{ + GTK_STATE_FLAG_NORMAL, GTK_STATE_FLAG_PRELIGHT, + GtkStateFlags(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE), + GTK_STATE_FLAG_BACKDROP, GTK_STATE_FLAG_INSENSITIVE}; + + GtkStyleContext* style = GetStyleContext(aNodeType); + + GValue value = G_VALUE_INIT; + for (GtkStateFlags state : sFlagsToCheck) { + gtk_style_context_get_property(style, "background-image", state, &value); + bool hasPattern = G_VALUE_TYPE(&value) == CAIRO_GOBJECT_TYPE_PATTERN && + g_value_get_boxed(&value); + g_value_unset(&value); + if (hasPattern) { + return true; + } + } + return false; +} + +void nsLookAndFeel::RecordLookAndFeelSpecificTelemetry() { + // Gtk version we're on. + nsString version; + version.AppendPrintf("%d.%d", gtk_major_version, gtk_minor_version); + Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_GTK_VERSION, version); + + // Whether the current Gtk theme has scrollbar buttons. + bool hasScrollbarButtons = + GetInt(LookAndFeel::IntID::ScrollArrowStyle) != eScrollArrow_None; + mozilla::Telemetry::ScalarSet( + mozilla::Telemetry::ScalarID::WIDGET_GTK_THEME_HAS_SCROLLBAR_BUTTONS, + hasScrollbarButtons); + + // Whether the current Gtk theme uses something other than a solid color + // background for scrollbar parts. + bool scrollbarUsesImage = + WidgetUsesImage(MOZ_GTK_SCROLLBAR_VERTICAL) || + WidgetUsesImage(MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL) || + WidgetUsesImage(MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL) || + WidgetUsesImage(MOZ_GTK_SCROLLBAR_THUMB_VERTICAL); + mozilla::Telemetry::ScalarSet( + mozilla::Telemetry::ScalarID::WIDGET_GTK_THEME_SCROLLBAR_USES_IMAGES, + scrollbarUsesImage); +} + +bool nsLookAndFeel::ShouldHonorThemeScrollbarColors() { + // If the Gtk theme uses anything other than solid color backgrounds for Gtk + // scrollbar parts, this is a good indication that painting XUL scrollbar part + // elements using colors extracted from the theme won't provide good results. + return !WidgetUsesImage(MOZ_GTK_SCROLLBAR_VERTICAL) && + !WidgetUsesImage(MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL) && + !WidgetUsesImage(MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL) && + !WidgetUsesImage(MOZ_GTK_SCROLLBAR_THUMB_VERTICAL); +} diff --git a/widget/gtk/nsLookAndFeel.h b/widget/gtk/nsLookAndFeel.h new file mode 100644 index 0000000000..c8e4ec83b1 --- /dev/null +++ b/widget/gtk/nsLookAndFeel.h @@ -0,0 +1,131 @@ +/* -*- 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 __nsLookAndFeel +#define __nsLookAndFeel + +#include "X11UndefineNone.h" +#include "nsXPLookAndFeel.h" +#include "nsCOMPtr.h" +#include "gfxFont.h" + +enum WidgetNodeType : int; +struct _GtkStyle; + +class nsLookAndFeel final : public nsXPLookAndFeel { + public: + explicit nsLookAndFeel(const LookAndFeelCache* aCache); + virtual ~nsLookAndFeel(); + + void NativeInit() final; + void RefreshImpl() override; + nsresult NativeGetInt(IntID aID, int32_t& aResult) override; + nsresult NativeGetFloat(FloatID aID, float& aResult) override; + nsresult NativeGetColor(ColorID aID, nscolor& aResult) override; + bool NativeGetFont(FontID aID, nsString& aFontName, + gfxFontStyle& aFontStyle) override; + + char16_t GetPasswordCharacterImpl() override; + bool GetEchoPasswordImpl() override; + + LookAndFeelCache GetCacheImpl() override; + void SetCacheImpl(const LookAndFeelCache& aCache) override; + + void WithThemeConfiguredForContent( + const std::function<void(const LookAndFeelTheme& aTheme)>& aFn) override; + static void ConfigureTheme(const LookAndFeelTheme& aTheme); + + bool IsCSDAvailable() const { return mCSDAvailable; } + + static const nscolor kBlack = NS_RGB(0, 0, 0); + static const nscolor kWhite = NS_RGB(255, 255, 255); + + protected: + void DoSetCache(const LookAndFeelCache& aCache); + bool WidgetUsesImage(WidgetNodeType aNodeType); + void RecordLookAndFeelSpecificTelemetry() override; + bool ShouldHonorThemeScrollbarColors(); + + // Cached fonts + nsString mDefaultFontName; + nsString mButtonFontName; + nsString mFieldFontName; + nsString mMenuFontName; + gfxFontStyle mDefaultFontStyle; + gfxFontStyle mButtonFontStyle; + gfxFontStyle mFieldFontStyle; + gfxFontStyle mMenuFontStyle; + + // Cached colors + nscolor mInfoBackground = kWhite; + nscolor mInfoText = kBlack; + nscolor mMenuBackground = kWhite; + nscolor mMenuBarText = kBlack; + nscolor mMenuBarHoverText = kBlack; + nscolor mMenuText = kBlack; + nscolor mMenuTextInactive = kWhite; + nscolor mMenuHover = kWhite; + nscolor mMenuHoverText = kBlack; + nscolor mButtonDefault = kWhite; + nscolor mButtonText = kBlack; + nscolor mButtonHoverText = kBlack; + nscolor mButtonHoverFace = kWhite; + nscolor mButtonActiveText = kBlack; + nscolor mFrameOuterLightBorder = kBlack; + nscolor mFrameInnerDarkBorder = kBlack; + nscolor mOddCellBackground = kWhite; + nscolor mNativeHyperLinkText = kBlack; + nscolor mComboBoxText = kBlack; + nscolor mComboBoxBackground = kWhite; + nscolor mFieldText = kBlack; + nscolor mFieldBackground = kWhite; + nscolor mMozWindowText = kBlack; + nscolor mMozWindowBackground = kWhite; + nscolor mMozWindowActiveBorder = kBlack; + nscolor mMozWindowInactiveBorder = kBlack; + nscolor mMozWindowInactiveCaption = kWhite; + nscolor mMozCellHighlightBackground = kWhite; + nscolor mMozCellHighlightText = kBlack; + nscolor mTextSelectedText = kBlack; + nscolor mTextSelectedBackground = kWhite; + nscolor mMozScrollbar = kWhite; + nscolor mInfoBarText = kBlack; + nscolor mMozColHeaderText = kBlack; + nscolor mMozColHeaderHoverText = kBlack; + nscolor mThemedScrollbar = kWhite; + nscolor mThemedScrollbarInactive = kWhite; + nscolor mThemedScrollbarThumb = kBlack; + nscolor mThemedScrollbarThumbHover = kBlack; + nscolor mThemedScrollbarThumbActive = kBlack; + nscolor mThemedScrollbarThumbInactive = kBlack; + char16_t mInvisibleCharacter = 0; + float mCaretRatio = 0.0f; + int32_t mCaretBlinkTime = 0; + bool mMenuSupportsDrag = false; + bool mCSDAvailable = false; + bool mCSDHideTitlebarByDefault = false; + bool mCSDMaximizeButton = false; + bool mCSDMinimizeButton = false; + bool mCSDCloseButton = false; + bool mCSDReversedPlacement = false; + bool mSystemUsesDarkTheme = false; + bool mPrefersReducedMotion = false; + bool mHighContrast = false; + bool mInitialized = false; + int32_t mCSDMaximizeButtonPosition = 0; + int32_t mCSDMinimizeButtonPosition = 0; + int32_t mCSDCloseButtonPosition = 0; + + void EnsureInit(); + // Returns whether the current theme or theme variant was changed. + bool ConfigureContentGtkTheme(); + + private: + nsresult InitCellHighlightColors(); +}; + +#endif diff --git a/widget/gtk/nsNativeBasicThemeGTK.cpp b/widget/gtk/nsNativeBasicThemeGTK.cpp new file mode 100644 index 0000000000..cf32161c40 --- /dev/null +++ b/widget/gtk/nsNativeBasicThemeGTK.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNativeBasicThemeGTK.h" + +#include "nsLayoutUtils.h" + +using namespace mozilla; + +static constexpr CSSCoord kGtkMinimumScrollbarSize = 12; +static constexpr CSSCoord kGtkMinimumThinScrollbarSize = 6; +static constexpr CSSCoord kGtkMinimumScrollbarThumbSize = 40; + +already_AddRefed<nsITheme> do_GetBasicNativeThemeDoNotUseDirectly() { + static mozilla::StaticRefPtr<nsITheme> gInstance; + if (MOZ_UNLIKELY(!gInstance)) { + gInstance = new nsNativeBasicThemeGTK(); + ClearOnShutdown(&gInstance); + } + return do_AddRef(gInstance); +} + +nsITheme::Transparency nsNativeBasicThemeGTK::GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbarHorizontal: + // Make scrollbar tracks opaque on the window's scroll frame to prevent + // leaf layers from overlapping. See bug 1179780. + return IsRootScrollbar(aFrame) ? eOpaque : eTransparent; + default: + return nsNativeBasicTheme::GetWidgetTransparency(aFrame, aAppearance); + } +} + +NS_IMETHODIMP +nsNativeBasicThemeGTK::GetMinimumWidgetSize(nsPresContext* aPresContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntSize* aResult, + bool* aIsOverridable) { + DPIRatio dpiRatio = GetDPIRatio(aFrame); + + switch (aAppearance) { + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: + case StyleAppearance::ScrollbarthumbVertical: + case StyleAppearance::ScrollbarthumbHorizontal: + case StyleAppearance::ScrollbartrackHorizontal: + case StyleAppearance::ScrollbartrackVertical: + case StyleAppearance::Scrollcorner: { + ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame); + if (style->StyleUIReset()->mScrollbarWidth == StyleScrollbarWidth::Thin) { + aResult->SizeTo(kGtkMinimumThinScrollbarSize * dpiRatio, + kGtkMinimumThinScrollbarSize * dpiRatio); + } else { + aResult->SizeTo(kGtkMinimumScrollbarSize * dpiRatio, + kGtkMinimumScrollbarSize * dpiRatio); + } + break; + } + default: + return nsNativeBasicTheme::GetMinimumWidgetSize( + aPresContext, aFrame, aAppearance, aResult, aIsOverridable); + } + + switch (aAppearance) { + case StyleAppearance::ScrollbarthumbHorizontal: + aResult->width = kGtkMinimumScrollbarThumbSize * dpiRatio; + break; + case StyleAppearance::ScrollbarthumbVertical: + aResult->height = kGtkMinimumScrollbarThumbSize * dpiRatio; + break; + default: + break; + } + + *aIsOverridable = true; + return NS_OK; +} + +void nsNativeBasicThemeGTK::PaintScrollbarThumb( + DrawTarget* aDrawTarget, const LayoutDeviceRect& aRect, bool aHorizontal, + nsIFrame* aFrame, const ComputedStyle& aStyle, + const EventStates& aElementState, const EventStates& aDocumentState, + DPIRatio aDpiRatio) { + sRGBColor thumbColor = + ComputeScrollbarThumbColor(aFrame, aStyle, aElementState, aDocumentState); + LayoutDeviceRect thumbRect(aRect); + thumbRect.Deflate(floorf((aHorizontal ? aRect.height : aRect.width) / 4.0f)); + LayoutDeviceCoord radius = + (aHorizontal ? thumbRect.height : thumbRect.width) / 2.0f; + PaintRoundedRectWithRadius(aDrawTarget, thumbRect, thumbColor, sRGBColor(), 0, + radius / aDpiRatio, aDpiRatio); +} + +void nsNativeBasicThemeGTK::PaintScrollbar(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + bool aHorizontal, nsIFrame* aFrame, + const ComputedStyle& aStyle, + const EventStates& aDocumentState, + DPIRatio aDpiRatio, bool aIsRoot) { + auto [trackColor, borderColor] = + ComputeScrollbarColors(aFrame, aStyle, aDocumentState, aIsRoot); + Unused << borderColor; + aDrawTarget->FillRect(aRect.ToUnknownRect(), + gfx::ColorPattern(ToDeviceColor(trackColor))); +} + +void nsNativeBasicThemeGTK::PaintScrollCorner( + DrawTarget* aDrawTarget, const LayoutDeviceRect& aRect, nsIFrame* aFrame, + const ComputedStyle& aStyle, const EventStates& aDocumentState, + DPIRatio aDpiRatio, bool aIsRoot) { + auto [trackColor, borderColor] = + ComputeScrollbarColors(aFrame, aStyle, aDocumentState, aIsRoot); + Unused << borderColor; + aDrawTarget->FillRect(aRect.ToUnknownRect(), + gfx::ColorPattern(ToDeviceColor(trackColor))); +} diff --git a/widget/gtk/nsNativeBasicThemeGTK.h b/widget/gtk/nsNativeBasicThemeGTK.h new file mode 100644 index 0000000000..9cad0cbb08 --- /dev/null +++ b/widget/gtk/nsNativeBasicThemeGTK.h @@ -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/. */ + +#ifndef nsNativeBasicThemeGTK_h +#define nsNativeBasicThemeGTK_h + +#include "nsNativeBasicTheme.h" + +class nsNativeBasicThemeGTK : public nsNativeBasicTheme { + public: + nsNativeBasicThemeGTK() = default; + + NS_IMETHOD GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + mozilla::LayoutDeviceIntSize* aResult, + bool* aIsOverridable) override; + + nsITheme::Transparency GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) override; + void PaintScrollbarThumb(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, bool aHorizontal, + nsIFrame* aFrame, const ComputedStyle& aStyle, + const EventStates& aElementState, + const EventStates& aDocumentState, + DPIRatio aDpiRatio) override; + void PaintScrollbar(DrawTarget* aDrawTarget, const LayoutDeviceRect& aRect, + bool aHorizontal, nsIFrame* aFrame, + const ComputedStyle& aStyle, + const EventStates& aDocumentState, DPIRatio aDpiRatio, + bool aIsRoot) override; + void PaintScrollCorner(DrawTarget* aDrawTarget, const LayoutDeviceRect& aRect, + nsIFrame* aFrame, const ComputedStyle& aStyle, + const EventStates& aDocumentState, DPIRatio aDpiRatio, + bool aIsRoot) override; + bool ThemeSupportsScrollbarButtons() override { return false; } + + protected: + virtual ~nsNativeBasicThemeGTK() = default; +}; + +#endif diff --git a/widget/gtk/nsNativeThemeGTK.cpp b/widget/gtk/nsNativeThemeGTK.cpp new file mode 100644 index 0000000000..f392db8013 --- /dev/null +++ b/widget/gtk/nsNativeThemeGTK.cpp @@ -0,0 +1,2019 @@ +/* -*- 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 "nsNativeThemeGTK.h" +#include "HeadlessThemeGTK.h" +#include "nsStyleConsts.h" +#include "gtkdrawing.h" +#include "ScreenHelperGTK.h" + +#include "gfx2DGlue.h" +#include "nsIObserverService.h" +#include "nsIFrame.h" +#include "nsIContent.h" +#include "nsViewManager.h" +#include "nsNameSpaceManager.h" +#include "nsGfxCIID.h" +#include "nsTransform2D.h" +#include "nsMenuFrame.h" +#include "tree/nsTreeBodyFrame.h" +#include "prlink.h" +#include "nsGkAtoms.h" +#include "nsAttrValueInlines.h" + +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EventStates.h" +#include "mozilla/Services.h" + +#include <gdk/gdkprivate.h> +#include <gtk/gtk.h> + +#include "gfxContext.h" +#include "gfxPlatformGtk.h" +#include "gfxGdkNativeRenderer.h" +#include "mozilla/gfx/BorrowedContext.h" +#include "mozilla/gfx/HelpersCairo.h" +#include "mozilla/gfx/PathHelpers.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/layers/StackingContextHelper.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_widget.h" +#include "nsWindow.h" +#include "nsLayoutUtils.h" +#include "nsNativeBasicTheme.h" + +#ifdef MOZ_X11 +# ifdef CAIRO_HAS_XLIB_SURFACE +# include "cairo-xlib.h" +# endif +# ifdef CAIRO_HAS_XLIB_XRENDER_SURFACE +# include "cairo-xlib-xrender.h" +# endif +#endif + +#include <algorithm> +#include <dlfcn.h> + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; +using mozilla::dom::HTMLInputElement; + +NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeGTK, nsNativeTheme, nsITheme, + nsIObserver) + +static int gLastGdkError; + +// Return scale factor of the monitor where the window is located +// by the most part or layout.css.devPixelsPerPx pref if set to > 0. +static inline gint GetMonitorScaleFactor(nsIFrame* aFrame) { + // When the layout.css.devPixelsPerPx is set the scale can be < 1, + // the real monitor scale cannot go under 1. + double scale = StaticPrefs::layout_css_devPixelsPerPx(); + if (scale <= 0) { + nsIWidget* rootWidget = aFrame->PresContext()->GetRootWidget(); + if (rootWidget) { + // We need to use GetDefaultScale() despite it returns monitor scale + // factor multiplied by font scale factor because it is the only scale + // updated in nsPuppetWidget. + // Since we don't want to apply font scale factor for UI elements + // (because GTK does not do so) we need to remove that from returned + // value. The computed monitor scale factor needs to be rounded before + // casting to integer to avoid rounding errors which would lead to + // returning 0. + int monitorScale = int(round(rootWidget->GetDefaultScale().scale / + gfxPlatformGtk::GetFontScaleFactor())); + // Monitor scale can be negative if it has not been initialized in the + // puppet widget yet. We also make sure that we return positive value. + if (monitorScale < 1) { + return 1; + } + return monitorScale; + } + } + // Use monitor scaling factor where devPixelsPerPx is set + return ScreenHelperGTK::GetGTKMonitorScaleFactor(); +} + +nsNativeThemeGTK::nsNativeThemeGTK() { + if (moz_gtk_init() != MOZ_GTK_SUCCESS) { + memset(mDisabledWidgetTypes, 0xff, sizeof(mDisabledWidgetTypes)); + return; + } + + // We have to call moz_gtk_shutdown before the event loop stops running. + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + obsServ->AddObserver(this, "xpcom-shutdown", false); + + ThemeChanged(); +} + +nsNativeThemeGTK::~nsNativeThemeGTK() = default; + +NS_IMETHODIMP +nsNativeThemeGTK::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) { + moz_gtk_shutdown(); + } else { + MOZ_ASSERT_UNREACHABLE("unexpected topic"); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +void nsNativeThemeGTK::RefreshWidgetWindow(nsIFrame* aFrame) { + MOZ_ASSERT(aFrame); + MOZ_ASSERT(aFrame->PresShell()); + + nsViewManager* vm = aFrame->PresShell()->GetViewManager(); + if (!vm) { + return; + } + vm->InvalidateAllViews(); +} + +static bool IsFrameContentNodeInNamespace(nsIFrame* aFrame, + uint32_t aNamespace) { + nsIContent* content = aFrame ? aFrame->GetContent() : nullptr; + if (!content) return false; + return content->IsInNamespace(aNamespace); +} + +static bool IsWidgetTypeDisabled(const uint8_t* aDisabledVector, + StyleAppearance aAppearance) { + auto type = static_cast<size_t>(aAppearance); + MOZ_ASSERT(type < static_cast<size_t>(mozilla::StyleAppearance::Count)); + return (aDisabledVector[type >> 3] & (1 << (type & 7))) != 0; +} + +static void SetWidgetTypeDisabled(uint8_t* aDisabledVector, + StyleAppearance aAppearance) { + auto type = static_cast<size_t>(aAppearance); + MOZ_ASSERT(type < static_cast<size_t>(mozilla::StyleAppearance::Count)); + aDisabledVector[type >> 3] |= (1 << (type & 7)); +} + +static inline uint16_t GetWidgetStateKey(StyleAppearance aAppearance, + GtkWidgetState* aWidgetState) { + return (aWidgetState->active | aWidgetState->focused << 1 | + aWidgetState->inHover << 2 | aWidgetState->disabled << 3 | + aWidgetState->isDefault << 4 | + static_cast<uint16_t>(aAppearance) << 5); +} + +static bool IsWidgetStateSafe(uint8_t* aSafeVector, StyleAppearance aAppearance, + GtkWidgetState* aWidgetState) { + MOZ_ASSERT(static_cast<size_t>(aAppearance) < + static_cast<size_t>(mozilla::StyleAppearance::Count)); + uint16_t key = GetWidgetStateKey(aAppearance, aWidgetState); + return (aSafeVector[key >> 3] & (1 << (key & 7))) != 0; +} + +static void SetWidgetStateSafe(uint8_t* aSafeVector, + StyleAppearance aAppearance, + GtkWidgetState* aWidgetState) { + MOZ_ASSERT(static_cast<size_t>(aAppearance) < + static_cast<size_t>(mozilla::StyleAppearance::Count)); + uint16_t key = GetWidgetStateKey(aAppearance, aWidgetState); + aSafeVector[key >> 3] |= (1 << (key & 7)); +} + +/* static */ +GtkTextDirection nsNativeThemeGTK::GetTextDirection(nsIFrame* aFrame) { + // IsFrameRTL() treats vertical-rl modes as right-to-left (in addition to + // horizontal text with direction=RTL), rather than just considering the + // text direction. GtkTextDirection does not have distinct values for + // vertical writing modes, but considering the block flow direction is + // important for resizers and scrollbar elements, at least. + return IsFrameRTL(aFrame) ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR; +} + +// Returns positive for negative margins (otherwise 0). +gint nsNativeThemeGTK::GetTabMarginPixels(nsIFrame* aFrame) { + nscoord margin = IsBottomTab(aFrame) ? aFrame->GetUsedMargin().top + : aFrame->GetUsedMargin().bottom; + + return std::min<gint>( + MOZ_GTK_TAB_MARGIN_MASK, + std::max(0, aFrame->PresContext()->AppUnitsToDevPixels(-margin))); +} + +static bool ShouldScrollbarButtonBeDisabled(int32_t aCurpos, int32_t aMaxpos, + StyleAppearance aAppearance) { + return ( + (aCurpos == 0 && (aAppearance == StyleAppearance::ScrollbarbuttonUp || + aAppearance == StyleAppearance::ScrollbarbuttonLeft)) || + (aCurpos == aMaxpos && + (aAppearance == StyleAppearance::ScrollbarbuttonDown || + aAppearance == StyleAppearance::ScrollbarbuttonRight))); +} + +bool nsNativeThemeGTK::GetGtkWidgetAndState(StyleAppearance aAppearance, + nsIFrame* aFrame, + WidgetNodeType& aGtkWidgetType, + GtkWidgetState* aState, + gint* aWidgetFlags) { + if (aWidgetFlags) { + *aWidgetFlags = 0; + } + if (aState) { + memset(aState, 0, sizeof(GtkWidgetState)); + + // For XUL checkboxes and radio buttons, the state of the parent + // determines our state. + nsIFrame* stateFrame = aFrame; + if (aFrame && ((aWidgetFlags && (aAppearance == StyleAppearance::Checkbox || + aAppearance == StyleAppearance::Radio)) || + aAppearance == StyleAppearance::CheckboxLabel || + aAppearance == StyleAppearance::RadioLabel)) { + nsAtom* atom = nullptr; + if (IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) { + if (aAppearance == StyleAppearance::CheckboxLabel || + aAppearance == StyleAppearance::RadioLabel) { + // Adjust stateFrame so GetContentState finds the correct state. + stateFrame = aFrame = aFrame->GetParent()->GetParent(); + } else { + // GetContentState knows to look one frame up for radio/checkbox + // widgets, so don't adjust stateFrame here. + aFrame = aFrame->GetParent(); + } + if (aWidgetFlags) { + if (!atom) { + atom = (aAppearance == StyleAppearance::Checkbox || + aAppearance == StyleAppearance::CheckboxLabel) + ? nsGkAtoms::checked + : nsGkAtoms::selected; + } + *aWidgetFlags = CheckBooleanAttr(aFrame, atom); + } + } else { + if (aWidgetFlags) { + *aWidgetFlags = 0; + HTMLInputElement* inputElt = + HTMLInputElement::FromNode(aFrame->GetContent()); + if (inputElt && inputElt->Checked()) + *aWidgetFlags |= MOZ_GTK_WIDGET_CHECKED; + + if (GetIndeterminate(aFrame)) + *aWidgetFlags |= MOZ_GTK_WIDGET_INCONSISTENT; + } + } + } else if (aAppearance == StyleAppearance::ToolbarbuttonDropdown || + aAppearance == StyleAppearance::Treeheadersortarrow || + aAppearance == StyleAppearance::ButtonArrowPrevious || + aAppearance == StyleAppearance::ButtonArrowNext || + aAppearance == StyleAppearance::ButtonArrowUp || + aAppearance == StyleAppearance::ButtonArrowDown) { + // The state of an arrow comes from its parent. + stateFrame = aFrame = aFrame->GetParent(); + } + + EventStates eventState = GetContentState(stateFrame, aAppearance); + + aState->disabled = IsDisabled(aFrame, eventState) || IsReadOnly(aFrame); + aState->active = eventState.HasState(NS_EVENT_STATE_ACTIVE); + aState->focused = eventState.HasState(NS_EVENT_STATE_FOCUS); + aState->inHover = eventState.HasState(NS_EVENT_STATE_HOVER); + aState->isDefault = IsDefaultButton(aFrame); + aState->canDefault = FALSE; // XXX fix me + + if (aAppearance == StyleAppearance::FocusOutline) { + aState->disabled = FALSE; + aState->active = FALSE; + aState->inHover = FALSE; + aState->isDefault = FALSE; + aState->canDefault = FALSE; + + aState->focused = TRUE; + aState->depressed = TRUE; // see moz_gtk_entry_paint() + } else if (aAppearance == StyleAppearance::Button || + aAppearance == StyleAppearance::Toolbarbutton || + aAppearance == StyleAppearance::Dualbutton || + aAppearance == StyleAppearance::ToolbarbuttonDropdown || + aAppearance == StyleAppearance::Menulist || + aAppearance == StyleAppearance::MenulistButton || + aAppearance == StyleAppearance::MozMenulistArrowButton) { + aState->active &= aState->inHover; + } else if (aAppearance == StyleAppearance::Treetwisty || + aAppearance == StyleAppearance::Treetwistyopen) { + nsTreeBodyFrame* treeBodyFrame = do_QueryFrame(aFrame); + if (treeBodyFrame) { + const mozilla::AtomArray& atoms = + treeBodyFrame->GetPropertyArrayForCurrentDrawingItem(); + aState->selected = atoms.Contains((nsStaticAtom*)nsGkAtoms::selected); + aState->inHover = atoms.Contains((nsStaticAtom*)nsGkAtoms::hover); + } + } + + if (IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) { + // For these widget types, some element (either a child or parent) + // actually has element focus, so we check the focused attribute + // to see whether to draw in the focused state. + if (aAppearance == StyleAppearance::NumberInput || + aAppearance == StyleAppearance::Textfield || + aAppearance == StyleAppearance::Textarea || + aAppearance == StyleAppearance::SpinnerTextfield || + aAppearance == StyleAppearance::RadioContainer || + aAppearance == StyleAppearance::RadioLabel) { + aState->focused = IsFocused(aFrame); + } else if (aAppearance == StyleAppearance::Radio || + aAppearance == StyleAppearance::Checkbox) { + // In XUL, checkboxes and radios shouldn't have focus rings, their + // labels do + aState->focused = FALSE; + } + + if (aAppearance == StyleAppearance::ScrollbarthumbVertical || + aAppearance == StyleAppearance::ScrollbarthumbHorizontal) { + // for scrollbars we need to go up two to go from the thumb to + // the slider to the actual scrollbar object + nsIFrame* tmpFrame = aFrame->GetParent()->GetParent(); + + aState->curpos = CheckIntAttr(tmpFrame, nsGkAtoms::curpos, 0); + aState->maxpos = CheckIntAttr(tmpFrame, nsGkAtoms::maxpos, 100); + + if (CheckBooleanAttr(aFrame, nsGkAtoms::active)) { + aState->active = TRUE; + // Set hover state to emulate Gtk style of active scrollbar thumb + aState->inHover = TRUE; + } + } + + if (aAppearance == StyleAppearance::ScrollbarbuttonUp || + aAppearance == StyleAppearance::ScrollbarbuttonDown || + aAppearance == StyleAppearance::ScrollbarbuttonLeft || + aAppearance == StyleAppearance::ScrollbarbuttonRight) { + // set the state to disabled when the scrollbar is scrolled to + // the beginning or the end, depending on the button type. + int32_t curpos = CheckIntAttr(aFrame, nsGkAtoms::curpos, 0); + int32_t maxpos = CheckIntAttr(aFrame, nsGkAtoms::maxpos, 100); + if (ShouldScrollbarButtonBeDisabled(curpos, maxpos, aAppearance)) { + aState->disabled = true; + } + + // In order to simulate native GTK scrollbar click behavior, + // we set the active attribute on the element to true if it's + // pressed with any mouse button. + // This allows us to show that it's active without setting :active + else if (CheckBooleanAttr(aFrame, nsGkAtoms::active)) + aState->active = true; + + if (aWidgetFlags) { + *aWidgetFlags = GetScrollbarButtonType(aFrame); + if (static_cast<uint8_t>(aAppearance) - + static_cast<uint8_t>(StyleAppearance::ScrollbarbuttonUp) < + 2) + *aWidgetFlags |= MOZ_GTK_STEPPER_VERTICAL; + } + } + + // menu item state is determined by the attribute "_moz-menuactive", + // and not by the mouse hovering (accessibility). as a special case, + // menus which are children of a menu bar are only marked as prelight + // if they are open, not on normal hover. + + if (aAppearance == StyleAppearance::Menuitem || + aAppearance == StyleAppearance::Checkmenuitem || + aAppearance == StyleAppearance::Radiomenuitem || + aAppearance == StyleAppearance::Menuseparator || + aAppearance == StyleAppearance::Menuarrow) { + bool isTopLevel = false; + nsMenuFrame* menuFrame = do_QueryFrame(aFrame); + if (menuFrame) { + isTopLevel = menuFrame->IsOnMenuBar(); + } + + if (isTopLevel) { + aState->inHover = menuFrame->IsOpen(); + } else { + aState->inHover = CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + } + + aState->active = FALSE; + + if (aAppearance == StyleAppearance::Checkmenuitem || + aAppearance == StyleAppearance::Radiomenuitem) { + *aWidgetFlags = 0; + if (aFrame && aFrame->GetContent() && + aFrame->GetContent()->IsElement()) { + *aWidgetFlags = aFrame->GetContent()->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, + eIgnoreCase); + } + } + } + + // A button with drop down menu open or an activated toggle button + // should always appear depressed. + if (aAppearance == StyleAppearance::Button || + aAppearance == StyleAppearance::Toolbarbutton || + aAppearance == StyleAppearance::Dualbutton || + aAppearance == StyleAppearance::ToolbarbuttonDropdown || + aAppearance == StyleAppearance::Menulist || + aAppearance == StyleAppearance::MenulistButton || + aAppearance == StyleAppearance::MozMenulistArrowButton) { + bool menuOpen = IsOpenButton(aFrame); + aState->depressed = IsCheckedButton(aFrame) || menuOpen; + // we must not highlight buttons with open drop down menus on hover. + aState->inHover = aState->inHover && !menuOpen; + } + + // When the input field of the drop down button has focus, some themes + // should draw focus for the drop down button as well. + if ((aAppearance == StyleAppearance::MenulistButton || + aAppearance == StyleAppearance::MozMenulistArrowButton) && + aWidgetFlags) { + *aWidgetFlags = CheckBooleanAttr(aFrame, nsGkAtoms::parentfocused); + } + } + + if (aAppearance == StyleAppearance::MozWindowTitlebar || + aAppearance == StyleAppearance::MozWindowTitlebarMaximized || + aAppearance == StyleAppearance::MozWindowButtonClose || + aAppearance == StyleAppearance::MozWindowButtonMinimize || + aAppearance == StyleAppearance::MozWindowButtonMaximize || + aAppearance == StyleAppearance::MozWindowButtonRestore) { + aState->backdrop = !nsWindow::GetTopLevelWindowActiveState(aFrame); + } + + if (aAppearance == StyleAppearance::ScrollbarbuttonUp || + aAppearance == StyleAppearance::ScrollbarbuttonDown || + aAppearance == StyleAppearance::ScrollbarbuttonLeft || + aAppearance == StyleAppearance::ScrollbarbuttonRight || + aAppearance == StyleAppearance::ScrollbarVertical || + aAppearance == StyleAppearance::ScrollbarHorizontal || + aAppearance == StyleAppearance::ScrollbartrackHorizontal || + aAppearance == StyleAppearance::ScrollbartrackVertical || + aAppearance == StyleAppearance::ScrollbarthumbVertical || + aAppearance == StyleAppearance::ScrollbarthumbHorizontal) { + EventStates docState = + aFrame->GetContent()->OwnerDoc()->GetDocumentState(); + aState->backdrop = docState.HasState(NS_DOCUMENT_STATE_WINDOW_INACTIVE); + } + } + + switch (aAppearance) { + case StyleAppearance::Button: + if (aWidgetFlags) *aWidgetFlags = GTK_RELIEF_NORMAL; + aGtkWidgetType = MOZ_GTK_BUTTON; + break; + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Dualbutton: + if (aWidgetFlags) *aWidgetFlags = GTK_RELIEF_NONE; + aGtkWidgetType = MOZ_GTK_TOOLBAR_BUTTON; + break; + case StyleAppearance::FocusOutline: + aGtkWidgetType = MOZ_GTK_ENTRY; + break; + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: + aGtkWidgetType = (aAppearance == StyleAppearance::Radio) + ? MOZ_GTK_RADIOBUTTON + : MOZ_GTK_CHECKBUTTON; + break; + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_BUTTON; + break; + case StyleAppearance::ScrollbarVertical: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_VERTICAL; + if (GetWidgetTransparency(aFrame, aAppearance) == eOpaque) + *aWidgetFlags = MOZ_GTK_TRACK_OPAQUE; + else + *aWidgetFlags = 0; + break; + case StyleAppearance::ScrollbarHorizontal: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_HORIZONTAL; + if (GetWidgetTransparency(aFrame, aAppearance) == eOpaque) + *aWidgetFlags = MOZ_GTK_TRACK_OPAQUE; + else + *aWidgetFlags = 0; + break; + case StyleAppearance::ScrollbartrackHorizontal: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL; + break; + case StyleAppearance::ScrollbartrackVertical: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL; + break; + case StyleAppearance::ScrollbarthumbVertical: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_THUMB_VERTICAL; + break; + case StyleAppearance::ScrollbarthumbHorizontal: + aGtkWidgetType = MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL; + break; + case StyleAppearance::Spinner: + aGtkWidgetType = MOZ_GTK_SPINBUTTON; + break; + case StyleAppearance::SpinnerUpbutton: + aGtkWidgetType = MOZ_GTK_SPINBUTTON_UP; + break; + case StyleAppearance::SpinnerDownbutton: + aGtkWidgetType = MOZ_GTK_SPINBUTTON_DOWN; + break; + case StyleAppearance::SpinnerTextfield: + aGtkWidgetType = MOZ_GTK_SPINBUTTON_ENTRY; + break; + case StyleAppearance::Range: { + if (IsRangeHorizontal(aFrame)) { + if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_HORIZONTAL; + aGtkWidgetType = MOZ_GTK_SCALE_HORIZONTAL; + } else { + if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_VERTICAL; + aGtkWidgetType = MOZ_GTK_SCALE_VERTICAL; + } + break; + } + case StyleAppearance::RangeThumb: { + if (IsRangeHorizontal(aFrame)) { + if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_HORIZONTAL; + aGtkWidgetType = MOZ_GTK_SCALE_THUMB_HORIZONTAL; + } else { + if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_VERTICAL; + aGtkWidgetType = MOZ_GTK_SCALE_THUMB_VERTICAL; + } + break; + } + case StyleAppearance::Separator: + aGtkWidgetType = MOZ_GTK_TOOLBAR_SEPARATOR; + break; + case StyleAppearance::Toolbargripper: + aGtkWidgetType = MOZ_GTK_GRIPPER; + break; + case StyleAppearance::Resizer: + aGtkWidgetType = MOZ_GTK_RESIZER; + break; + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + aGtkWidgetType = MOZ_GTK_ENTRY; + break; + case StyleAppearance::Textarea: + aGtkWidgetType = MOZ_GTK_TEXT_VIEW; + break; + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + aGtkWidgetType = MOZ_GTK_TREEVIEW; + break; + case StyleAppearance::Treeheadercell: + if (aWidgetFlags) { + // In this case, the flag denotes whether the header is the sorted one + // or not + if (GetTreeSortDirection(aFrame) == eTreeSortDirection_Natural) + *aWidgetFlags = false; + else + *aWidgetFlags = true; + } + aGtkWidgetType = MOZ_GTK_TREE_HEADER_CELL; + break; + case StyleAppearance::Treeheadersortarrow: + if (aWidgetFlags) { + switch (GetTreeSortDirection(aFrame)) { + case eTreeSortDirection_Ascending: + *aWidgetFlags = GTK_ARROW_DOWN; + break; + case eTreeSortDirection_Descending: + *aWidgetFlags = GTK_ARROW_UP; + break; + case eTreeSortDirection_Natural: + default: + /* This prevents the treecolums from getting smaller + * and wider when switching sort direction off and on + * */ + *aWidgetFlags = GTK_ARROW_NONE; + break; + } + } + aGtkWidgetType = MOZ_GTK_TREE_HEADER_SORTARROW; + break; + case StyleAppearance::Treetwisty: + aGtkWidgetType = MOZ_GTK_TREEVIEW_EXPANDER; + if (aWidgetFlags) *aWidgetFlags = GTK_EXPANDER_COLLAPSED; + break; + case StyleAppearance::Treetwistyopen: + aGtkWidgetType = MOZ_GTK_TREEVIEW_EXPANDER; + if (aWidgetFlags) *aWidgetFlags = GTK_EXPANDER_EXPANDED; + break; + case StyleAppearance::MenulistButton: + case StyleAppearance::Menulist: + aGtkWidgetType = MOZ_GTK_DROPDOWN; + if (aWidgetFlags) + *aWidgetFlags = + IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XHTML); + break; + case StyleAppearance::MenulistText: + return false; // nothing to do, but prevents the bg from being drawn + case StyleAppearance::MozMenulistArrowButton: + aGtkWidgetType = MOZ_GTK_DROPDOWN_ARROW; + break; + case StyleAppearance::ToolbarbuttonDropdown: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowNext: + case StyleAppearance::ButtonArrowPrevious: + aGtkWidgetType = MOZ_GTK_TOOLBARBUTTON_ARROW; + if (aWidgetFlags) { + *aWidgetFlags = GTK_ARROW_DOWN; + + if (aAppearance == StyleAppearance::ButtonArrowUp) + *aWidgetFlags = GTK_ARROW_UP; + else if (aAppearance == StyleAppearance::ButtonArrowNext) + *aWidgetFlags = GTK_ARROW_RIGHT; + else if (aAppearance == StyleAppearance::ButtonArrowPrevious) + *aWidgetFlags = GTK_ARROW_LEFT; + } + break; + case StyleAppearance::CheckboxContainer: + aGtkWidgetType = MOZ_GTK_CHECKBUTTON_CONTAINER; + break; + case StyleAppearance::RadioContainer: + aGtkWidgetType = MOZ_GTK_RADIOBUTTON_CONTAINER; + break; + case StyleAppearance::CheckboxLabel: + aGtkWidgetType = MOZ_GTK_CHECKBUTTON_LABEL; + break; + case StyleAppearance::RadioLabel: + aGtkWidgetType = MOZ_GTK_RADIOBUTTON_LABEL; + break; + case StyleAppearance::Toolbar: + aGtkWidgetType = MOZ_GTK_TOOLBAR; + break; + case StyleAppearance::Tooltip: + aGtkWidgetType = MOZ_GTK_TOOLTIP; + break; + case StyleAppearance::Statusbarpanel: + case StyleAppearance::Resizerpanel: + aGtkWidgetType = MOZ_GTK_FRAME; + break; + case StyleAppearance::ProgressBar: + aGtkWidgetType = MOZ_GTK_PROGRESSBAR; + break; + case StyleAppearance::Progresschunk: { + nsIFrame* stateFrame = aFrame->GetParent(); + EventStates eventStates = GetContentState(stateFrame, aAppearance); + + aGtkWidgetType = IsIndeterminateProgress(stateFrame, eventStates) + ? IsVerticalProgress(stateFrame) + ? MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE + : MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE + : MOZ_GTK_PROGRESS_CHUNK; + } break; + case StyleAppearance::TabScrollArrowBack: + case StyleAppearance::TabScrollArrowForward: + if (aWidgetFlags) + *aWidgetFlags = aAppearance == StyleAppearance::TabScrollArrowBack + ? GTK_ARROW_LEFT + : GTK_ARROW_RIGHT; + aGtkWidgetType = MOZ_GTK_TAB_SCROLLARROW; + break; + case StyleAppearance::Tabpanels: + aGtkWidgetType = MOZ_GTK_TABPANELS; + break; + case StyleAppearance::Tab: { + if (IsBottomTab(aFrame)) { + aGtkWidgetType = MOZ_GTK_TAB_BOTTOM; + } else { + aGtkWidgetType = MOZ_GTK_TAB_TOP; + } + + if (aWidgetFlags) { + /* First bits will be used to store max(0,-bmargin) where bmargin + * is the bottom margin of the tab in pixels (resp. top margin, + * for bottom tabs). */ + *aWidgetFlags = GetTabMarginPixels(aFrame); + + if (IsSelectedTab(aFrame)) *aWidgetFlags |= MOZ_GTK_TAB_SELECTED; + + if (IsFirstTab(aFrame)) *aWidgetFlags |= MOZ_GTK_TAB_FIRST; + } + } break; + case StyleAppearance::Splitter: + if (IsHorizontal(aFrame)) + aGtkWidgetType = MOZ_GTK_SPLITTER_VERTICAL; + else + aGtkWidgetType = MOZ_GTK_SPLITTER_HORIZONTAL; + break; + case StyleAppearance::Menubar: + aGtkWidgetType = MOZ_GTK_MENUBAR; + break; + case StyleAppearance::Menupopup: + aGtkWidgetType = MOZ_GTK_MENUPOPUP; + break; + case StyleAppearance::Menuitem: { + nsMenuFrame* menuFrame = do_QueryFrame(aFrame); + if (menuFrame && menuFrame->IsOnMenuBar()) { + aGtkWidgetType = MOZ_GTK_MENUBARITEM; + break; + } + } + aGtkWidgetType = MOZ_GTK_MENUITEM; + break; + case StyleAppearance::Menuseparator: + aGtkWidgetType = MOZ_GTK_MENUSEPARATOR; + break; + case StyleAppearance::Menuarrow: + aGtkWidgetType = MOZ_GTK_MENUARROW; + break; + case StyleAppearance::Checkmenuitem: + aGtkWidgetType = MOZ_GTK_CHECKMENUITEM; + break; + case StyleAppearance::Radiomenuitem: + aGtkWidgetType = MOZ_GTK_RADIOMENUITEM; + break; + case StyleAppearance::Window: + case StyleAppearance::Dialog: + aGtkWidgetType = MOZ_GTK_WINDOW; + break; + case StyleAppearance::MozGtkInfoBar: + aGtkWidgetType = MOZ_GTK_INFO_BAR; + break; + case StyleAppearance::MozWindowTitlebar: + aGtkWidgetType = MOZ_GTK_HEADER_BAR; + break; + case StyleAppearance::MozWindowTitlebarMaximized: + aGtkWidgetType = MOZ_GTK_HEADER_BAR_MAXIMIZED; + break; + case StyleAppearance::MozWindowButtonBox: + aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_BOX; + break; + case StyleAppearance::MozWindowButtonClose: + aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_CLOSE; + break; + case StyleAppearance::MozWindowButtonMinimize: + aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE; + break; + case StyleAppearance::MozWindowButtonMaximize: + aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE; + break; + case StyleAppearance::MozWindowButtonRestore: + aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE; + break; + default: + return false; + } + + return true; +} + +class SystemCairoClipper : public ClipExporter { + public: + explicit SystemCairoClipper(cairo_t* aContext, gint aScaleFactor = 1) + : mContext(aContext), mScaleFactor(aScaleFactor) {} + + void BeginClip(const Matrix& aTransform) override { + cairo_matrix_t mat; + GfxMatrixToCairoMatrix(aTransform, mat); + // We also need to remove the scale factor effect from the matrix + mat.y0 = mat.y0 / mScaleFactor; + mat.x0 = mat.x0 / mScaleFactor; + cairo_set_matrix(mContext, &mat); + + cairo_new_path(mContext); + } + + void MoveTo(const Point& aPoint) override { + cairo_move_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor); + mBeginPoint = aPoint; + mCurrentPoint = aPoint; + } + + void LineTo(const Point& aPoint) override { + cairo_line_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor); + mCurrentPoint = aPoint; + } + + void BezierTo(const Point& aCP1, const Point& aCP2, + const Point& aCP3) override { + cairo_curve_to(mContext, aCP1.x / mScaleFactor, aCP1.y / mScaleFactor, + aCP2.x / mScaleFactor, aCP2.y / mScaleFactor, + aCP3.x / mScaleFactor, aCP3.y / mScaleFactor); + mCurrentPoint = aCP3; + } + + void QuadraticBezierTo(const Point& aCP1, const Point& aCP2) override { + Point CP0 = CurrentPoint(); + Point CP1 = (CP0 + aCP1 * 2.0) / 3.0; + Point CP2 = (aCP2 + aCP1 * 2.0) / 3.0; + Point CP3 = aCP2; + cairo_curve_to(mContext, CP1.x / mScaleFactor, CP1.y / mScaleFactor, + CP2.x / mScaleFactor, CP2.y / mScaleFactor, + CP3.x / mScaleFactor, CP3.y / mScaleFactor); + mCurrentPoint = aCP2; + } + + void Arc(const Point& aOrigin, float aRadius, float aStartAngle, + float aEndAngle, bool aAntiClockwise) override { + ArcToBezier(this, aOrigin, Size(aRadius, aRadius), aStartAngle, aEndAngle, + aAntiClockwise); + } + + void Close() override { + cairo_close_path(mContext); + mCurrentPoint = mBeginPoint; + } + + void EndClip() override { cairo_clip(mContext); } + + private: + cairo_t* mContext; + gint mScaleFactor; +}; + +static void DrawThemeWithCairo(gfxContext* aContext, DrawTarget* aDrawTarget, + GtkWidgetState aState, + WidgetNodeType aGTKWidgetType, gint aFlags, + GtkTextDirection aDirection, gint aScaleFactor, + bool aSnapped, const Point& aDrawOrigin, + const nsIntSize& aDrawSize, + GdkRectangle& aGDKRect, + nsITheme::Transparency aTransparency) { + bool isX11Display = gfxPlatformGtk::GetPlatform()->IsX11Display(); + static auto sCairoSurfaceSetDeviceScalePtr = + (void (*)(cairo_surface_t*, double, double))dlsym( + RTLD_DEFAULT, "cairo_surface_set_device_scale"); + bool useHiDPIWidgets = + (aScaleFactor != 1) && (sCairoSurfaceSetDeviceScalePtr != nullptr); + + Point drawOffsetScaled; + Point drawOffsetOriginal; + Matrix transform; + if (!aSnapped) { + // If we are not snapped, we depend on the DT for translation. + drawOffsetOriginal = aDrawOrigin; + drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor + : drawOffsetOriginal; + transform = aDrawTarget->GetTransform().PreTranslate(drawOffsetScaled); + } else { + // Otherwise, we only need to take the device offset into account. + drawOffsetOriginal = aDrawOrigin - aContext->GetDeviceOffset(); + drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor + : drawOffsetOriginal; + transform = Matrix::Translation(drawOffsetScaled); + } + + if (!useHiDPIWidgets && aScaleFactor != 1) { + transform.PreScale(aScaleFactor, aScaleFactor); + } + + cairo_matrix_t mat; + GfxMatrixToCairoMatrix(transform, mat); + + nsIntSize clipSize((aDrawSize.width + aScaleFactor - 1) / aScaleFactor, + (aDrawSize.height + aScaleFactor - 1) / aScaleFactor); + + // A direct Cairo draw target is not available, so we need to create a + // temporary one. +#if defined(MOZ_X11) && defined(CAIRO_HAS_XLIB_SURFACE) + if (isX11Display) { + // If using a Cairo xlib surface, then try to reuse it. + BorrowedXlibDrawable borrow(aDrawTarget); + if (borrow.GetDrawable()) { + nsIntSize size = borrow.GetSize(); + cairo_surface_t* surf = nullptr; + // Check if the surface is using XRender. +# ifdef CAIRO_HAS_XLIB_XRENDER_SURFACE + if (borrow.GetXRenderFormat()) { + surf = cairo_xlib_surface_create_with_xrender_format( + borrow.GetDisplay(), borrow.GetDrawable(), borrow.GetScreen(), + borrow.GetXRenderFormat(), size.width, size.height); + } else { +# else + if (!borrow.GetXRenderFormat()) { +# endif + surf = cairo_xlib_surface_create( + borrow.GetDisplay(), borrow.GetDrawable(), borrow.GetVisual(), + size.width, size.height); + } + if (!NS_WARN_IF(!surf)) { + Point offset = borrow.GetOffset(); + if (offset != Point()) { + cairo_surface_set_device_offset(surf, offset.x, offset.y); + } + cairo_t* cr = cairo_create(surf); + if (!NS_WARN_IF(!cr)) { + RefPtr<SystemCairoClipper> clipper = new SystemCairoClipper(cr); + aContext->ExportClip(*clipper); + + cairo_set_matrix(cr, &mat); + + cairo_new_path(cr); + cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height); + cairo_clip(cr); + + moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags, + aDirection); + + cairo_destroy(cr); + } + cairo_surface_destroy(surf); + } + borrow.Finish(); + return; + } + } +#endif + + // Check if the widget requires complex masking that must be composited. + // Try to directly write to the draw target's pixels if possible. + uint8_t* data; + nsIntSize size; + int32_t stride; + SurfaceFormat format; + IntPoint origin; + if (aDrawTarget->LockBits(&data, &size, &stride, &format, &origin)) { + // Create a Cairo image surface context the device rectangle. + cairo_surface_t* surf = cairo_image_surface_create_for_data( + data, GfxFormatToCairoFormat(format), size.width, size.height, stride); + if (!NS_WARN_IF(!surf)) { + if (useHiDPIWidgets) { + sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor); + } + if (origin != IntPoint()) { + cairo_surface_set_device_offset(surf, -origin.x, -origin.y); + } + cairo_t* cr = cairo_create(surf); + if (!NS_WARN_IF(!cr)) { + RefPtr<SystemCairoClipper> clipper = + new SystemCairoClipper(cr, useHiDPIWidgets ? aScaleFactor : 1); + aContext->ExportClip(*clipper); + + cairo_set_matrix(cr, &mat); + + cairo_new_path(cr); + cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height); + cairo_clip(cr); + + moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags, + aDirection); + + cairo_destroy(cr); + } + cairo_surface_destroy(surf); + } + aDrawTarget->ReleaseBits(data); + } else { + // If the widget has any transparency, make sure to choose an alpha format. + format = aTransparency != nsITheme::eOpaque ? SurfaceFormat::B8G8R8A8 + : aDrawTarget->GetFormat(); + // Create a temporary data surface to render the widget into. + RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface( + aDrawSize, format, aTransparency != nsITheme::eOpaque); + DataSourceSurface::MappedSurface map; + if (!NS_WARN_IF( + !(dataSurface && + dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)))) { + // Create a Cairo image surface wrapping the data surface. + cairo_surface_t* surf = cairo_image_surface_create_for_data( + map.mData, GfxFormatToCairoFormat(format), aDrawSize.width, + aDrawSize.height, map.mStride); + cairo_t* cr = nullptr; + if (!NS_WARN_IF(!surf)) { + cr = cairo_create(surf); + if (!NS_WARN_IF(!cr)) { + if (aScaleFactor != 1) { + if (useHiDPIWidgets) { + sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor); + } else { + cairo_scale(cr, aScaleFactor, aScaleFactor); + } + } + + moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags, + aDirection); + } + } + + // Unmap the surface before using it as a source + dataSurface->Unmap(); + + if (cr) { + // The widget either needs to be masked or has transparency, so use the + // slower drawing path. + aDrawTarget->DrawSurface( + dataSurface, + Rect(aSnapped ? drawOffsetOriginal - + aDrawTarget->GetTransform().GetTranslation() + : drawOffsetOriginal, + Size(aDrawSize)), + Rect(0, 0, aDrawSize.width, aDrawSize.height)); + cairo_destroy(cr); + } + + if (surf) { + cairo_surface_destroy(surf); + } + } + } +} + +bool nsNativeThemeGTK::GetExtraSizeForWidget(nsIFrame* aFrame, + StyleAppearance aAppearance, + nsIntMargin* aExtra) { + *aExtra = nsIntMargin(0, 0, 0, 0); + // Allow an extra one pixel above and below the thumb for certain + // GTK2 themes (Ximian Industrial, Bluecurve, Misty, at least); + // We modify the frame's overflow area. See bug 297508. + switch (aAppearance) { + case StyleAppearance::ScrollbarthumbVertical: + aExtra->top = aExtra->bottom = 1; + break; + case StyleAppearance::ScrollbarthumbHorizontal: + aExtra->left = aExtra->right = 1; + break; + + case StyleAppearance::Button: { + if (IsDefaultButton(aFrame)) { + // Some themes draw a default indicator outside the widget, + // include that in overflow + gint top, left, bottom, right; + moz_gtk_button_get_default_overflow(&top, &left, &bottom, &right); + aExtra->top = top; + aExtra->right = right; + aExtra->bottom = bottom; + aExtra->left = left; + break; + } + return false; + } + case StyleAppearance::FocusOutline: { + moz_gtk_get_focus_outline_size(&aExtra->left, &aExtra->top); + aExtra->right = aExtra->left; + aExtra->bottom = aExtra->top; + break; + } + case StyleAppearance::Tab: { + if (!IsSelectedTab(aFrame)) return false; + + gint gap_height = moz_gtk_get_tab_thickness( + IsBottomTab(aFrame) ? MOZ_GTK_TAB_BOTTOM : MOZ_GTK_TAB_TOP); + if (!gap_height) return false; + + int32_t extra = gap_height - GetTabMarginPixels(aFrame); + if (extra <= 0) return false; + + if (IsBottomTab(aFrame)) { + aExtra->top = extra; + } else { + aExtra->bottom = extra; + } + return false; + } + default: + return false; + } + gint scale = GetMonitorScaleFactor(aFrame); + aExtra->top *= scale; + aExtra->right *= scale; + aExtra->bottom *= scale; + aExtra->left *= scale; + return true; +} + +bool nsNativeThemeGTK::IsWidgetVisible(StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::MozWindowButtonBox: + return false; + default: + break; + } + return true; +} + +NS_IMETHODIMP +nsNativeThemeGTK::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, + const nsRect& aDirtyRect) { + GtkWidgetState state; + WidgetNodeType gtkWidgetType; + GtkTextDirection direction = GetTextDirection(aFrame); + gint flags; + + if (!IsWidgetVisible(aAppearance) || + !GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, &state, + &flags)) { + return NS_OK; + } + + gfxContext* ctx = aContext; + nsPresContext* presContext = aFrame->PresContext(); + + gfxRect rect = presContext->AppUnitsToGfxUnits(aRect); + gfxRect dirtyRect = presContext->AppUnitsToGfxUnits(aDirtyRect); + gint scaleFactor = GetMonitorScaleFactor(aFrame); + + // Align to device pixels where sensible + // to provide crisper and faster drawing. + // Don't snap if it's a non-unit scale factor. We're going to have to take + // slow paths then in any case. + bool snapped = ctx->UserToDevicePixelSnapped(rect); + if (snapped) { + // Leave rect in device coords but make dirtyRect consistent. + dirtyRect = ctx->UserToDevice(dirtyRect); + } + + // Translate the dirty rect so that it is wrt the widget top-left. + dirtyRect.MoveBy(-rect.TopLeft()); + // Round out the dirty rect to gdk pixels to ensure that gtk draws + // enough pixels for interpolation to device pixels. + dirtyRect.RoundOut(); + + // GTK themes can only draw an integer number of pixels + // (even when not snapped). + nsIntRect widgetRect(0, 0, NS_lround(rect.Width()), NS_lround(rect.Height())); + nsIntRect overflowRect(widgetRect); + nsIntMargin extraSize; + if (GetExtraSizeForWidget(aFrame, aAppearance, &extraSize)) { + overflowRect.Inflate(extraSize); + } + + // This is the rectangle that will actually be drawn, in gdk pixels + nsIntRect drawingRect(int32_t(dirtyRect.X()), int32_t(dirtyRect.Y()), + int32_t(dirtyRect.Width()), + int32_t(dirtyRect.Height())); + if (widgetRect.IsEmpty() || + !drawingRect.IntersectRect(overflowRect, drawingRect)) + return NS_OK; + + NS_ASSERTION(!IsWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance), + "Trying to render an unsafe widget!"); + + bool safeState = IsWidgetStateSafe(mSafeWidgetStates, aAppearance, &state); + if (!safeState) { + gLastGdkError = 0; + gdk_error_trap_push(); + } + + Transparency transparency = GetWidgetTransparency(aFrame, aAppearance); + + // gdk rectangles are wrt the drawing rect. + GdkRectangle gdk_rect = { + -drawingRect.x / scaleFactor, -drawingRect.y / scaleFactor, + widgetRect.width / scaleFactor, widgetRect.height / scaleFactor}; + + // Save actual widget scale to GtkWidgetState as we don't provide + // nsFrame to gtk3drawing routines. + state.scale = scaleFactor; + + // translate everything so (0,0) is the top left of the drawingRect + gfxPoint origin = rect.TopLeft() + drawingRect.TopLeft(); + + DrawThemeWithCairo(ctx, aContext->GetDrawTarget(), state, gtkWidgetType, + flags, direction, scaleFactor, snapped, ToPoint(origin), + drawingRect.Size(), gdk_rect, transparency); + + if (!safeState) { + // gdk_flush() call from expose event crashes Gtk+ on Wayland + // (Gnome BZ #773307) + if (gfxPlatformGtk::GetPlatform()->IsX11Display()) { + gdk_flush(); + } + gLastGdkError = gdk_error_trap_pop(); + + if (gLastGdkError) { +#ifdef DEBUG + printf( + "GTK theme failed for widget type %d, error was %d, state was " + "[active=%d,focused=%d,inHover=%d,disabled=%d]\n", + static_cast<int>(aAppearance), gLastGdkError, state.active, + state.focused, state.inHover, state.disabled); +#endif + NS_WARNING("GTK theme failed; disabling unsafe widget"); + SetWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance); + // force refresh of the window, because the widget was not + // successfully drawn it must be redrawn using the default look + RefreshWidgetWindow(aFrame); + } else { + SetWidgetStateSafe(mSafeWidgetStates, aAppearance, &state); + } + } + + // Indeterminate progress bar are animated. + if (gtkWidgetType == MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE || + gtkWidgetType == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE) { + if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) { + NS_WARNING("unable to animate widget!"); + } + } + + return NS_OK; +} + +bool nsNativeThemeGTK::CreateWebRenderCommandsForWidget( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame, + StyleAppearance aAppearance, const nsRect& aRect) { + nsPresContext* presContext = aFrame->PresContext(); + wr::LayoutRect bounds = wr::ToLayoutRect(LayoutDeviceRect::FromAppUnits( + aRect, presContext->AppUnitsPerDevPixel())); + + switch (aAppearance) { + case StyleAppearance::Window: + case StyleAppearance::Dialog: + aBuilder.PushRect( + bounds, bounds, true, + wr::ToColorF(ToDeviceColor(LookAndFeel::GetColor( + LookAndFeel::ColorID::WindowBackground, NS_RGBA(0, 0, 0, 0))))); + return true; + + default: + return false; + } +} + +WidgetNodeType nsNativeThemeGTK::NativeThemeToGtkTheme( + StyleAppearance aAppearance, nsIFrame* aFrame) { + WidgetNodeType gtkWidgetType; + gint unusedFlags; + + if (!GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr, + &unusedFlags)) { + MOZ_ASSERT_UNREACHABLE("Unknown native widget to gtk widget mapping"); + return MOZ_GTK_WINDOW; + } + return gtkWidgetType; +} + +static void FixupForVerticalWritingMode(WritingMode aWritingMode, + LayoutDeviceIntMargin* aResult) { + if (aWritingMode.IsVertical()) { + bool rtl = aWritingMode.IsBidiRTL(); + LogicalMargin logical(aWritingMode, aResult->top, + rtl ? aResult->left : aResult->right, aResult->bottom, + rtl ? aResult->right : aResult->left); + nsMargin physical = logical.GetPhysicalMargin(aWritingMode); + aResult->top = physical.top; + aResult->right = physical.right; + aResult->bottom = physical.bottom; + aResult->left = physical.left; + } +} + +void nsNativeThemeGTK::GetCachedWidgetBorder(nsIFrame* aFrame, + StyleAppearance aAppearance, + GtkTextDirection aDirection, + LayoutDeviceIntMargin* aResult) { + aResult->SizeTo(0, 0, 0, 0); + + WidgetNodeType gtkWidgetType; + gint unusedFlags; + if (GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr, + &unusedFlags)) { + MOZ_ASSERT(0 <= gtkWidgetType && gtkWidgetType < MOZ_GTK_WIDGET_NODE_COUNT); + uint8_t cacheIndex = gtkWidgetType / 8; + uint8_t cacheBit = 1u << (gtkWidgetType % 8); + + if (mBorderCacheValid[cacheIndex] & cacheBit) { + *aResult = mBorderCache[gtkWidgetType]; + } else { + moz_gtk_get_widget_border(gtkWidgetType, &aResult->left, &aResult->top, + &aResult->right, &aResult->bottom, aDirection); + if (gtkWidgetType != MOZ_GTK_DROPDOWN) { // depends on aDirection + mBorderCacheValid[cacheIndex] |= cacheBit; + mBorderCache[gtkWidgetType] = *aResult; + } + } + } + FixupForVerticalWritingMode(aFrame->GetWritingMode(), aResult); +} + +LayoutDeviceIntMargin nsNativeThemeGTK::GetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { + LayoutDeviceIntMargin result; + GtkTextDirection direction = GetTextDirection(aFrame); + switch (aAppearance) { + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbarVertical: { + GtkOrientation orientation = + aAppearance == StyleAppearance::ScrollbarHorizontal + ? GTK_ORIENTATION_HORIZONTAL + : GTK_ORIENTATION_VERTICAL; + const ScrollbarGTKMetrics* metrics = + GetActiveScrollbarMetrics(orientation); + + const GtkBorder& border = metrics->border.scrollbar; + result.top = border.top; + result.right = border.right; + result.bottom = border.bottom; + result.left = border.left; + } break; + case StyleAppearance::ScrollbartrackHorizontal: + case StyleAppearance::ScrollbartrackVertical: { + GtkOrientation orientation = + aAppearance == StyleAppearance::ScrollbartrackHorizontal + ? GTK_ORIENTATION_HORIZONTAL + : GTK_ORIENTATION_VERTICAL; + const ScrollbarGTKMetrics* metrics = + GetActiveScrollbarMetrics(orientation); + + const GtkBorder& border = metrics->border.track; + result.top = border.top; + result.right = border.right; + result.bottom = border.bottom; + result.left = border.left; + } break; + case StyleAppearance::Toolbox: + // gtk has no toolbox equivalent. So, although we map toolbox to + // gtk's 'toolbar' for purposes of painting the widget background, + // we don't use the toolbar border for toolbox. + break; + case StyleAppearance::Dualbutton: + // TOOLBAR_DUAL_BUTTON is an interesting case. We want a border to draw + // around the entire button + dropdown, and also an inner border if you're + // over the button part. But, we want the inner button to be right up + // against the edge of the outer button so that the borders overlap. + // To make this happen, we draw a button border for the outer button, + // but don't reserve any space for it. + break; + case StyleAppearance::Tab: { + WidgetNodeType gtkWidgetType; + gint flags; + + if (!GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr, + &flags)) { + return result; + } + moz_gtk_get_tab_border(&result.left, &result.top, &result.right, + &result.bottom, direction, (GtkTabFlags)flags, + gtkWidgetType); + } break; + case StyleAppearance::Menuitem: + case StyleAppearance::Checkmenuitem: + case StyleAppearance::Radiomenuitem: + // For regular menuitems, we will be using GetWidgetPadding instead of + // GetWidgetBorder to pad up the widget's internals; other menuitems + // will need to fall through and use the default case as before. + if (IsRegularMenuItem(aFrame)) break; + [[fallthrough]]; + default: { + GetCachedWidgetBorder(aFrame, aAppearance, direction, &result); + } + } + + gint scale = GetMonitorScaleFactor(aFrame); + result.top *= scale; + result.right *= scale; + result.bottom *= scale; + result.left *= scale; + return result; +} + +bool nsNativeThemeGTK::GetWidgetPadding(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) { + switch (aAppearance) { + case StyleAppearance::ButtonFocus: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::MozWindowButtonBox: + case StyleAppearance::MozWindowButtonClose: + case StyleAppearance::MozWindowButtonMinimize: + case StyleAppearance::MozWindowButtonMaximize: + case StyleAppearance::MozWindowButtonRestore: + case StyleAppearance::Dualbutton: + case StyleAppearance::TabScrollArrowBack: + case StyleAppearance::TabScrollArrowForward: + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::ToolbarbuttonDropdown: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::ButtonArrowNext: + case StyleAppearance::ButtonArrowPrevious: + case StyleAppearance::RangeThumb: + // Radios and checkboxes return a fixed size in GetMinimumWidgetSize + // and have a meaningful baseline, so they can't have + // author-specified padding. + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: + aResult->SizeTo(0, 0, 0, 0); + return true; + case StyleAppearance::Menuitem: + case StyleAppearance::Checkmenuitem: + case StyleAppearance::Radiomenuitem: { + // Menubar and menulist have their padding specified in CSS. + if (!IsRegularMenuItem(aFrame)) return false; + + GetCachedWidgetBorder(aFrame, aAppearance, GetTextDirection(aFrame), + aResult); + + gint horizontal_padding; + if (aAppearance == StyleAppearance::Menuitem) + moz_gtk_menuitem_get_horizontal_padding(&horizontal_padding); + else + moz_gtk_checkmenuitem_get_horizontal_padding(&horizontal_padding); + + aResult->left += horizontal_padding; + aResult->right += horizontal_padding; + + gint scale = GetMonitorScaleFactor(aFrame); + aResult->top *= scale; + aResult->right *= scale; + aResult->bottom *= scale; + aResult->left *= scale; + + return true; + } + default: + break; + } + + return false; +} + +bool nsNativeThemeGTK::GetWidgetOverflow(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) { + nsIntMargin extraSize; + if (!GetExtraSizeForWidget(aFrame, aAppearance, &extraSize)) return false; + + int32_t p2a = aContext->AppUnitsPerDevPixel(); + nsMargin m(NSIntPixelsToAppUnits(extraSize.top, p2a), + NSIntPixelsToAppUnits(extraSize.right, p2a), + NSIntPixelsToAppUnits(extraSize.bottom, p2a), + NSIntPixelsToAppUnits(extraSize.left, p2a)); + + aOverflowRect->Inflate(m); + return true; +} + +NS_IMETHODIMP +nsNativeThemeGTK::GetMinimumWidgetSize(nsPresContext* aPresContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntSize* aResult, + bool* aIsOverridable) { + aResult->width = aResult->height = 0; + *aIsOverridable = true; + + switch (aAppearance) { + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: { + const ScrollbarGTKMetrics* metrics = + GetActiveScrollbarMetrics(GTK_ORIENTATION_VERTICAL); + + aResult->width = metrics->size.button.width; + aResult->height = metrics->size.button.height; + *aIsOverridable = false; + } break; + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: { + const ScrollbarGTKMetrics* metrics = + GetActiveScrollbarMetrics(GTK_ORIENTATION_HORIZONTAL); + + aResult->width = metrics->size.button.width; + aResult->height = metrics->size.button.height; + *aIsOverridable = false; + } break; + case StyleAppearance::Splitter: { + gint metrics; + if (IsHorizontal(aFrame)) { + moz_gtk_splitter_get_metrics(GTK_ORIENTATION_HORIZONTAL, &metrics); + aResult->width = metrics; + aResult->height = 0; + } else { + moz_gtk_splitter_get_metrics(GTK_ORIENTATION_VERTICAL, &metrics); + aResult->width = 0; + aResult->height = metrics; + } + *aIsOverridable = false; + } break; + case StyleAppearance::ScrollbarNonDisappearing: { + const ScrollbarGTKMetrics* verticalMetrics = + GetActiveScrollbarMetrics(GTK_ORIENTATION_VERTICAL); + const ScrollbarGTKMetrics* horizontalMetrics = + GetActiveScrollbarMetrics(GTK_ORIENTATION_HORIZONTAL); + aResult->width = verticalMetrics->size.scrollbar.width; + aResult->height = horizontalMetrics->size.scrollbar.height; + } break; + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbarVertical: { + /* While we enforce a minimum size for the thumb, this is ignored + * for the some scrollbars if buttons are hidden (bug 513006) because + * the thumb isn't a direct child of the scrollbar, unlike the buttons + * or track. So add a minimum size to the track as well to prevent a + * 0-width scrollbar. */ + GtkOrientation orientation = + aAppearance == StyleAppearance::ScrollbarHorizontal + ? GTK_ORIENTATION_HORIZONTAL + : GTK_ORIENTATION_VERTICAL; + const ScrollbarGTKMetrics* metrics = + GetActiveScrollbarMetrics(orientation); + + aResult->width = metrics->size.scrollbar.width; + aResult->height = metrics->size.scrollbar.height; + } break; + case StyleAppearance::ScrollbarthumbVertical: + case StyleAppearance::ScrollbarthumbHorizontal: { + GtkOrientation orientation = + aAppearance == StyleAppearance::ScrollbarthumbHorizontal + ? GTK_ORIENTATION_HORIZONTAL + : GTK_ORIENTATION_VERTICAL; + const ScrollbarGTKMetrics* metrics = + GetActiveScrollbarMetrics(orientation); + + aResult->width = metrics->size.thumb.width; + aResult->height = metrics->size.thumb.height; + *aIsOverridable = false; + } break; + case StyleAppearance::RangeThumb: { + gint thumb_length, thumb_height; + + if (IsRangeHorizontal(aFrame)) { + moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_HORIZONTAL, + &thumb_length, &thumb_height); + } else { + moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_VERTICAL, &thumb_height, + &thumb_length); + } + aResult->width = thumb_length; + aResult->height = thumb_height; + + *aIsOverridable = false; + } break; + case StyleAppearance::TabScrollArrowBack: + case StyleAppearance::TabScrollArrowForward: { + moz_gtk_get_tab_scroll_arrow_size(&aResult->width, &aResult->height); + *aIsOverridable = false; + } break; + case StyleAppearance::MozMenulistArrowButton: { + moz_gtk_get_combo_box_entry_button_size(&aResult->width, + &aResult->height); + *aIsOverridable = false; + } break; + case StyleAppearance::Menuseparator: { + gint separator_height; + + moz_gtk_get_menu_separator_height(&separator_height); + aResult->height = separator_height; + + *aIsOverridable = false; + } break; + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: { + const ToggleGTKMetrics* metrics = GetToggleMetrics( + aAppearance == StyleAppearance::Radio ? MOZ_GTK_RADIOBUTTON + : MOZ_GTK_CHECKBUTTON); + aResult->width = metrics->minSizeWithBorder.width; + aResult->height = metrics->minSizeWithBorder.height; + } break; + case StyleAppearance::ToolbarbuttonDropdown: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::ButtonArrowNext: + case StyleAppearance::ButtonArrowPrevious: { + moz_gtk_get_arrow_size(MOZ_GTK_TOOLBARBUTTON_ARROW, &aResult->width, + &aResult->height); + *aIsOverridable = false; + } break; + case StyleAppearance::MozWindowButtonClose: { + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_CLOSE); + aResult->width = metrics->minSizeWithBorderMargin.width; + aResult->height = metrics->minSizeWithBorderMargin.height; + break; + } + case StyleAppearance::MozWindowButtonMinimize: { + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE); + aResult->width = metrics->minSizeWithBorderMargin.width; + aResult->height = metrics->minSizeWithBorderMargin.height; + break; + } + case StyleAppearance::MozWindowButtonMaximize: + case StyleAppearance::MozWindowButtonRestore: { + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE); + aResult->width = metrics->minSizeWithBorderMargin.width; + aResult->height = metrics->minSizeWithBorderMargin.height; + break; + } + case StyleAppearance::CheckboxContainer: + case StyleAppearance::RadioContainer: + case StyleAppearance::CheckboxLabel: + case StyleAppearance::RadioLabel: + case StyleAppearance::Button: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Treeheadercell: { + if (aAppearance == StyleAppearance::Menulist || + aAppearance == StyleAppearance::MenulistButton) { + // Include the arrow size. + moz_gtk_get_arrow_size(MOZ_GTK_DROPDOWN, &aResult->width, + &aResult->height); + } + // else the minimum size is missing consideration of container + // descendants; the value returned here will not be helpful, but the + // box model may consider border and padding with child minimum sizes. + + LayoutDeviceIntMargin border; + GetCachedWidgetBorder(aFrame, aAppearance, GetTextDirection(aFrame), + &border); + aResult->width += border.left + border.right; + aResult->height += border.top + border.bottom; + } break; + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: { + gint contentHeight = 0; + gint borderPaddingHeight = 0; + moz_gtk_get_entry_min_height(&contentHeight, &borderPaddingHeight); + + // Scale the min content height proportionately with the font-size if it's + // smaller than the default one. This prevents <input type=text + // style="font-size: .5em"> from keeping a ridiculously large size, for + // example. + const gfxFloat fieldFontSizeInCSSPixels = [] { + gfxFontStyle fieldFontStyle; + nsAutoString unusedFontName; + DebugOnly<bool> result = LookAndFeel::GetFont( + LookAndFeel::FontID::Field, unusedFontName, fieldFontStyle); + MOZ_ASSERT(result, "GTK look and feel supports the field font"); + // NOTE: GetFont returns font sizes in CSS pixels, and we want just + // that. + return fieldFontStyle.size; + }(); + + const gfxFloat fontSize = aFrame->StyleFont()->mFont.size.ToCSSPixels(); + if (fieldFontSizeInCSSPixels > fontSize) { + contentHeight = + std::round(contentHeight * fontSize / fieldFontSizeInCSSPixels); + } + + gint height = contentHeight + borderPaddingHeight; + if (aFrame->GetWritingMode().IsVertical()) { + aResult->width = height; + } else { + aResult->height = height; + } + } break; + case StyleAppearance::Separator: { + gint separator_width; + + moz_gtk_get_toolbar_separator_width(&separator_width); + + aResult->width = separator_width; + } break; + case StyleAppearance::Spinner: + // hard code these sizes + aResult->width = 14; + aResult->height = 26; + break; + case StyleAppearance::Treeheadersortarrow: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + // hard code these sizes + aResult->width = 14; + aResult->height = 13; + break; + case StyleAppearance::Resizer: + // same as Windows to make our lives easier + aResult->width = aResult->height = 15; + *aIsOverridable = false; + break; + case StyleAppearance::Treetwisty: + case StyleAppearance::Treetwistyopen: { + gint expander_size; + + moz_gtk_get_treeview_expander_size(&expander_size); + aResult->width = aResult->height = expander_size; + *aIsOverridable = false; + } break; + default: + break; + } + + *aResult = *aResult * GetMonitorScaleFactor(aFrame); + + return NS_OK; +} + +NS_IMETHODIMP +nsNativeThemeGTK::WidgetStateChanged(nsIFrame* aFrame, + StyleAppearance aAppearance, + nsAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) { + // Some widget types just never change state. + if (aAppearance == StyleAppearance::Toolbox || + aAppearance == StyleAppearance::Toolbar || + aAppearance == StyleAppearance::Statusbar || + aAppearance == StyleAppearance::Statusbarpanel || + aAppearance == StyleAppearance::Resizerpanel || + aAppearance == StyleAppearance::Progresschunk || + aAppearance == StyleAppearance::ProgressBar || + aAppearance == StyleAppearance::Menubar || + aAppearance == StyleAppearance::Menupopup || + aAppearance == StyleAppearance::Tooltip || + aAppearance == StyleAppearance::Menuseparator || + aAppearance == StyleAppearance::Window || + aAppearance == StyleAppearance::Dialog) { + *aShouldRepaint = false; + return NS_OK; + } + + if (aAppearance == StyleAppearance::MozWindowTitlebar || + aAppearance == StyleAppearance::MozWindowTitlebarMaximized || + aAppearance == StyleAppearance::MozWindowButtonClose || + aAppearance == StyleAppearance::MozWindowButtonMinimize || + aAppearance == StyleAppearance::MozWindowButtonMaximize || + aAppearance == StyleAppearance::MozWindowButtonRestore) { + *aShouldRepaint = true; + return NS_OK; + } + + if ((aAppearance == StyleAppearance::ScrollbarthumbVertical || + aAppearance == StyleAppearance::ScrollbarthumbHorizontal) && + aAttribute == nsGkAtoms::active) { + *aShouldRepaint = true; + return NS_OK; + } + + if ((aAppearance == StyleAppearance::ScrollbarbuttonUp || + aAppearance == StyleAppearance::ScrollbarbuttonDown || + aAppearance == StyleAppearance::ScrollbarbuttonLeft || + aAppearance == StyleAppearance::ScrollbarbuttonRight) && + (aAttribute == nsGkAtoms::curpos || aAttribute == nsGkAtoms::maxpos)) { + // If 'curpos' has changed and we are passed its old value, we can + // determine whether the button's enablement actually needs to change. + if (aAttribute == nsGkAtoms::curpos && aOldValue) { + int32_t curpos = CheckIntAttr(aFrame, nsGkAtoms::curpos, 0); + int32_t maxpos = CheckIntAttr(aFrame, nsGkAtoms::maxpos, 0); + nsAutoString str; + aOldValue->ToString(str); + nsresult err; + int32_t oldCurpos = str.ToInteger(&err); + if (str.IsEmpty() || NS_FAILED(err)) { + *aShouldRepaint = true; + } else { + bool disabledBefore = + ShouldScrollbarButtonBeDisabled(oldCurpos, maxpos, aAppearance); + bool disabledNow = + ShouldScrollbarButtonBeDisabled(curpos, maxpos, aAppearance); + *aShouldRepaint = (disabledBefore != disabledNow); + } + } else { + *aShouldRepaint = true; + } + return NS_OK; + } + + // XXXdwh Not sure what can really be done here. Can at least guess for + // specific widgets that they're highly unlikely to have certain states. + // For example, a toolbar doesn't care about any states. + if (!aAttribute) { + // Hover/focus/active changed. Always repaint. + *aShouldRepaint = true; + } else { + // Check the attribute to see if it's relevant. + // disabled, checked, dlgtype, default, etc. + *aShouldRepaint = false; + if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked || + aAttribute == nsGkAtoms::selected || + aAttribute == nsGkAtoms::visuallyselected || + aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::readonly || + aAttribute == nsGkAtoms::_default || + aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::open || + aAttribute == nsGkAtoms::parentfocused) + *aShouldRepaint = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsNativeThemeGTK::ThemeChanged() { + memset(mDisabledWidgetTypes, 0, sizeof(mDisabledWidgetTypes)); + memset(mSafeWidgetStates, 0, sizeof(mSafeWidgetStates)); + memset(mBorderCacheValid, 0, sizeof(mBorderCacheValid)); + return NS_OK; +} + +NS_IMETHODIMP_(bool) +nsNativeThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext, + nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance)) { + return false; + } + + if (IsWidgetScrollbarPart(aAppearance)) { + ComputedStyle* cs = nsLayoutUtils::StyleForScrollbar(aFrame); + if (cs->StyleUI()->HasCustomScrollbars() || + // We cannot handle thin scrollbar on GTK+ widget directly as well. + cs->StyleUIReset()->mScrollbarWidth == StyleScrollbarWidth::Thin) { + return false; + } + } + + switch (aAppearance) { + // Combobox dropdowns don't support native theming in vertical mode. + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::MenulistText: + if (aFrame && aFrame->GetWritingMode().IsVertical()) { + return false; + } + [[fallthrough]]; + + case StyleAppearance::Button: + case StyleAppearance::ButtonFocus: + case StyleAppearance::Radio: + case StyleAppearance::Checkbox: + case StyleAppearance::Toolbox: // N/A + case StyleAppearance::Toolbar: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Dualbutton: // so we can override the border with 0 + case StyleAppearance::ToolbarbuttonDropdown: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::ButtonArrowNext: + case StyleAppearance::ButtonArrowPrevious: + case StyleAppearance::Separator: + case StyleAppearance::Toolbargripper: + case StyleAppearance::Statusbar: + case StyleAppearance::Statusbarpanel: + case StyleAppearance::Resizerpanel: + case StyleAppearance::Resizer: + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + // case StyleAppearance::Treeitem: + case StyleAppearance::Treetwisty: + // case StyleAppearance::Treeline: + // case StyleAppearance::Treeheader: + case StyleAppearance::Treeheadercell: + case StyleAppearance::Treeheadersortarrow: + case StyleAppearance::Treetwistyopen: + case StyleAppearance::ProgressBar: + case StyleAppearance::Progresschunk: + case StyleAppearance::Tab: + // case StyleAppearance::Tabpanel: + case StyleAppearance::Tabpanels: + case StyleAppearance::TabScrollArrowBack: + case StyleAppearance::TabScrollArrowForward: + case StyleAppearance::Tooltip: + case StyleAppearance::Spinner: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + case StyleAppearance::SpinnerTextfield: + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbartrackHorizontal: + case StyleAppearance::ScrollbartrackVertical: + case StyleAppearance::ScrollbarthumbHorizontal: + case StyleAppearance::ScrollbarthumbVertical: + case StyleAppearance::ScrollbarNonDisappearing: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Range: + case StyleAppearance::RangeThumb: + case StyleAppearance::CheckboxContainer: + case StyleAppearance::RadioContainer: + case StyleAppearance::CheckboxLabel: + case StyleAppearance::RadioLabel: + case StyleAppearance::Menubar: + case StyleAppearance::Menupopup: + case StyleAppearance::Menuitem: + case StyleAppearance::Menuarrow: + case StyleAppearance::Menuseparator: + case StyleAppearance::Checkmenuitem: + case StyleAppearance::Radiomenuitem: + case StyleAppearance::Splitter: + case StyleAppearance::Window: + case StyleAppearance::Dialog: + case StyleAppearance::MozGtkInfoBar: + case StyleAppearance::MozWindowButtonBox: + case StyleAppearance::MozWindowButtonClose: + case StyleAppearance::MozWindowButtonMinimize: + case StyleAppearance::MozWindowButtonMaximize: + case StyleAppearance::MozWindowButtonRestore: + case StyleAppearance::MozWindowTitlebar: + case StyleAppearance::MozWindowTitlebarMaximized: + return !IsWidgetStyled(aPresContext, aFrame, aAppearance); + + case StyleAppearance::MozMenulistArrowButton: + if (aFrame && aFrame->GetWritingMode().IsVertical()) { + return false; + } + // "Native" dropdown buttons cause padding and margin problems, but only + // in HTML so allow them in XUL. + return (!aFrame || + IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) && + !IsWidgetStyled(aPresContext, aFrame, aAppearance); + + case StyleAppearance::FocusOutline: + return true; + default: + break; + } + + return false; +} + +NS_IMETHODIMP_(bool) +nsNativeThemeGTK::WidgetIsContainer(StyleAppearance aAppearance) { + // XXXdwh At some point flesh all of this out. + if (aAppearance == StyleAppearance::MozMenulistArrowButton || + aAppearance == StyleAppearance::Radio || + aAppearance == StyleAppearance::RangeThumb || + aAppearance == StyleAppearance::Checkbox || + aAppearance == StyleAppearance::TabScrollArrowBack || + aAppearance == StyleAppearance::TabScrollArrowForward || + aAppearance == StyleAppearance::ButtonArrowUp || + aAppearance == StyleAppearance::ButtonArrowDown || + aAppearance == StyleAppearance::ButtonArrowNext || + aAppearance == StyleAppearance::ButtonArrowPrevious) + return false; + return true; +} + +bool nsNativeThemeGTK::ThemeDrawsFocusForWidget(StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::Button: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Textarea: + case StyleAppearance::Textfield: + case StyleAppearance::Treeheadercell: + case StyleAppearance::NumberInput: + return true; + default: + return false; + } +} + +bool nsNativeThemeGTK::ThemeNeedsComboboxDropmarker() { return false; } + +nsITheme::Transparency nsNativeThemeGTK::GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) { + switch (aAppearance) { + // These widgets always draw a default background. + case StyleAppearance::Menupopup: + case StyleAppearance::Window: + case StyleAppearance::Dialog: + return eOpaque; + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbarHorizontal: + // Make scrollbar tracks opaque on the window's scroll frame to prevent + // leaf layers from overlapping. See bug 1179780. + if (!(CheckBooleanAttr(aFrame, nsGkAtoms::root_) && + aFrame->PresContext()->IsRootContentDocument() && + IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL))) { + return eTransparent; + } + return eOpaque; + // Tooltips use gtk_paint_flat_box() on Gtk2 + // but are shaped on Gtk3 + case StyleAppearance::Tooltip: + return eTransparent; + default: + return eUnknownTransparency; + } +} + +bool nsNativeThemeGTK::WidgetAppearanceDependsOnWindowFocus( + StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbartrackHorizontal: + case StyleAppearance::ScrollbartrackVertical: + case StyleAppearance::ScrollbarthumbVertical: + case StyleAppearance::ScrollbarthumbHorizontal: + return true; + default: + return false; + } +} + +already_AddRefed<nsITheme> do_GetNativeThemeDoNotUseDirectly() { + static nsCOMPtr<nsITheme> inst; + + if (!inst) { + if (gfxPlatform::IsHeadless()) { + inst = new HeadlessThemeGTK(); + } else { + inst = new nsNativeThemeGTK(); + } + ClearOnShutdown(&inst); + } + + return do_AddRef(inst); +} diff --git a/widget/gtk/nsNativeThemeGTK.h b/widget/gtk/nsNativeThemeGTK.h new file mode 100644 index 0000000000..8b86625bda --- /dev/null +++ b/widget/gtk/nsNativeThemeGTK.h @@ -0,0 +1,116 @@ +/* -*- 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 _GTK_NSNATIVETHEMEGTK_H_ +#define _GTK_NSNATIVETHEMEGTK_H_ + +#include "nsITheme.h" +#include "nsCOMPtr.h" +#include "nsAtom.h" +#include "nsIObserver.h" +#include "nsNativeTheme.h" +#include "nsStyleConsts.h" + +#include <gtk/gtk.h> +#include "gtkdrawing.h" + +class nsNativeThemeGTK final : private nsNativeTheme, + public nsITheme, + public nsIObserver { + public: + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIOBSERVER + + // The nsITheme interface. + NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, + const nsRect& aDirtyRect) override; + + bool CreateWebRenderCommandsForWidget( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame, + StyleAppearance aAppearance, const nsRect& aRect) override; + + [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance) override; + + bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) override; + + virtual bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) override; + + NS_IMETHOD GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + mozilla::LayoutDeviceIntSize* aResult, + bool* aIsOverridable) override; + + NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance, + nsAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) override; + + NS_IMETHOD ThemeChanged() override; + + NS_IMETHOD_(bool) + ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) override; + + NS_IMETHOD_(bool) WidgetIsContainer(StyleAppearance aAppearance) override; + + NS_IMETHOD_(bool) + ThemeDrawsFocusForWidget(StyleAppearance aAppearance) override; + + virtual bool ThemeNeedsComboboxDropmarker() override; + + virtual Transparency GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) override; + + virtual bool WidgetAppearanceDependsOnWindowFocus( + StyleAppearance aAppearance) override; + + nsNativeThemeGTK(); + + protected: + virtual ~nsNativeThemeGTK(); + + private: + GtkTextDirection GetTextDirection(nsIFrame* aFrame); + gint GetTabMarginPixels(nsIFrame* aFrame); + bool GetGtkWidgetAndState(StyleAppearance aAppearance, nsIFrame* aFrame, + WidgetNodeType& aGtkWidgetType, + GtkWidgetState* aState, gint* aWidgetFlags); + bool GetExtraSizeForWidget(nsIFrame* aFrame, StyleAppearance aAppearance, + nsIntMargin* aExtra); + bool IsWidgetVisible(StyleAppearance aAppearance); + + void RefreshWidgetWindow(nsIFrame* aFrame); + WidgetNodeType NativeThemeToGtkTheme(StyleAppearance aAppearance, + nsIFrame* aFrame); + + uint8_t mDisabledWidgetTypes + [(static_cast<size_t>(mozilla::StyleAppearance::Count) + 7) / 8]; + uint8_t + mSafeWidgetStates[static_cast<size_t>(mozilla::StyleAppearance::Count) * + 4]; // 32 bits per widget + static const char* sDisabledEngines[]; + + // Because moz_gtk_get_widget_border can be slow, we cache its results + // by widget type. Each bit in mBorderCacheValid says whether the + // corresponding entry in mBorderCache is valid. + void GetCachedWidgetBorder(nsIFrame* aFrame, StyleAppearance aAppearance, + GtkTextDirection aDirection, + LayoutDeviceIntMargin* aResult); + uint8_t mBorderCacheValid[(MOZ_GTK_WIDGET_NODE_COUNT + 7) / 8]; + LayoutDeviceIntMargin mBorderCache[MOZ_GTK_WIDGET_NODE_COUNT]; +}; + +#endif diff --git a/widget/gtk/nsPrintDialogGTK.cpp b/widget/gtk/nsPrintDialogGTK.cpp new file mode 100644 index 0000000000..9c1a72a6e3 --- /dev/null +++ b/widget/gtk/nsPrintDialogGTK.cpp @@ -0,0 +1,1054 @@ +/* -*- 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 <gtk/gtk.h> +#include <gtk/gtkunixprint.h> +#include <stdlib.h> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Services.h" + +#include "MozContainer.h" +#include "nsIPrintSettings.h" +#include "nsIWidget.h" +#include "nsPrintDialogGTK.h" +#include "nsPrintSettingsGTK.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsIStringBundle.h" +#include "nsIPrintSettingsService.h" +#include "nsPIDOMWindow.h" +#include "nsIGIOService.h" +#include "WidgetUtils.h" +#include "nsIObserverService.h" + +// for gdk_x11_window_get_xid +#include <gdk/gdkx.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <gio/gunixfdlist.h> +#include "gfxPlatformGtk.h" + +// for dlsym +#include <dlfcn.h> +#include "MainThreadUtils.h" + +using namespace mozilla; +using namespace mozilla::widget; + +static const char header_footer_tags[][4] = {"", "&T", "&U", "&D", "&P", "&PT"}; + +#define CUSTOM_VALUE_INDEX gint(ArrayLength(header_footer_tags)) + +static GtkWindow* get_gtk_window_for_nsiwidget(nsIWidget* widget) { + return GTK_WINDOW(widget->GetNativeData(NS_NATIVE_SHELLWIDGET)); +} + +static void ShowCustomDialog(GtkComboBox* changed_box, gpointer user_data) { + if (gtk_combo_box_get_active(changed_box) != CUSTOM_VALUE_INDEX) { + g_object_set_data(G_OBJECT(changed_box), "previous-active", + GINT_TO_POINTER(gtk_combo_box_get_active(changed_box))); + return; + } + + GtkWindow* printDialog = GTK_WINDOW(user_data); + nsCOMPtr<nsIStringBundleService> bundleSvc = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + + nsCOMPtr<nsIStringBundle> printBundle; + bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties", + getter_AddRefs(printBundle)); + nsAutoString intlString; + + printBundle->GetStringFromName("headerFooterCustom", intlString); + GtkWidget* prompt_dialog = gtk_dialog_new_with_buttons( + NS_ConvertUTF16toUTF8(intlString).get(), printDialog, + (GtkDialogFlags)(GTK_DIALOG_MODAL), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, + GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, nullptr); + gtk_dialog_set_default_response(GTK_DIALOG(prompt_dialog), + GTK_RESPONSE_ACCEPT); + gtk_dialog_set_alternative_button_order( + GTK_DIALOG(prompt_dialog), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_REJECT, -1); + + printBundle->GetStringFromName("customHeaderFooterPrompt", intlString); + GtkWidget* custom_label = + gtk_label_new(NS_ConvertUTF16toUTF8(intlString).get()); + GtkWidget* custom_entry = gtk_entry_new(); + GtkWidget* question_icon = + gtk_image_new_from_stock(GTK_STOCK_DIALOG_QUESTION, GTK_ICON_SIZE_DIALOG); + + // To be convenient, prefill the textbox with the existing value, if any, and + // select it all so they can easily both edit it and type in a new one. + const char* current_text = + (const char*)g_object_get_data(G_OBJECT(changed_box), "custom-text"); + if (current_text) { + gtk_entry_set_text(GTK_ENTRY(custom_entry), current_text); + gtk_editable_select_region(GTK_EDITABLE(custom_entry), 0, -1); + } + gtk_entry_set_activates_default(GTK_ENTRY(custom_entry), TRUE); + + GtkWidget* custom_vbox = gtk_vbox_new(TRUE, 2); + gtk_box_pack_start(GTK_BOX(custom_vbox), custom_label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(custom_vbox), custom_entry, FALSE, FALSE, + 5); // Make entry 5px underneath label + GtkWidget* custom_hbox = gtk_hbox_new(FALSE, 2); + gtk_box_pack_start(GTK_BOX(custom_hbox), question_icon, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(custom_hbox), custom_vbox, FALSE, FALSE, + 10); // Make question icon 10px away from content + gtk_container_set_border_width(GTK_CONTAINER(custom_hbox), 2); + gtk_widget_show_all(custom_hbox); + + gtk_box_pack_start( + GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(prompt_dialog))), + custom_hbox, FALSE, FALSE, 0); + gint diag_response = gtk_dialog_run(GTK_DIALOG(prompt_dialog)); + + if (diag_response == GTK_RESPONSE_ACCEPT) { + const gchar* response_text = gtk_entry_get_text(GTK_ENTRY(custom_entry)); + g_object_set_data_full(G_OBJECT(changed_box), "custom-text", + strdup(response_text), (GDestroyNotify)free); + g_object_set_data(G_OBJECT(changed_box), "previous-active", + GINT_TO_POINTER(CUSTOM_VALUE_INDEX)); + } else { + // Go back to the previous index + gint previous_active = GPOINTER_TO_INT( + g_object_get_data(G_OBJECT(changed_box), "previous-active")); + gtk_combo_box_set_active(changed_box, previous_active); + } + + gtk_widget_destroy(prompt_dialog); +} + +class nsPrintDialogWidgetGTK { + public: + nsPrintDialogWidgetGTK(nsPIDOMWindowOuter* aParent, + nsIPrintSettings* aPrintSettings); + ~nsPrintDialogWidgetGTK() { gtk_widget_destroy(dialog); } + NS_ConvertUTF16toUTF8 GetUTF8FromBundle(const char* aKey); + gint Run(); + + nsresult ImportSettings(nsIPrintSettings* aNSSettings); + nsresult ExportSettings(nsIPrintSettings* aNSSettings); + + private: + GtkWidget* dialog; + GtkWidget* shrink_to_fit_toggle; + GtkWidget* print_bg_colors_toggle; + GtkWidget* print_bg_images_toggle; + GtkWidget* selection_only_toggle; + GtkWidget* header_dropdown[3]; // {left, center, right} + GtkWidget* footer_dropdown[3]; + + nsCOMPtr<nsIStringBundle> printBundle; + + bool useNativeSelection; + + GtkWidget* ConstructHeaderFooterDropdown(const char16_t* currentString); + const char* OptionWidgetToString(GtkWidget* dropdown); + + /* Code to copy between GTK and NS print settings structures. + * In the following, + * "Import" means to copy from NS to GTK + * "Export" means to copy from GTK to NS + */ + void ExportHeaderFooter(nsIPrintSettings* aNS); +}; + +nsPrintDialogWidgetGTK::nsPrintDialogWidgetGTK(nsPIDOMWindowOuter* aParent, + nsIPrintSettings* aSettings) { + nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aParent); + NS_ASSERTION(widget, "Need a widget for dialog to be modal."); + GtkWindow* gtkParent = get_gtk_window_for_nsiwidget(widget); + NS_ASSERTION(gtkParent, "Need a GTK window for dialog to be modal."); + + nsCOMPtr<nsIStringBundleService> bundleSvc = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties", + getter_AddRefs(printBundle)); + + dialog = gtk_print_unix_dialog_new(GetUTF8FromBundle("printTitleGTK").get(), + gtkParent); + + gtk_print_unix_dialog_set_manual_capabilities( + GTK_PRINT_UNIX_DIALOG(dialog), + GtkPrintCapabilities( + GTK_PRINT_CAPABILITY_COPIES | GTK_PRINT_CAPABILITY_COLLATE | + GTK_PRINT_CAPABILITY_REVERSE | GTK_PRINT_CAPABILITY_SCALE | + GTK_PRINT_CAPABILITY_GENERATE_PDF)); + + // The vast majority of magic numbers in this widget construction are padding. + // e.g. for the set_border_width below, 12px matches that of just about every + // other window. + GtkWidget* custom_options_tab = gtk_vbox_new(FALSE, 0); + gtk_container_set_border_width(GTK_CONTAINER(custom_options_tab), 12); + GtkWidget* tab_label = + gtk_label_new(GetUTF8FromBundle("optionsTabLabelGTK").get()); + + // Check buttons for shrink-to-fit and print selection + GtkWidget* check_buttons_container = gtk_vbox_new(TRUE, 2); + shrink_to_fit_toggle = gtk_check_button_new_with_mnemonic( + GetUTF8FromBundle("shrinkToFit").get()); + gtk_box_pack_start(GTK_BOX(check_buttons_container), shrink_to_fit_toggle, + FALSE, FALSE, 0); + + // GTK+2.18 and above allow us to add a "Selection" option to the main + // settings screen, rather than adding an option on a custom tab like we must + // do on older versions. + bool canSelectText = aSettings->GetIsPrintSelectionRBEnabled(); + if (gtk_major_version > 2 || + (gtk_major_version == 2 && gtk_minor_version >= 18)) { + useNativeSelection = true; + g_object_set(dialog, "support-selection", TRUE, "has-selection", + canSelectText, "embed-page-setup", TRUE, nullptr); + } else { + useNativeSelection = false; + selection_only_toggle = gtk_check_button_new_with_mnemonic( + GetUTF8FromBundle("selectionOnly").get()); + gtk_widget_set_sensitive(selection_only_toggle, canSelectText); + gtk_box_pack_start(GTK_BOX(check_buttons_container), selection_only_toggle, + FALSE, FALSE, 0); + } + + // Check buttons for printing background + GtkWidget* appearance_buttons_container = gtk_vbox_new(TRUE, 2); + print_bg_colors_toggle = gtk_check_button_new_with_mnemonic( + GetUTF8FromBundle("printBGColors").get()); + print_bg_images_toggle = gtk_check_button_new_with_mnemonic( + GetUTF8FromBundle("printBGImages").get()); + gtk_box_pack_start(GTK_BOX(appearance_buttons_container), + print_bg_colors_toggle, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(appearance_buttons_container), + print_bg_images_toggle, FALSE, FALSE, 0); + + // "Appearance" options label, bold and center-aligned + GtkWidget* appearance_label = gtk_label_new(nullptr); + char* pangoMarkup = g_markup_printf_escaped( + "<b>%s</b>", GetUTF8FromBundle("printBGOptions").get()); + gtk_label_set_markup(GTK_LABEL(appearance_label), pangoMarkup); + g_free(pangoMarkup); + gtk_misc_set_alignment(GTK_MISC(appearance_label), 0, 0); + + GtkWidget* appearance_container = gtk_alignment_new(0, 0, 0, 0); + gtk_alignment_set_padding(GTK_ALIGNMENT(appearance_container), 8, 0, 12, 0); + gtk_container_add(GTK_CONTAINER(appearance_container), + appearance_buttons_container); + + GtkWidget* appearance_vertical_squasher = gtk_vbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(appearance_vertical_squasher), appearance_label, + FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(appearance_vertical_squasher), + appearance_container, FALSE, FALSE, 0); + + // "Header & Footer" options label, bold and center-aligned + GtkWidget* header_footer_label = gtk_label_new(nullptr); + pangoMarkup = g_markup_printf_escaped( + "<b>%s</b>", GetUTF8FromBundle("headerFooter").get()); + gtk_label_set_markup(GTK_LABEL(header_footer_label), pangoMarkup); + g_free(pangoMarkup); + gtk_misc_set_alignment(GTK_MISC(header_footer_label), 0, 0); + + GtkWidget* header_footer_container = gtk_alignment_new(0, 0, 0, 0); + gtk_alignment_set_padding(GTK_ALIGNMENT(header_footer_container), 8, 0, 12, + 0); + + // --- Table for making the header and footer options --- + GtkWidget* header_footer_table = gtk_table_new(3, 3, FALSE); // 3x3 table + nsString header_footer_str[3]; + + aSettings->GetHeaderStrLeft(header_footer_str[0]); + aSettings->GetHeaderStrCenter(header_footer_str[1]); + aSettings->GetHeaderStrRight(header_footer_str[2]); + + for (unsigned int i = 0; i < ArrayLength(header_dropdown); i++) { + header_dropdown[i] = + ConstructHeaderFooterDropdown(header_footer_str[i].get()); + // Those 4 magic numbers in the middle provide the position in the table. + // The last two numbers mean 2 px padding on every side. + gtk_table_attach(GTK_TABLE(header_footer_table), header_dropdown[i], i, + (i + 1), 0, 1, (GtkAttachOptions)0, (GtkAttachOptions)0, 2, + 2); + } + + const char labelKeys[][7] = {"left", "center", "right"}; + for (unsigned int i = 0; i < ArrayLength(labelKeys); i++) { + gtk_table_attach(GTK_TABLE(header_footer_table), + gtk_label_new(GetUTF8FromBundle(labelKeys[i]).get()), i, + (i + 1), 1, 2, (GtkAttachOptions)0, (GtkAttachOptions)0, 2, + 2); + } + + aSettings->GetFooterStrLeft(header_footer_str[0]); + aSettings->GetFooterStrCenter(header_footer_str[1]); + aSettings->GetFooterStrRight(header_footer_str[2]); + + for (unsigned int i = 0; i < ArrayLength(footer_dropdown); i++) { + footer_dropdown[i] = + ConstructHeaderFooterDropdown(header_footer_str[i].get()); + gtk_table_attach(GTK_TABLE(header_footer_table), footer_dropdown[i], i, + (i + 1), 2, 3, (GtkAttachOptions)0, (GtkAttachOptions)0, 2, + 2); + } + // --- + + gtk_container_add(GTK_CONTAINER(header_footer_container), + header_footer_table); + + GtkWidget* header_footer_vertical_squasher = gtk_vbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(header_footer_vertical_squasher), + header_footer_label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(header_footer_vertical_squasher), + header_footer_container, FALSE, FALSE, 0); + + // Construction of everything + gtk_box_pack_start(GTK_BOX(custom_options_tab), check_buttons_container, + FALSE, FALSE, 10); // 10px padding + gtk_box_pack_start(GTK_BOX(custom_options_tab), appearance_vertical_squasher, + FALSE, FALSE, 10); + gtk_box_pack_start(GTK_BOX(custom_options_tab), + header_footer_vertical_squasher, FALSE, FALSE, 0); + + gtk_print_unix_dialog_add_custom_tab(GTK_PRINT_UNIX_DIALOG(dialog), + custom_options_tab, tab_label); + gtk_widget_show_all(custom_options_tab); +} + +NS_ConvertUTF16toUTF8 nsPrintDialogWidgetGTK::GetUTF8FromBundle( + const char* aKey) { + nsAutoString intlString; + printBundle->GetStringFromName(aKey, intlString); + return NS_ConvertUTF16toUTF8( + intlString); // Return the actual object so we don't lose reference +} + +const char* nsPrintDialogWidgetGTK::OptionWidgetToString(GtkWidget* dropdown) { + gint index = gtk_combo_box_get_active(GTK_COMBO_BOX(dropdown)); + + NS_ASSERTION(index <= CUSTOM_VALUE_INDEX, + "Index of dropdown is higher than expected!"); + + if (index == CUSTOM_VALUE_INDEX) + return (const char*)g_object_get_data(G_OBJECT(dropdown), "custom-text"); + else + return header_footer_tags[index]; +} + +gint nsPrintDialogWidgetGTK::Run() { + const gint response = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_hide(dialog); + return response; +} + +void nsPrintDialogWidgetGTK::ExportHeaderFooter(nsIPrintSettings* aNS) { + const char* header_footer_str; + header_footer_str = OptionWidgetToString(header_dropdown[0]); + aNS->SetHeaderStrLeft(NS_ConvertUTF8toUTF16(header_footer_str)); + + header_footer_str = OptionWidgetToString(header_dropdown[1]); + aNS->SetHeaderStrCenter(NS_ConvertUTF8toUTF16(header_footer_str)); + + header_footer_str = OptionWidgetToString(header_dropdown[2]); + aNS->SetHeaderStrRight(NS_ConvertUTF8toUTF16(header_footer_str)); + + header_footer_str = OptionWidgetToString(footer_dropdown[0]); + aNS->SetFooterStrLeft(NS_ConvertUTF8toUTF16(header_footer_str)); + + header_footer_str = OptionWidgetToString(footer_dropdown[1]); + aNS->SetFooterStrCenter(NS_ConvertUTF8toUTF16(header_footer_str)); + + header_footer_str = OptionWidgetToString(footer_dropdown[2]); + aNS->SetFooterStrRight(NS_ConvertUTF8toUTF16(header_footer_str)); +} + +nsresult nsPrintDialogWidgetGTK::ImportSettings(nsIPrintSettings* aNSSettings) { + MOZ_ASSERT(aNSSettings, "aSettings must not be null"); + NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE); + + nsCOMPtr<nsPrintSettingsGTK> aNSSettingsGTK(do_QueryInterface(aNSSettings)); + if (!aNSSettingsGTK) return NS_ERROR_FAILURE; + + GtkPrintSettings* settings = aNSSettingsGTK->GetGtkPrintSettings(); + GtkPageSetup* setup = aNSSettingsGTK->GetGtkPageSetup(); + + bool geckoBool; + aNSSettings->GetShrinkToFit(&geckoBool); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(shrink_to_fit_toggle), + geckoBool); + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_bg_colors_toggle), + aNSSettings->GetPrintBGColors()); + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_bg_images_toggle), + aNSSettings->GetPrintBGImages()); + + gtk_print_unix_dialog_set_settings(GTK_PRINT_UNIX_DIALOG(dialog), settings); + gtk_print_unix_dialog_set_page_setup(GTK_PRINT_UNIX_DIALOG(dialog), setup); + + return NS_OK; +} + +nsresult nsPrintDialogWidgetGTK::ExportSettings(nsIPrintSettings* aNSSettings) { + MOZ_ASSERT(aNSSettings, "aSettings must not be null"); + NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE); + + GtkPrintSettings* settings = + gtk_print_unix_dialog_get_settings(GTK_PRINT_UNIX_DIALOG(dialog)); + GtkPageSetup* setup = + gtk_print_unix_dialog_get_page_setup(GTK_PRINT_UNIX_DIALOG(dialog)); + GtkPrinter* printer = + gtk_print_unix_dialog_get_selected_printer(GTK_PRINT_UNIX_DIALOG(dialog)); + if (settings && setup && printer) { + ExportHeaderFooter(aNSSettings); + + aNSSettings->SetOutputFormat(nsIPrintSettings::kOutputFormatNative); + + // Print-to-file is true by default. This must be turned off or else + // printing won't occur! (We manually copy the spool file when this flag is + // set, because we love our embedders) Even if it is print-to-file in GTK's + // case, GTK does The Right Thing when we send the job. + aNSSettings->SetPrintToFile(false); + + aNSSettings->SetShrinkToFit( + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(shrink_to_fit_toggle))); + + aNSSettings->SetPrintBGColors(gtk_toggle_button_get_active( + GTK_TOGGLE_BUTTON(print_bg_colors_toggle))); + aNSSettings->SetPrintBGImages(gtk_toggle_button_get_active( + GTK_TOGGLE_BUTTON(print_bg_images_toggle))); + + // Try to save native settings in the session object + nsCOMPtr<nsPrintSettingsGTK> aNSSettingsGTK(do_QueryInterface(aNSSettings)); + if (aNSSettingsGTK) { + aNSSettingsGTK->SetGtkPrintSettings(settings); + aNSSettingsGTK->SetGtkPageSetup(setup); + aNSSettingsGTK->SetGtkPrinter(printer); + bool printSelectionOnly; + if (useNativeSelection) { + _GtkPrintPages pageSetting = + (_GtkPrintPages)gtk_print_settings_get_print_pages(settings); + printSelectionOnly = (pageSetting == _GTK_PRINT_PAGES_SELECTION); + } else { + printSelectionOnly = gtk_toggle_button_get_active( + GTK_TOGGLE_BUTTON(selection_only_toggle)); + } + aNSSettingsGTK->SetPrintSelectionOnly(printSelectionOnly); + } + } + + if (settings) g_object_unref(settings); + return NS_OK; +} + +GtkWidget* nsPrintDialogWidgetGTK::ConstructHeaderFooterDropdown( + const char16_t* currentString) { + GtkWidget* dropdown = gtk_combo_box_text_new(); + const char hf_options[][22] = {"headerFooterBlank", "headerFooterTitle", + "headerFooterURL", "headerFooterDate", + "headerFooterPage", "headerFooterPageTotal", + "headerFooterCustom"}; + + for (unsigned int i = 0; i < ArrayLength(hf_options); i++) { + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(dropdown), nullptr, + GetUTF8FromBundle(hf_options[i]).get()); + } + + bool shouldBeCustom = true; + NS_ConvertUTF16toUTF8 currentStringUTF8(currentString); + + for (unsigned int i = 0; i < ArrayLength(header_footer_tags); i++) { + if (!strcmp(currentStringUTF8.get(), header_footer_tags[i])) { + gtk_combo_box_set_active(GTK_COMBO_BOX(dropdown), i); + g_object_set_data(G_OBJECT(dropdown), "previous-active", + GINT_TO_POINTER(i)); + shouldBeCustom = false; + break; + } + } + + if (shouldBeCustom) { + gtk_combo_box_set_active(GTK_COMBO_BOX(dropdown), CUSTOM_VALUE_INDEX); + g_object_set_data(G_OBJECT(dropdown), "previous-active", + GINT_TO_POINTER(CUSTOM_VALUE_INDEX)); + char* custom_string = strdup(currentStringUTF8.get()); + g_object_set_data_full(G_OBJECT(dropdown), "custom-text", custom_string, + (GDestroyNotify)free); + } + + g_signal_connect(dropdown, "changed", (GCallback)ShowCustomDialog, dialog); + return dropdown; +} + +NS_IMPL_ISUPPORTS(nsPrintDialogServiceGTK, nsIPrintDialogService) + +nsPrintDialogServiceGTK::nsPrintDialogServiceGTK() = default; + +nsPrintDialogServiceGTK::~nsPrintDialogServiceGTK() = default; + +NS_IMETHODIMP +nsPrintDialogServiceGTK::Init() { return NS_OK; } + +// Used to obtain window handle. The portal use this handle +// to ensure that print dialog is modal. +typedef void (*WindowHandleExported)(GtkWindow* window, const char* handle, + gpointer user_data); + +typedef void (*GtkWindowHandleExported)(GtkWindow* window, const char* handle, + gpointer user_data); +#ifdef MOZ_WAYLAND +# if !GTK_CHECK_VERSION(3, 22, 0) +typedef void (*GdkWaylandWindowExported)(GdkWindow* window, const char* handle, + gpointer user_data); +# endif + +typedef struct { + GtkWindow* window; + WindowHandleExported callback; + gpointer user_data; +} WaylandWindowHandleExportedData; + +static void wayland_window_handle_exported(GdkWindow* window, + const char* wayland_handle_str, + gpointer user_data) { + WaylandWindowHandleExportedData* data = + static_cast<WaylandWindowHandleExportedData*>(user_data); + char* handle_str; + + handle_str = g_strdup_printf("wayland:%s", wayland_handle_str); + data->callback(data->window, handle_str, data->user_data); + g_free(handle_str); +} +#endif + +// Get window handle for the portal, taken from gtk/gtkwindow.c +// (currently not exported) +static gboolean window_export_handle(GtkWindow* window, + GtkWindowHandleExported callback, + gpointer user_data) { + if (gfxPlatformGtk::GetPlatform()->IsX11Display()) { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + char* handle_str; + guint32 xid = (guint32)gdk_x11_window_get_xid(gdk_window); + + handle_str = g_strdup_printf("x11:%x", xid); + callback(window, handle_str, user_data); + g_free(handle_str); + return true; + } +#ifdef MOZ_WAYLAND + else { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + WaylandWindowHandleExportedData* data; + + data = g_new0(WaylandWindowHandleExportedData, 1); + data->window = window; + data->callback = callback; + data->user_data = user_data; + + static auto s_gdk_wayland_window_export_handle = + reinterpret_cast<gboolean (*)(GdkWindow*, GdkWaylandWindowExported, + gpointer, GDestroyNotify)>( + dlsym(RTLD_DEFAULT, "gdk_wayland_window_export_handle")); + if (!s_gdk_wayland_window_export_handle || + !s_gdk_wayland_window_export_handle( + gdk_window, wayland_window_handle_exported, data, g_free)) { + g_free(data); + return false; + } else { + return true; + } + } +#endif + + g_warning("Couldn't export handle, unsupported windowing system"); + + return false; +} +/** + * Communication class with the GTK print portal handler + * + * To print document from flatpak we need to use print portal because + * printers are not directly accessible in the sandboxed environment. + * + * At first we request portal to show the print dialog to let user choose + * printer settings. We use DBUS interface for that (PreparePrint method). + * + * Next we force application to print to temporary file and after the writing + * to the file is finished we pass its file descriptor to the portal. + * Portal will pass duplicate of the file descriptor to the printer which + * user selected before (by DBUS Print method). + * + * Since DBUS communication is done async while nsPrintDialogServiceGTK::Show + * is expecting sync execution, we need to create a new GMainLoop during the + * print portal dialog is running. The loop is stopped after the dialog + * is closed. + */ +class nsFlatpakPrintPortal : public nsIObserver { + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + public: + explicit nsFlatpakPrintPortal(nsPrintSettingsGTK* aPrintSettings); + nsresult PreparePrintRequest(GtkWindow* aWindow); + static void OnWindowExportHandleDone(GtkWindow* aWindow, + const char* aWindowHandleStr, + gpointer aUserData); + void PreparePrint(GtkWindow* aWindow, const char* aWindowHandleStr); + static void OnPreparePrintResponse(GDBusConnection* connection, + const char* sender_name, + const char* object_path, + const char* interface_name, + const char* signal_name, + GVariant* parameters, gpointer data); + GtkPrintOperationResult GetResult(); + + private: + virtual ~nsFlatpakPrintPortal(); + void FinishPrintDialog(GVariant* parameters); + nsCOMPtr<nsPrintSettingsGTK> mPrintAndPageSettings; + GDBusProxy* mProxy; + guint32 mToken; + GMainLoop* mLoop; + GtkPrintOperationResult mResult; + guint mResponseSignalId; + GtkWindow* mParentWindow; +}; + +NS_IMPL_ISUPPORTS(nsFlatpakPrintPortal, nsIObserver) + +nsFlatpakPrintPortal::nsFlatpakPrintPortal(nsPrintSettingsGTK* aPrintSettings) + : mPrintAndPageSettings(aPrintSettings), + mProxy(nullptr), + mLoop(nullptr), + mResponseSignalId(0), + mParentWindow(nullptr) {} + +/** + * Creates GDBusProxy, query for window handle and create a new GMainLoop. + * + * The GMainLoop is to be run from GetResult() and be quitted during + * FinishPrintDialog. + * + * @param aWindow toplevel application window which is used as parent of print + * dialog + */ +nsresult nsFlatpakPrintPortal::PreparePrintRequest(GtkWindow* aWindow) { + MOZ_ASSERT(aWindow, "aWindow must not be null"); + MOZ_ASSERT(mPrintAndPageSettings, "mPrintAndPageSettings must not be null"); + + GError* error = nullptr; + mProxy = g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr, + "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Print", nullptr, &error); + if (mProxy == nullptr) { + NS_WARNING( + nsPrintfCString("Unable to create dbus proxy: %s", error->message) + .get()); + g_error_free(error); + return NS_ERROR_FAILURE; + } + + // The window handler is returned async, we will continue by PreparePrint + // method when it is returned. + if (!window_export_handle( + aWindow, &nsFlatpakPrintPortal::OnWindowExportHandleDone, this)) { + NS_WARNING("Unable to get window handle for creating modal print dialog."); + return NS_ERROR_FAILURE; + } + + mLoop = g_main_loop_new(NULL, FALSE); + return NS_OK; +} + +void nsFlatpakPrintPortal::OnWindowExportHandleDone( + GtkWindow* aWindow, const char* aWindowHandleStr, gpointer aUserData) { + nsFlatpakPrintPortal* printPortal = + static_cast<nsFlatpakPrintPortal*>(aUserData); + printPortal->PreparePrint(aWindow, aWindowHandleStr); +} + +/** + * Ask print portal to show the print dialog. + * + * Print and page settings and window handle are passed to the portal to prefill + * last used settings. + */ +void nsFlatpakPrintPortal::PreparePrint(GtkWindow* aWindow, + const char* aWindowHandleStr) { + GtkPrintSettings* gtkSettings = mPrintAndPageSettings->GetGtkPrintSettings(); + GtkPageSetup* pageSetup = mPrintAndPageSettings->GetGtkPageSetup(); + + // We need to remember GtkWindow to unexport window handle after it is + // no longer needed by the portal dialog (apply only on non-X11 sessions). + if (gfxPlatformGtk::GetPlatform()->IsWaylandDisplay()) { + mParentWindow = aWindow; + } + + GVariantBuilder opt_builder; + g_variant_builder_init(&opt_builder, G_VARIANT_TYPE_VARDICT); + char* token = g_strdup_printf("mozilla%d", g_random_int_range(0, G_MAXINT)); + g_variant_builder_add(&opt_builder, "{sv}", "handle_token", + g_variant_new_string(token)); + g_free(token); + GVariant* options = g_variant_builder_end(&opt_builder); + static auto s_gtk_print_settings_to_gvariant = + reinterpret_cast<GVariant* (*)(GtkPrintSettings*)>( + dlsym(RTLD_DEFAULT, "gtk_print_settings_to_gvariant")); + static auto s_gtk_page_setup_to_gvariant = + reinterpret_cast<GVariant* (*)(GtkPageSetup*)>( + dlsym(RTLD_DEFAULT, "gtk_page_setup_to_gvariant")); + if (!s_gtk_print_settings_to_gvariant || !s_gtk_page_setup_to_gvariant) { + mResult = GTK_PRINT_OPERATION_RESULT_ERROR; + FinishPrintDialog(nullptr); + return; + } + + // Get translated window title + nsCOMPtr<nsIStringBundleService> bundleSvc = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + nsCOMPtr<nsIStringBundle> printBundle; + bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties", + getter_AddRefs(printBundle)); + nsAutoString intlPrintTitle; + printBundle->GetStringFromName("printTitleGTK", intlPrintTitle); + + GError* error = nullptr; + GVariant* ret = g_dbus_proxy_call_sync( + mProxy, "PreparePrint", + g_variant_new( + "(ss@a{sv}@a{sv}@a{sv})", aWindowHandleStr, + NS_ConvertUTF16toUTF8(intlPrintTitle).get(), // Title of the window + s_gtk_print_settings_to_gvariant(gtkSettings), + s_gtk_page_setup_to_gvariant(pageSetup), options), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (ret == nullptr) { + NS_WARNING( + nsPrintfCString("Unable to call dbus proxy: %s", error->message).get()); + g_error_free(error); + mResult = GTK_PRINT_OPERATION_RESULT_ERROR; + FinishPrintDialog(nullptr); + return; + } + + const char* handle = nullptr; + g_variant_get(ret, "(&o)", &handle); + if (strcmp(aWindowHandleStr, handle) != 0) { + aWindowHandleStr = g_strdup(handle); + if (mResponseSignalId) { + g_dbus_connection_signal_unsubscribe( + g_dbus_proxy_get_connection(G_DBUS_PROXY(mProxy)), mResponseSignalId); + } + } + mResponseSignalId = g_dbus_connection_signal_subscribe( + g_dbus_proxy_get_connection(G_DBUS_PROXY(mProxy)), + "org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request", + "Response", aWindowHandleStr, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + &nsFlatpakPrintPortal::OnPreparePrintResponse, this, NULL); +} + +void nsFlatpakPrintPortal::OnPreparePrintResponse( + GDBusConnection* connection, const char* sender_name, + const char* object_path, const char* interface_name, + const char* signal_name, GVariant* parameters, gpointer data) { + nsFlatpakPrintPortal* printPortal = static_cast<nsFlatpakPrintPortal*>(data); + printPortal->FinishPrintDialog(parameters); +} + +/** + * When the dialog is accepted, read print and page settings and token. + * + * Token is later used for printing portal as print operation identifier. + * Print and page settings are modified in-place and stored to + * mPrintAndPageSettings. + */ +void nsFlatpakPrintPortal::FinishPrintDialog(GVariant* parameters) { + // This ends GetResult() method + if (mLoop) { + g_main_loop_quit(mLoop); + mLoop = nullptr; + } + + if (!parameters) { + // mResult should be already defined + return; + } + + guint32 response; + GVariant* options; + + g_variant_get(parameters, "(u@a{sv})", &response, &options); + mResult = GTK_PRINT_OPERATION_RESULT_CANCEL; + if (response == 0) { + GVariant* v = + g_variant_lookup_value(options, "settings", G_VARIANT_TYPE_VARDICT); + static auto s_gtk_print_settings_new_from_gvariant = + reinterpret_cast<GtkPrintSettings* (*)(GVariant*)>( + dlsym(RTLD_DEFAULT, "gtk_print_settings_new_from_gvariant")); + + GtkPrintSettings* printSettings = s_gtk_print_settings_new_from_gvariant(v); + g_variant_unref(v); + + v = g_variant_lookup_value(options, "page-setup", G_VARIANT_TYPE_VARDICT); + static auto s_gtk_page_setup_new_from_gvariant = + reinterpret_cast<GtkPageSetup* (*)(GVariant*)>( + dlsym(RTLD_DEFAULT, "gtk_page_setup_new_from_gvariant")); + GtkPageSetup* pageSetup = s_gtk_page_setup_new_from_gvariant(v); + g_variant_unref(v); + + g_variant_lookup(options, "token", "u", &mToken); + + // Save native settings in the session object + mPrintAndPageSettings->SetGtkPrintSettings(printSettings); + mPrintAndPageSettings->SetGtkPageSetup(pageSetup); + + // Portal consumes PDF file + mPrintAndPageSettings->SetOutputFormat(nsIPrintSettings::kOutputFormatPDF); + + // We need to set to print to file + mPrintAndPageSettings->SetPrintToFile(true); + + mResult = GTK_PRINT_OPERATION_RESULT_APPLY; + } +} + +/** + * Get result of the print dialog. + * + * This call blocks until FinishPrintDialog is called. + * + */ +GtkPrintOperationResult nsFlatpakPrintPortal::GetResult() { + // If the mLoop has not been initialized we haven't go thru PreparePrint + // method + if (!NS_IsMainThread() || !mLoop) { + return GTK_PRINT_OPERATION_RESULT_ERROR; + } + // Calling g_main_loop_run stops current code until g_main_loop_quit is called + g_main_loop_run(mLoop); + + // Free resources we've allocated in order to show print dialog. +#ifdef MOZ_WAYLAND + if (mParentWindow) { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(mParentWindow)); + static auto s_gdk_wayland_window_unexport_handle = + reinterpret_cast<void (*)(GdkWindow*)>( + dlsym(RTLD_DEFAULT, "gdk_wayland_window_unexport_handle")); + if (s_gdk_wayland_window_unexport_handle) { + s_gdk_wayland_window_unexport_handle(gdk_window); + } + } +#endif + return mResult; +} + +/** + * Send file descriptor of the file which contains document to the portal to + * finish the print operation. + */ +NS_IMETHODIMP +nsFlatpakPrintPortal::Observe(nsISupports* aObject, const char* aTopic, + const char16_t* aData) { + // Check that written file match to the stored filename in case multiple + // print operations are in progress. + nsAutoString filenameStr; + mPrintAndPageSettings->GetToFileName(filenameStr); + if (!nsDependentString(aData).Equals(filenameStr)) { + // Different file is finished, not for this instance + return NS_OK; + } + int fd, idx; + fd = open(NS_ConvertUTF16toUTF8(filenameStr).get(), O_RDONLY | O_CLOEXEC); + static auto s_g_unix_fd_list_new = reinterpret_cast<GUnixFDList* (*)(void)>( + dlsym(RTLD_DEFAULT, "g_unix_fd_list_new")); + NS_ASSERTION(s_g_unix_fd_list_new, + "Cannot find g_unix_fd_list_new function."); + + GUnixFDList* fd_list = s_g_unix_fd_list_new(); + static auto s_g_unix_fd_list_append = + reinterpret_cast<gint (*)(GUnixFDList*, gint, GError**)>( + dlsym(RTLD_DEFAULT, "g_unix_fd_list_append")); + idx = s_g_unix_fd_list_append(fd_list, fd, NULL); + close(fd); + + GVariantBuilder opt_builder; + g_variant_builder_init(&opt_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&opt_builder, "{sv}", "token", + g_variant_new_uint32(mToken)); + g_dbus_proxy_call_with_unix_fd_list( + mProxy, "Print", + g_variant_new("(ssh@a{sv})", "", /* window */ + "Print", /* title */ + idx, g_variant_builder_end(&opt_builder)), + G_DBUS_CALL_FLAGS_NONE, -1, fd_list, NULL, + NULL, // TODO portal result cb function + nullptr); // data + g_object_unref(fd_list); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + // Let the nsFlatpakPrintPortal instance die + os->RemoveObserver(this, "print-to-file-finished"); + return NS_OK; +} + +nsFlatpakPrintPortal::~nsFlatpakPrintPortal() { + if (mProxy) { + if (mResponseSignalId) { + g_dbus_connection_signal_unsubscribe( + g_dbus_proxy_get_connection(G_DBUS_PROXY(mProxy)), mResponseSignalId); + } + g_object_unref(mProxy); + } + if (mLoop) g_main_loop_quit(mLoop); +} + +NS_IMETHODIMP +nsPrintDialogServiceGTK::Show(nsPIDOMWindowOuter* aParent, + nsIPrintSettings* aSettings) { + MOZ_ASSERT(aParent, "aParent must not be null"); + MOZ_ASSERT(aSettings, "aSettings must not be null"); + + // Check for the flatpak portal first + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + bool shouldUsePortal; + giovfs->ShouldUseFlatpakPortal(&shouldUsePortal); + if (shouldUsePortal && gtk_check_version(3, 22, 0) == nullptr) { + nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aParent); + NS_ASSERTION(widget, "Need a widget for dialog to be modal."); + GtkWindow* gtkParent = get_gtk_window_for_nsiwidget(widget); + NS_ASSERTION(gtkParent, "Need a GTK window for dialog to be modal."); + + nsCOMPtr<nsPrintSettingsGTK> printSettingsGTK(do_QueryInterface(aSettings)); + RefPtr<nsFlatpakPrintPortal> fpPrintPortal = + new nsFlatpakPrintPortal(printSettingsGTK); + + nsresult rv = fpPrintPortal->PreparePrintRequest(gtkParent); + NS_ENSURE_SUCCESS(rv, rv); + + // This blocks until nsFlatpakPrintPortal::FinishPrintDialog is called + GtkPrintOperationResult printDialogResult = fpPrintPortal->GetResult(); + + switch (printDialogResult) { + case GTK_PRINT_OPERATION_RESULT_APPLY: { + nsCOMPtr<nsIObserverService> os = + mozilla::services::GetObserverService(); + NS_ENSURE_STATE(os); + // Observer waits until notified that the file with the content + // to print has been written. + rv = os->AddObserver(fpPrintPortal, "print-to-file-finished", false); + NS_ENSURE_SUCCESS(rv, rv); + break; + } + case GTK_PRINT_OPERATION_RESULT_CANCEL: + rv = NS_ERROR_ABORT; + break; + default: + NS_WARNING("Unexpected response"); + rv = NS_ERROR_ABORT; + } + return rv; + } + + nsPrintDialogWidgetGTK printDialog(aParent, aSettings); + nsresult rv = printDialog.ImportSettings(aSettings); + + NS_ENSURE_SUCCESS(rv, rv); + + const gint response = printDialog.Run(); + + // Handle the result + switch (response) { + case GTK_RESPONSE_OK: // Proceed + rv = printDialog.ExportSettings(aSettings); + break; + + case GTK_RESPONSE_CANCEL: + case GTK_RESPONSE_CLOSE: + case GTK_RESPONSE_DELETE_EVENT: + case GTK_RESPONSE_NONE: + rv = NS_ERROR_ABORT; + break; + + case GTK_RESPONSE_APPLY: // Print preview + default: + NS_WARNING("Unexpected response"); + rv = NS_ERROR_ABORT; + } + return rv; +} + +NS_IMETHODIMP +nsPrintDialogServiceGTK::ShowPageSetup(nsPIDOMWindowOuter* aParent, + nsIPrintSettings* aNSSettings) { + MOZ_ASSERT(aParent, "aParent must not be null"); + MOZ_ASSERT(aNSSettings, "aSettings must not be null"); + NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE); + + nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aParent); + NS_ASSERTION(widget, "Need a widget for dialog to be modal."); + GtkWindow* gtkParent = get_gtk_window_for_nsiwidget(widget); + NS_ASSERTION(gtkParent, "Need a GTK window for dialog to be modal."); + + nsCOMPtr<nsPrintSettingsGTK> aNSSettingsGTK(do_QueryInterface(aNSSettings)); + if (!aNSSettingsGTK) return NS_ERROR_FAILURE; + + // We need to init the prefs here because aNSSettings in its current form is a + // dummy in both uses of the word + nsCOMPtr<nsIPrintSettingsService> psService = + do_GetService("@mozilla.org/gfx/printsettings-service;1"); + if (psService) { + nsString printName; + aNSSettings->GetPrinterName(printName); + if (printName.IsVoid()) { + psService->GetLastUsedPrinterName(printName); + aNSSettings->SetPrinterName(printName); + } + psService->InitPrintSettingsFromPrefs(aNSSettings, true, + nsIPrintSettings::kInitSaveAll); + } + + // Frustratingly, gtk_print_run_page_setup_dialog doesn't tell us whether + // the user cancelled or confirmed the dialog! So to avoid needlessly + // refreshing the preview when Page Setup was cancelled, we compare the + // serializations of old and new settings; if they're the same, bail out. + GtkPrintSettings* gtkSettings = aNSSettingsGTK->GetGtkPrintSettings(); + GtkPageSetup* oldPageSetup = aNSSettingsGTK->GetGtkPageSetup(); + GKeyFile* oldKeyFile = g_key_file_new(); + gtk_page_setup_to_key_file(oldPageSetup, oldKeyFile, nullptr); + gsize oldLength; + gchar* oldData = g_key_file_to_data(oldKeyFile, &oldLength, nullptr); + g_key_file_free(oldKeyFile); + + GtkPageSetup* newPageSetup = + gtk_print_run_page_setup_dialog(gtkParent, oldPageSetup, gtkSettings); + + GKeyFile* newKeyFile = g_key_file_new(); + gtk_page_setup_to_key_file(newPageSetup, newKeyFile, nullptr); + gsize newLength; + gchar* newData = g_key_file_to_data(newKeyFile, &newLength, nullptr); + g_key_file_free(newKeyFile); + + bool unchanged = + (oldLength == newLength && !memcmp(oldData, newData, oldLength)); + g_free(oldData); + g_free(newData); + if (unchanged) { + g_object_unref(newPageSetup); + return NS_ERROR_ABORT; + } + + aNSSettingsGTK->SetGtkPageSetup(newPageSetup); + + // Now newPageSetup has a refcount of 2 (SetGtkPageSetup will addref), put it + // to 1 so if this gets replaced we don't leak. + g_object_unref(newPageSetup); + + if (psService) + psService->SavePrintSettingsToPrefs( + aNSSettings, true, + nsIPrintSettings::kInitSaveOrientation | + nsIPrintSettings::kInitSavePaperSize | + nsIPrintSettings::kInitSaveUnwriteableMargins); + + return NS_OK; +} diff --git a/widget/gtk/nsPrintDialogGTK.h b/widget/gtk/nsPrintDialogGTK.h new file mode 100644 index 0000000000..83cd357992 --- /dev/null +++ b/widget/gtk/nsPrintDialogGTK.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsPrintDialog_h__ +#define nsPrintDialog_h__ + +#include "nsIPrintDialogService.h" + +class nsIPrintSettings; + +// Copy the print pages enum here because not all versions +// have SELECTION, which we will use +typedef enum { + _GTK_PRINT_PAGES_ALL, + _GTK_PRINT_PAGES_CURRENT, + _GTK_PRINT_PAGES_RANGES, + _GTK_PRINT_PAGES_SELECTION +} _GtkPrintPages; + +class nsPrintDialogServiceGTK : public nsIPrintDialogService { + virtual ~nsPrintDialogServiceGTK(); + + public: + nsPrintDialogServiceGTK(); + + NS_DECL_ISUPPORTS + + NS_IMETHOD Init() override; + NS_IMETHOD Show(nsPIDOMWindowOuter* aParent, + nsIPrintSettings* aSettings) override; + NS_IMETHOD ShowPageSetup(nsPIDOMWindowOuter* aParent, + nsIPrintSettings* aSettings) override; +}; + +#endif diff --git a/widget/gtk/nsPrintSettingsGTK.cpp b/widget/gtk/nsPrintSettingsGTK.cpp new file mode 100644 index 0000000000..986e90ee4b --- /dev/null +++ b/widget/gtk/nsPrintSettingsGTK.cpp @@ -0,0 +1,685 @@ +/* -*- 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 "nsPrintSettingsGTK.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include <stdlib.h> +#include <algorithm> + +static gboolean ref_printer(GtkPrinter* aPrinter, gpointer aData) { + ((nsPrintSettingsGTK*)aData)->SetGtkPrinter(aPrinter); + return TRUE; +} + +static gboolean printer_enumerator(GtkPrinter* aPrinter, gpointer aData) { + if (gtk_printer_is_default(aPrinter)) return ref_printer(aPrinter, aData); + + return FALSE; // Keep 'em coming... +} + +static GtkPaperSize* moz_gtk_paper_size_copy_to_new_custom( + GtkPaperSize* oldPaperSize) { + // We make a "custom-ified" copy of the paper size so it can be changed later. + return gtk_paper_size_new_custom( + gtk_paper_size_get_name(oldPaperSize), + gtk_paper_size_get_display_name(oldPaperSize), + gtk_paper_size_get_width(oldPaperSize, GTK_UNIT_INCH), + gtk_paper_size_get_height(oldPaperSize, GTK_UNIT_INCH), GTK_UNIT_INCH); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsGTK, nsPrintSettings, + nsPrintSettingsGTK) + +/** --------------------------------------------------- + */ +nsPrintSettingsGTK::nsPrintSettingsGTK() + : mPageSetup(nullptr), mPrintSettings(nullptr), mGTKPrinter(nullptr) { + // The aim here is to set up the objects enough that silent printing works + // well. These will be replaced anyway if the print dialog is used. + mPrintSettings = gtk_print_settings_new(); + GtkPageSetup* pageSetup = gtk_page_setup_new(); + SetGtkPageSetup(pageSetup); + g_object_unref(pageSetup); + + SetOutputFormat(nsIPrintSettings::kOutputFormatNative); +} + +already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings( + const mozilla::PrintSettingsInitializer& aSettings) { + RefPtr<nsPrintSettings> settings = new nsPrintSettingsGTK(); + settings->InitWithInitializer(aSettings); + settings->SetDefaultFileName(); + return settings.forget(); +} + +/** --------------------------------------------------- + */ +nsPrintSettingsGTK::~nsPrintSettingsGTK() { + if (mPageSetup) { + g_object_unref(mPageSetup); + mPageSetup = nullptr; + } + if (mPrintSettings) { + g_object_unref(mPrintSettings); + mPrintSettings = nullptr; + } + if (mGTKPrinter) { + g_object_unref(mGTKPrinter); + mGTKPrinter = nullptr; + } +} + +/** --------------------------------------------------- + */ +nsPrintSettingsGTK::nsPrintSettingsGTK(const nsPrintSettingsGTK& aPS) + : mPageSetup(nullptr), mPrintSettings(nullptr), mGTKPrinter(nullptr) { + *this = aPS; +} + +/** --------------------------------------------------- + */ +nsPrintSettingsGTK& nsPrintSettingsGTK::operator=( + const nsPrintSettingsGTK& rhs) { + if (this == &rhs) { + return *this; + } + + nsPrintSettings::operator=(rhs); + + if (mPageSetup) g_object_unref(mPageSetup); + mPageSetup = gtk_page_setup_copy(rhs.mPageSetup); + // NOTE: No need to re-initialize mUnwriteableMargin here (even + // though mPageSetup is changing). It'll be copied correctly by + // nsPrintSettings::operator=. + + if (mPrintSettings) g_object_unref(mPrintSettings); + mPrintSettings = gtk_print_settings_copy(rhs.mPrintSettings); + + if (mGTKPrinter) g_object_unref(mGTKPrinter); + mGTKPrinter = (GtkPrinter*)g_object_ref(rhs.mGTKPrinter); + + return *this; +} + +/** ------------------------------------------- + */ +nsresult nsPrintSettingsGTK::_Clone(nsIPrintSettings** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsPrintSettingsGTK* newSettings = new nsPrintSettingsGTK(*this); + if (!newSettings) return NS_ERROR_FAILURE; + *_retval = newSettings; + NS_ADDREF(*_retval); + return NS_OK; +} + +/** ------------------------------------------- + */ +NS_IMETHODIMP +nsPrintSettingsGTK::_Assign(nsIPrintSettings* aPS) { + nsPrintSettingsGTK* printSettingsGTK = static_cast<nsPrintSettingsGTK*>(aPS); + if (!printSettingsGTK) return NS_ERROR_UNEXPECTED; + *this = *printSettingsGTK; + return NS_OK; +} + +/** --------------------------------------------------- + */ +void nsPrintSettingsGTK::SetGtkPageSetup(GtkPageSetup* aPageSetup) { + if (mPageSetup) g_object_unref(mPageSetup); + + mPageSetup = (GtkPageSetup*)g_object_ref(aPageSetup); + InitUnwriteableMargin(); + + // If the paper size is not custom, then we make a custom copy of the + // GtkPaperSize, so it can be mutable. If a GtkPaperSize wasn't made as + // custom, its properties are immutable. + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(aPageSetup); + if (!gtk_paper_size_is_custom(paperSize)) { + GtkPaperSize* customPaperSize = + moz_gtk_paper_size_copy_to_new_custom(paperSize); + gtk_page_setup_set_paper_size(mPageSetup, customPaperSize); + gtk_paper_size_free(customPaperSize); + } + SaveNewPageSize(); +} + +/** --------------------------------------------------- + */ +void nsPrintSettingsGTK::SetGtkPrintSettings(GtkPrintSettings* aPrintSettings) { + if (mPrintSettings) g_object_unref(mPrintSettings); + + mPrintSettings = (GtkPrintSettings*)g_object_ref(aPrintSettings); + + GtkPaperSize* paperSize = gtk_print_settings_get_paper_size(aPrintSettings); + if (paperSize) { + GtkPaperSize* customPaperSize = + moz_gtk_paper_size_copy_to_new_custom(paperSize); + gtk_paper_size_free(paperSize); + gtk_page_setup_set_paper_size(mPageSetup, customPaperSize); + gtk_paper_size_free(customPaperSize); + } else { + // paperSize was null, and so we add the paper size in the GtkPageSetup to + // the settings. + SaveNewPageSize(); + } +} + +/** --------------------------------------------------- + */ +void nsPrintSettingsGTK::SetGtkPrinter(GtkPrinter* aPrinter) { + if (mGTKPrinter) g_object_unref(mGTKPrinter); + + mGTKPrinter = (GtkPrinter*)g_object_ref(aPrinter); +} + +NS_IMETHODIMP nsPrintSettingsGTK::GetOutputFormat(int16_t* aOutputFormat) { + NS_ENSURE_ARG_POINTER(aOutputFormat); + + int16_t format; + nsresult rv = nsPrintSettings::GetOutputFormat(&format); + if (NS_FAILED(rv)) { + return rv; + } + + if (format == nsIPrintSettings::kOutputFormatNative && + GTK_IS_PRINTER(mGTKPrinter)) { + if (gtk_printer_accepts_pdf(mGTKPrinter)) { + format = nsIPrintSettings::kOutputFormatPDF; + } else { + format = nsIPrintSettings::kOutputFormatPS; + } + } + + *aOutputFormat = format; + return NS_OK; +} + +/** + * Reimplementation of nsPrintSettings functions so that we get the values + * from the GTK objects rather than our own variables. + */ + +NS_IMETHODIMP +nsPrintSettingsGTK::SetPageRanges(const nsTArray<int32_t>& aRanges) { + if (aRanges.Length() % 2 != 0) { + return NS_ERROR_FAILURE; + } + + gtk_print_settings_set_print_pages( + mPrintSettings, + aRanges.IsEmpty() ? GTK_PRINT_PAGES_ALL : GTK_PRINT_PAGES_RANGES); + + nsTArray<GtkPageRange> ranges; + ranges.SetCapacity(aRanges.Length() / 2); + for (size_t i = 0; i < aRanges.Length(); i += 2) { + GtkPageRange* gtkRange = ranges.AppendElement(); + gtkRange->start = aRanges[i] - 1; + gtkRange->end = aRanges[i + 1] - 1; + } + + gtk_print_settings_set_page_ranges(mPrintSettings, ranges.Elements(), + ranges.Length()); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPrintReversed(bool* aPrintReversed) { + *aPrintReversed = gtk_print_settings_get_reverse(mPrintSettings); + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetPrintReversed(bool aPrintReversed) { + gtk_print_settings_set_reverse(mPrintSettings, aPrintReversed); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPrintInColor(bool* aPrintInColor) { + *aPrintInColor = gtk_print_settings_get_use_color(mPrintSettings); + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetPrintInColor(bool aPrintInColor) { + gtk_print_settings_set_use_color(mPrintSettings, aPrintInColor); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetOrientation(int32_t* aOrientation) { + NS_ENSURE_ARG_POINTER(aOrientation); + + GtkPageOrientation gtkOrient = gtk_page_setup_get_orientation(mPageSetup); + switch (gtkOrient) { + case GTK_PAGE_ORIENTATION_LANDSCAPE: + case GTK_PAGE_ORIENTATION_REVERSE_LANDSCAPE: + *aOrientation = kLandscapeOrientation; + break; + + case GTK_PAGE_ORIENTATION_PORTRAIT: + case GTK_PAGE_ORIENTATION_REVERSE_PORTRAIT: + default: + *aOrientation = kPortraitOrientation; + } + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetOrientation(int32_t aOrientation) { + GtkPageOrientation gtkOrient; + if (aOrientation == kLandscapeOrientation) + gtkOrient = GTK_PAGE_ORIENTATION_LANDSCAPE; + else + gtkOrient = GTK_PAGE_ORIENTATION_PORTRAIT; + + gtk_print_settings_set_orientation(mPrintSettings, gtkOrient); + gtk_page_setup_set_orientation(mPageSetup, gtkOrient); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetToFileName(nsAString& aToFileName) { + // Get the gtk output filename + const char* gtk_output_uri = + gtk_print_settings_get(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_URI); + if (!gtk_output_uri) { + aToFileName = mToFileName; + return NS_OK; + } + + // Convert to an nsIFile + nsCOMPtr<nsIFile> file; + nsresult rv = NS_GetFileFromURLSpec(nsDependentCString(gtk_output_uri), + getter_AddRefs(file)); + if (NS_FAILED(rv)) return rv; + + // Extract the path + return file->GetPath(aToFileName); +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetToFileName(const nsAString& aToFileName) { + if (aToFileName.IsEmpty()) { + mToFileName.SetLength(0); + gtk_print_settings_set(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_URI, + nullptr); + return NS_OK; + } + + gtk_print_settings_set(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_FILE_FORMAT, + "pdf"); + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(aToFileName, true, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + // Convert the nsIFile to a URL + nsAutoCString url; + rv = NS_GetURLSpecFromFile(file, url); + NS_ENSURE_SUCCESS(rv, rv); + + gtk_print_settings_set(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_URI, + url.get()); + mToFileName = aToFileName; + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPrinterName(nsAString& aPrinter) { + const char* gtkPrintName = gtk_print_settings_get_printer(mPrintSettings); + if (!gtkPrintName) { + if (GTK_IS_PRINTER(mGTKPrinter)) { + gtkPrintName = gtk_printer_get_name(mGTKPrinter); + } else { + // This mimics what nsPrintSettingsImpl does when we try to Get before we + // Set + aPrinter.Truncate(); + return NS_OK; + } + } + CopyUTF8toUTF16(mozilla::MakeStringSpan(gtkPrintName), aPrinter); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetPrinterName(const nsAString& aPrinter) { + NS_ConvertUTF16toUTF8 gtkPrinter(aPrinter); + + if (StringBeginsWith(gtkPrinter, "CUPS/"_ns)) { + // Strip off "CUPS/"; GTK might recognize the rest + gtkPrinter.Cut(0, strlen("CUPS/")); + } + + // Give mPrintSettings the passed-in printer name if either... + // - it has no printer name stored yet + // - it has an existing printer name that's different from + // the name passed to this function. + const char* oldPrinterName = gtk_print_settings_get_printer(mPrintSettings); + if (!oldPrinterName || !gtkPrinter.Equals(oldPrinterName)) { + mIsInitedFromPrinter = false; + mIsInitedFromPrefs = false; + gtk_print_settings_set_printer(mPrintSettings, gtkPrinter.get()); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetNumCopies(int32_t* aNumCopies) { + NS_ENSURE_ARG_POINTER(aNumCopies); + *aNumCopies = gtk_print_settings_get_n_copies(mPrintSettings); + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetNumCopies(int32_t aNumCopies) { + gtk_print_settings_set_n_copies(mPrintSettings, aNumCopies); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetScaling(double* aScaling) { + *aScaling = gtk_print_settings_get_scale(mPrintSettings) / 100.0; + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetScaling(double aScaling) { + gtk_print_settings_set_scale(mPrintSettings, aScaling * 100.0); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPaperId(nsAString& aPaperId) { + const gchar* name = + gtk_paper_size_get_name(gtk_page_setup_get_paper_size(mPageSetup)); + CopyUTF8toUTF16(mozilla::MakeStringSpan(name), aPaperId); + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetPaperId(const nsAString& aPaperId) { + NS_ConvertUTF16toUTF8 gtkPaperName(aPaperId); + + // Convert these Gecko names to GTK names + // XXX (jfkthame): is this still relevant? + if (gtkPaperName.EqualsIgnoreCase("letter")) + gtkPaperName.AssignLiteral(GTK_PAPER_NAME_LETTER); + else if (gtkPaperName.EqualsIgnoreCase("legal")) + gtkPaperName.AssignLiteral(GTK_PAPER_NAME_LEGAL); + + GtkPaperSize* oldPaperSize = gtk_page_setup_get_paper_size(mPageSetup); + gdouble width = gtk_paper_size_get_width(oldPaperSize, GTK_UNIT_INCH); + gdouble height = gtk_paper_size_get_height(oldPaperSize, GTK_UNIT_INCH); + + // Try to get the display name from the name so our paper size fits in the + // Page Setup dialog. + GtkPaperSize* paperSize = gtk_paper_size_new(gtkPaperName.get()); + GtkPaperSize* customPaperSize = gtk_paper_size_new_custom( + gtkPaperName.get(), gtk_paper_size_get_display_name(paperSize), width, + height, GTK_UNIT_INCH); + gtk_paper_size_free(paperSize); + + gtk_page_setup_set_paper_size(mPageSetup, customPaperSize); + gtk_paper_size_free(customPaperSize); + SaveNewPageSize(); + return NS_OK; +} + +GtkUnit nsPrintSettingsGTK::GetGTKUnit(int16_t aGeckoUnit) { + if (aGeckoUnit == kPaperSizeMillimeters) + return GTK_UNIT_MM; + else + return GTK_UNIT_INCH; +} + +void nsPrintSettingsGTK::SaveNewPageSize() { + gtk_print_settings_set_paper_size(mPrintSettings, + gtk_page_setup_get_paper_size(mPageSetup)); +} + +void nsPrintSettingsGTK::InitUnwriteableMargin() { + mUnwriteableMargin.SizeTo( + NS_INCHES_TO_INT_TWIPS( + gtk_page_setup_get_top_margin(mPageSetup, GTK_UNIT_INCH)), + NS_INCHES_TO_INT_TWIPS( + gtk_page_setup_get_right_margin(mPageSetup, GTK_UNIT_INCH)), + NS_INCHES_TO_INT_TWIPS( + gtk_page_setup_get_bottom_margin(mPageSetup, GTK_UNIT_INCH)), + NS_INCHES_TO_INT_TWIPS( + gtk_page_setup_get_left_margin(mPageSetup, GTK_UNIT_INCH))); +} + +/** + * NOTE: Need a custom set of SetUnwriteableMargin functions, because + * whenever we change mUnwriteableMargin, we must pass the change + * down to our GTKPageSetup object. (This is needed in order for us + * to give the correct default values in nsPrintDialogGTK.) + * + * It's important that the following functions pass + * mUnwriteableMargin values rather than aUnwriteableMargin values + * to gtk_page_setup_set_[blank]_margin, because the two may not be + * the same. (Specifically, negative values of aUnwriteableMargin + * are ignored by the nsPrintSettings::SetUnwriteableMargin functions.) + */ +NS_IMETHODIMP +nsPrintSettingsGTK::SetUnwriteableMarginInTwips( + nsIntMargin& aUnwriteableMargin) { + nsPrintSettings::SetUnwriteableMarginInTwips(aUnwriteableMargin); + gtk_page_setup_set_top_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.top), GTK_UNIT_INCH); + gtk_page_setup_set_left_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.left), GTK_UNIT_INCH); + gtk_page_setup_set_bottom_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.bottom), GTK_UNIT_INCH); + gtk_page_setup_set_right_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.right), GTK_UNIT_INCH); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetUnwriteableMarginTop(double aUnwriteableMarginTop) { + nsPrintSettings::SetUnwriteableMarginTop(aUnwriteableMarginTop); + gtk_page_setup_set_top_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.top), GTK_UNIT_INCH); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetUnwriteableMarginLeft(double aUnwriteableMarginLeft) { + nsPrintSettings::SetUnwriteableMarginLeft(aUnwriteableMarginLeft); + gtk_page_setup_set_left_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.left), GTK_UNIT_INCH); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetUnwriteableMarginBottom( + double aUnwriteableMarginBottom) { + nsPrintSettings::SetUnwriteableMarginBottom(aUnwriteableMarginBottom); + gtk_page_setup_set_bottom_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.bottom), GTK_UNIT_INCH); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetUnwriteableMarginRight(double aUnwriteableMarginRight) { + nsPrintSettings::SetUnwriteableMarginRight(aUnwriteableMarginRight); + gtk_page_setup_set_right_margin( + mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.right), GTK_UNIT_INCH); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPaperWidth(double* aPaperWidth) { + NS_ENSURE_ARG_POINTER(aPaperWidth); + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup); + *aPaperWidth = + gtk_paper_size_get_width(paperSize, GetGTKUnit(mPaperSizeUnit)); + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetPaperWidth(double aPaperWidth) { + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup); + gtk_paper_size_set_size( + paperSize, aPaperWidth, + gtk_paper_size_get_height(paperSize, GetGTKUnit(mPaperSizeUnit)), + GetGTKUnit(mPaperSizeUnit)); + SaveNewPageSize(); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPaperHeight(double* aPaperHeight) { + NS_ENSURE_ARG_POINTER(aPaperHeight); + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup); + *aPaperHeight = + gtk_paper_size_get_height(paperSize, GetGTKUnit(mPaperSizeUnit)); + return NS_OK; +} +NS_IMETHODIMP +nsPrintSettingsGTK::SetPaperHeight(double aPaperHeight) { + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup); + gtk_paper_size_set_size( + paperSize, + gtk_paper_size_get_width(paperSize, GetGTKUnit(mPaperSizeUnit)), + aPaperHeight, GetGTKUnit(mPaperSizeUnit)); + SaveNewPageSize(); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetPaperSizeUnit(int16_t aPaperSizeUnit) { + // Convert units internally. e.g. they might have set the values while we're + // still in mm but they change to inch just afterwards, expecting that their + // sizes are in inches. + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup); + gtk_paper_size_set_size( + paperSize, + gtk_paper_size_get_width(paperSize, GetGTKUnit(mPaperSizeUnit)), + gtk_paper_size_get_height(paperSize, GetGTKUnit(mPaperSizeUnit)), + GetGTKUnit(aPaperSizeUnit)); + SaveNewPageSize(); + + mPaperSizeUnit = aPaperSizeUnit; + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetEffectivePageSize(double* aWidth, double* aHeight) { + GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup); + if (mPaperSizeUnit == kPaperSizeInches) { + *aWidth = + NS_INCHES_TO_TWIPS(gtk_paper_size_get_width(paperSize, GTK_UNIT_INCH)); + *aHeight = + NS_INCHES_TO_TWIPS(gtk_paper_size_get_height(paperSize, GTK_UNIT_INCH)); + } else { + MOZ_ASSERT(mPaperSizeUnit == kPaperSizeMillimeters, + "unexpected paper size unit"); + *aWidth = NS_MILLIMETERS_TO_TWIPS( + gtk_paper_size_get_width(paperSize, GTK_UNIT_MM)); + *aHeight = NS_MILLIMETERS_TO_TWIPS( + gtk_paper_size_get_height(paperSize, GTK_UNIT_MM)); + } + GtkPageOrientation gtkOrient = gtk_page_setup_get_orientation(mPageSetup); + + if (gtkOrient == GTK_PAGE_ORIENTATION_LANDSCAPE || + gtkOrient == GTK_PAGE_ORIENTATION_REVERSE_LANDSCAPE) { + double temp = *aWidth; + *aWidth = *aHeight; + *aHeight = temp; + } + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetupSilentPrinting() { + // We have to get a printer here, rather than when the print settings are + // constructed. This is because when we request sync, GTK makes us wait in the + // *event loop* while waiting for the enumeration to finish. We must do this + // when event loop runs are expected. + gtk_enumerate_printers(printer_enumerator, this, nullptr, TRUE); + + // XXX If no default printer set, get the first one. + if (!GTK_IS_PRINTER(mGTKPrinter)) + gtk_enumerate_printers(ref_printer, this, nullptr, TRUE); + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetPageRanges(nsTArray<int32_t>& aPages) { + GtkPrintPages gtkRange = gtk_print_settings_get_print_pages(mPrintSettings); + if (gtkRange != GTK_PRINT_PAGES_RANGES) { + aPages.Clear(); + return NS_OK; + } + + gint ctRanges; + GtkPageRange* lstRanges = + gtk_print_settings_get_page_ranges(mPrintSettings, &ctRanges); + + aPages.Clear(); + + for (gint i = 0; i < ctRanges; i++) { + aPages.AppendElement(lstRanges[i].start + 1); + aPages.AppendElement(lstRanges[i].end + 1); + } + + g_free(lstRanges); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetResolution(int32_t* aResolution) { + if (!gtk_print_settings_has_key(mPrintSettings, + GTK_PRINT_SETTINGS_RESOLUTION)) + return NS_ERROR_FAILURE; + *aResolution = gtk_print_settings_get_resolution(mPrintSettings); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetResolution(int32_t aResolution) { + gtk_print_settings_set_resolution(mPrintSettings, aResolution); + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::GetDuplex(int32_t* aDuplex) { + if (!gtk_print_settings_has_key(mPrintSettings, GTK_PRINT_SETTINGS_DUPLEX)) { + *aDuplex = GTK_PRINT_DUPLEX_SIMPLEX; + } else { + *aDuplex = gtk_print_settings_get_duplex(mPrintSettings); + } + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetDuplex(int32_t aDuplex) { + MOZ_ASSERT(aDuplex >= GTK_PRINT_DUPLEX_SIMPLEX && + aDuplex <= GTK_PRINT_DUPLEX_VERTICAL, + "value is out of bounds for GtkPrintDuplex enum"); + gtk_print_settings_set_duplex(mPrintSettings, + static_cast<GtkPrintDuplex>(aDuplex)); + + // We want to set the GTK CUPS Duplex setting in addition to calling + // gtk_print_settings_set_duplex(). Some systems may look for one, or the + // other, so it is best to set them both consistently. + constexpr char kCupsDuplex[] = "cups-Duplex"; + switch (aDuplex) { + case GTK_PRINT_DUPLEX_SIMPLEX: + gtk_print_settings_set(mPrintSettings, kCupsDuplex, "None"); + break; + case GTK_PRINT_DUPLEX_HORIZONTAL: + gtk_print_settings_set(mPrintSettings, kCupsDuplex, "DuplexNoTumble"); + break; + case GTK_PRINT_DUPLEX_VERTICAL: + gtk_print_settings_set(mPrintSettings, kCupsDuplex, "DuplexTumble"); + break; + } + + return NS_OK; +} diff --git a/widget/gtk/nsPrintSettingsGTK.h b/widget/gtk/nsPrintSettingsGTK.h new file mode 100644 index 0000000000..c32c594587 --- /dev/null +++ b/widget/gtk/nsPrintSettingsGTK.h @@ -0,0 +1,145 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsPrintSettingsGTK_h_ +#define nsPrintSettingsGTK_h_ + +#include "nsPrintSettingsImpl.h" + +extern "C" { +#include <gtk/gtk.h> +#include <gtk/gtkunixprint.h> +} + +#define NS_PRINTSETTINGSGTK_IID \ + { \ + 0x758df520, 0xc7c3, 0x11dc, { \ + 0x95, 0xff, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 \ + } \ + } + +//***************************************************************************** +//*** nsPrintSettingsGTK +//***************************************************************************** + +class nsPrintSettingsGTK : public nsPrintSettings { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECLARE_STATIC_IID_ACCESSOR(NS_PRINTSETTINGSGTK_IID) + + nsPrintSettingsGTK(); + explicit nsPrintSettingsGTK(const PrintSettingsInitializer& aSettings); + + // We're overriding these methods because we want to read/write with GTK + // objects, not local variables. This allows a simpler settings implementation + // between Gecko and GTK. + + GtkPageSetup* GetGtkPageSetup() { return mPageSetup; }; + void SetGtkPageSetup(GtkPageSetup* aPageSetup); + + GtkPrintSettings* GetGtkPrintSettings() { return mPrintSettings; }; + void SetGtkPrintSettings(GtkPrintSettings* aPrintSettings); + + GtkPrinter* GetGtkPrinter() { return mGTKPrinter; }; + void SetGtkPrinter(GtkPrinter* aPrinter); + + // Reversed, color, orientation and file name are all stored in the + // GtkPrintSettings. Orientation is also stored in the GtkPageSetup and its + // setting takes priority when getting the orientation. + NS_IMETHOD GetPrintReversed(bool* aPrintReversed) override; + NS_IMETHOD SetPrintReversed(bool aPrintReversed) override; + + NS_IMETHOD GetPrintInColor(bool* aPrintInColor) override; + NS_IMETHOD SetPrintInColor(bool aPrintInColor) override; + + NS_IMETHOD GetOrientation(int32_t* aOrientation) override; + NS_IMETHOD SetOrientation(int32_t aOrientation) override; + + NS_IMETHOD GetToFileName(nsAString& aToFileName) override; + NS_IMETHOD SetToFileName(const nsAString& aToFileName) override; + + // Gets/Sets the printer name in the GtkPrintSettings. If no printer name is + // specified there, you will get back the name of the current internal + // GtkPrinter. + NS_IMETHOD GetPrinterName(nsAString& Printer) override; + NS_IMETHOD SetPrinterName(const nsAString& aPrinter) override; + + // Number of copies is stored/gotten from the GtkPrintSettings. + NS_IMETHOD GetNumCopies(int32_t* aNumCopies) override; + NS_IMETHOD SetNumCopies(int32_t aNumCopies) override; + + NS_IMETHOD GetScaling(double* aScaling) override; + NS_IMETHOD SetScaling(double aScaling) override; + + // A name recognised by GTK is strongly advised here, as this is used to + // create a GtkPaperSize. + NS_IMETHOD GetPaperId(nsAString& aPaperId) override; + NS_IMETHOD SetPaperId(const nsAString& aPaperId) override; + + NS_IMETHOD SetUnwriteableMarginInTwips( + nsIntMargin& aUnwriteableMargin) override; + NS_IMETHOD SetUnwriteableMarginTop(double aUnwriteableMarginTop) override; + NS_IMETHOD SetUnwriteableMarginLeft(double aUnwriteableMarginLeft) override; + NS_IMETHOD SetUnwriteableMarginBottom( + double aUnwriteableMarginBottom) override; + NS_IMETHOD SetUnwriteableMarginRight(double aUnwriteableMarginRight) override; + + NS_IMETHOD GetPaperWidth(double* aPaperWidth) override; + NS_IMETHOD SetPaperWidth(double aPaperWidth) override; + + NS_IMETHOD GetPaperHeight(double* aPaperHeight) override; + NS_IMETHOD SetPaperHeight(double aPaperHeight) override; + + NS_IMETHOD SetPaperSizeUnit(int16_t aPaperSizeUnit) override; + + NS_IMETHOD GetEffectivePageSize(double* aWidth, double* aHeight) override; + + NS_IMETHOD SetupSilentPrinting() override; + + NS_IMETHOD SetPageRanges(const nsTArray<int32_t>&) override; + NS_IMETHOD GetPageRanges(nsTArray<int32_t>&) override; + + NS_IMETHOD GetResolution(int32_t* aResolution) override; + NS_IMETHOD SetResolution(int32_t aResolution) override; + + NS_IMETHOD GetDuplex(int32_t* aDuplex) override; + NS_IMETHOD SetDuplex(int32_t aDuplex) override; + + NS_IMETHOD GetOutputFormat(int16_t* aOutputFormat) override; + + protected: + virtual ~nsPrintSettingsGTK(); + + nsPrintSettingsGTK(const nsPrintSettingsGTK& src); + nsPrintSettingsGTK& operator=(const nsPrintSettingsGTK& rhs); + + virtual nsresult _Clone(nsIPrintSettings** _retval) override; + virtual nsresult _Assign(nsIPrintSettings* aPS) override; + + GtkUnit GetGTKUnit(int16_t aGeckoUnit); + void SaveNewPageSize(); + + /** + * Re-initialize mUnwriteableMargin with values from mPageSetup. + * Should be called whenever mPageSetup is initialized or overwritten. + */ + void InitUnwriteableMargin(); + + /** + * On construction: + * - mPrintSettings and mPageSetup are just new objects with defaults + * determined by GTK. + * - mGTKPrinter is nullptr!!! Remember to be careful when accessing this + * property. + */ + GtkPageSetup* mPageSetup; + GtkPrintSettings* mPrintSettings; + GtkPrinter* mGTKPrinter; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintSettingsGTK, NS_PRINTSETTINGSGTK_IID) + +#endif // nsPrintSettingsGTK_h_ diff --git a/widget/gtk/nsPrintSettingsServiceGTK.cpp b/widget/gtk/nsPrintSettingsServiceGTK.cpp new file mode 100644 index 0000000000..7894101831 --- /dev/null +++ b/widget/gtk/nsPrintSettingsServiceGTK.cpp @@ -0,0 +1,78 @@ +/* -*- 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 "nsPrintSettingsServiceGTK.h" +#include "nsPrintSettingsGTK.h" + +using namespace mozilla::embedding; + +static void serialize_gtk_printsettings_to_printdata(const gchar* key, + const gchar* value, + gpointer aData) { + PrintData* data = (PrintData*)aData; + CStringKeyValue pair; + pair.key() = key; + pair.value() = value; + data->GTKPrintSettings().AppendElement(pair); +} + +NS_IMETHODIMP +nsPrintSettingsServiceGTK::SerializeToPrintData(nsIPrintSettings* aSettings, + PrintData* data) { + nsresult rv = nsPrintSettingsService::SerializeToPrintData(aSettings, data); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsPrintSettingsGTK> settingsGTK(do_QueryInterface(aSettings)); + NS_ENSURE_STATE(settingsGTK); + + GtkPrintSettings* gtkPrintSettings = settingsGTK->GetGtkPrintSettings(); + NS_ENSURE_STATE(gtkPrintSettings); + + gtk_print_settings_foreach(gtkPrintSettings, + serialize_gtk_printsettings_to_printdata, data); + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsServiceGTK::DeserializeToPrintSettings( + const PrintData& data, nsIPrintSettings* settings) { + nsCOMPtr<nsPrintSettingsGTK> settingsGTK(do_QueryInterface(settings)); + NS_ENSURE_STATE(settingsGTK); + + nsresult rv = + nsPrintSettingsService::DeserializeToPrintSettings(data, settings); + NS_ENSURE_SUCCESS(rv, rv); + + // Instead of re-using the GtkPrintSettings that nsIPrintSettings is + // wrapping, we'll create a new one to deserialize to and replace it + // within nsIPrintSettings. + GtkPrintSettings* newGtkPrintSettings = gtk_print_settings_new(); + + for (uint32_t i = 0; i < data.GTKPrintSettings().Length(); ++i) { + CStringKeyValue pair = data.GTKPrintSettings()[i]; + gtk_print_settings_set(newGtkPrintSettings, pair.key().get(), + pair.value().get()); + } + + settingsGTK->SetGtkPrintSettings(newGtkPrintSettings); + + // nsPrintSettingsGTK is holding a reference to newGtkPrintSettings + g_object_unref(newGtkPrintSettings); + newGtkPrintSettings = nullptr; + return NS_OK; +} + +nsresult nsPrintSettingsServiceGTK::_CreatePrintSettings( + nsIPrintSettings** _retval) { + *_retval = nullptr; + nsPrintSettingsGTK* printSettings = + new nsPrintSettingsGTK(); // does not initially ref count + NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY); + + NS_ADDREF(*_retval = printSettings); // ref count + + return NS_OK; +} diff --git a/widget/gtk/nsPrintSettingsServiceGTK.h b/widget/gtk/nsPrintSettingsServiceGTK.h new file mode 100644 index 0000000000..d26f543110 --- /dev/null +++ b/widget/gtk/nsPrintSettingsServiceGTK.h @@ -0,0 +1,33 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsPrintSettingsServiceGTK_h +#define nsPrintSettingsServiceGTK_h + +#include "nsPrintSettingsService.h" + +namespace mozilla { +namespace embedding { +class PrintData; +} // namespace embedding +} // namespace mozilla + +class nsPrintSettingsServiceGTK final : public nsPrintSettingsService { + public: + nsPrintSettingsServiceGTK() = default; + + NS_IMETHODIMP SerializeToPrintData( + nsIPrintSettings* aSettings, + mozilla::embedding::PrintData* data) override; + + NS_IMETHODIMP DeserializeToPrintSettings( + const mozilla::embedding::PrintData& data, + nsIPrintSettings* settings) override; + + virtual nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override; +}; + +#endif // nsPrintSettingsServiceGTK_h diff --git a/widget/gtk/nsSound.cpp b/widget/gtk/nsSound.cpp new file mode 100644 index 0000000000..4d685d8566 --- /dev/null +++ b/widget/gtk/nsSound.cpp @@ -0,0 +1,398 @@ +/* -*- 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 <string.h> + +#include "nscore.h" +#include "plstr.h" +#include "prlink.h" + +#include "nsSound.h" + +#include "HeadlessSound.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsDirectoryService.h" +#include "nsDirectoryServiceDefs.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/WidgetUtils.h" +#include "nsIXULAppInfo.h" +#include "nsContentUtils.h" +#include "gfxPlatform.h" +#include "mozilla/ClearOnShutdown.h" + +#include <stdio.h> +#include <unistd.h> + +#include <gtk/gtk.h> +static PRLibrary* libcanberra = nullptr; + +/* used to play sounds with libcanberra. */ +typedef struct _ca_context ca_context; +typedef struct _ca_proplist ca_proplist; + +typedef void (*ca_finish_callback_t)(ca_context* c, uint32_t id, int error_code, + void* userdata); + +typedef int (*ca_context_create_fn)(ca_context**); +typedef int (*ca_context_destroy_fn)(ca_context*); +typedef int (*ca_context_play_fn)(ca_context* c, uint32_t id, ...); +typedef int (*ca_context_change_props_fn)(ca_context* c, ...); +typedef int (*ca_proplist_create_fn)(ca_proplist**); +typedef int (*ca_proplist_destroy_fn)(ca_proplist*); +typedef int (*ca_proplist_sets_fn)(ca_proplist* c, const char* key, + const char* value); +typedef int (*ca_context_play_full_fn)(ca_context* c, uint32_t id, + ca_proplist* p, ca_finish_callback_t cb, + void* userdata); + +static ca_context_create_fn ca_context_create; +static ca_context_destroy_fn ca_context_destroy; +static ca_context_play_fn ca_context_play; +static ca_context_change_props_fn ca_context_change_props; +static ca_proplist_create_fn ca_proplist_create; +static ca_proplist_destroy_fn ca_proplist_destroy; +static ca_proplist_sets_fn ca_proplist_sets; +static ca_context_play_full_fn ca_context_play_full; + +struct ScopedCanberraFile { + explicit ScopedCanberraFile(nsIFile* file) : mFile(file){}; + + ~ScopedCanberraFile() { + if (mFile) { + mFile->Remove(false); + } + } + + void forget() { mozilla::Unused << mFile.forget(); } + nsIFile* operator->() { return mFile; } + operator nsIFile*() { return mFile; } + + nsCOMPtr<nsIFile> mFile; +}; + +static ca_context* ca_context_get_default() { + // This allows us to avoid race conditions with freeing the context by handing + // that responsibility to Glib, and still use one context at a time + static GPrivate ctx_private = + G_PRIVATE_INIT((GDestroyNotify)ca_context_destroy); + + ca_context* ctx = (ca_context*)g_private_get(&ctx_private); + + if (ctx) { + return ctx; + } + + ca_context_create(&ctx); + if (!ctx) { + return nullptr; + } + + g_private_set(&ctx_private, ctx); + + GtkSettings* settings = gtk_settings_get_default(); + if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings), + "gtk-sound-theme-name")) { + gchar* sound_theme_name = nullptr; + g_object_get(settings, "gtk-sound-theme-name", &sound_theme_name, nullptr); + + if (sound_theme_name) { + ca_context_change_props(ctx, "canberra.xdg-theme.name", sound_theme_name, + nullptr); + g_free(sound_theme_name); + } + } + + nsAutoString wbrand; + mozilla::widget::WidgetUtils::GetBrandShortName(wbrand); + ca_context_change_props(ctx, "application.name", + NS_ConvertUTF16toUTF8(wbrand).get(), nullptr); + + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1"); + if (appInfo) { + nsAutoCString version; + appInfo->GetVersion(version); + + ca_context_change_props(ctx, "application.version", version.get(), nullptr); + } + + ca_context_change_props(ctx, "application.icon_name", MOZ_APP_NAME, nullptr); + + return ctx; +} + +static void ca_finish_cb(ca_context* c, uint32_t id, int error_code, + void* userdata) { + nsIFile* file = reinterpret_cast<nsIFile*>(userdata); + if (file) { + file->Remove(false); + NS_RELEASE(file); + } +} + +NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver) + +//////////////////////////////////////////////////////////////////////// +nsSound::nsSound() { mInited = false; } + +nsSound::~nsSound() = default; + +NS_IMETHODIMP +nsSound::Init() { + // This function is designed so that no library is compulsory, and + // one library missing doesn't cause the other(s) to not be used. + if (mInited) return NS_OK; + + mInited = true; + + if (!libcanberra) { + libcanberra = PR_LoadLibrary("libcanberra.so.0"); + if (libcanberra) { + ca_context_create = (ca_context_create_fn)PR_FindFunctionSymbol( + libcanberra, "ca_context_create"); + if (!ca_context_create) { +#ifdef MOZ_TSAN + // With TSan, we cannot unload libcanberra once we have loaded it + // because TSan does not support unloading libraries that are matched + // from its suppression list. Hence we just keep the library loaded in + // TSan builds. + libcanberra = nullptr; + return NS_OK; +#endif + PR_UnloadLibrary(libcanberra); + libcanberra = nullptr; + } else { + // at this point we know we have a good libcanberra library + ca_context_destroy = (ca_context_destroy_fn)PR_FindFunctionSymbol( + libcanberra, "ca_context_destroy"); + ca_context_play = (ca_context_play_fn)PR_FindFunctionSymbol( + libcanberra, "ca_context_play"); + ca_context_change_props = + (ca_context_change_props_fn)PR_FindFunctionSymbol( + libcanberra, "ca_context_change_props"); + ca_proplist_create = (ca_proplist_create_fn)PR_FindFunctionSymbol( + libcanberra, "ca_proplist_create"); + ca_proplist_destroy = (ca_proplist_destroy_fn)PR_FindFunctionSymbol( + libcanberra, "ca_proplist_destroy"); + ca_proplist_sets = (ca_proplist_sets_fn)PR_FindFunctionSymbol( + libcanberra, "ca_proplist_sets"); + ca_context_play_full = (ca_context_play_full_fn)PR_FindFunctionSymbol( + libcanberra, "ca_context_play_full"); + } + } + } + + return NS_OK; +} + +/* static */ +void nsSound::Shutdown() { +#ifndef MOZ_TSAN + if (libcanberra) { + PR_UnloadLibrary(libcanberra); + libcanberra = nullptr; + } +#endif +} + +namespace mozilla { +namespace sound { +StaticRefPtr<nsISound> sInstance; +} +} // namespace mozilla +/* static */ +already_AddRefed<nsISound> nsSound::GetInstance() { + using namespace mozilla::sound; + + if (!sInstance) { + if (gfxPlatform::IsHeadless()) { + sInstance = new mozilla::widget::HeadlessSound(); + } else { + sInstance = new nsSound(); + } + ClearOnShutdown(&sInstance); + } + + RefPtr<nsISound> service = sInstance.get(); + return service.forget(); +} + +NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* context, nsresult aStatus, + uint32_t dataLen, const uint8_t* data) { + // print a load error on bad status, and return + if (NS_FAILED(aStatus)) { +#ifdef DEBUG + if (aLoader) { + nsCOMPtr<nsIRequest> request; + aLoader->GetRequest(getter_AddRefs(request)); + if (request) { + nsCOMPtr<nsIURI> uri; + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + if (uri) { + printf("Failed to load %s\n", uri->GetSpecOrDefault().get()); + } + } + } + } +#endif + return aStatus; + } + + nsCOMPtr<nsIFile> tmpFile; + nsDirectoryService::gService->Get(NS_OS_TEMP_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(tmpFile)); + + nsresult rv = + tmpFile->AppendNative(nsDependentCString("mozilla_audio_sample")); + if (NS_FAILED(rv)) { + return rv; + } + + rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, PR_IRUSR | PR_IWUSR); + if (NS_FAILED(rv)) { + return rv; + } + + ScopedCanberraFile canberraFile(tmpFile); + + mozilla::AutoFDClose fd; + rv = canberraFile->OpenNSPRFileDesc(PR_WRONLY, PR_IRUSR | PR_IWUSR, + &fd.rwget()); + if (NS_FAILED(rv)) { + return rv; + } + + // XXX: Should we do this on another thread? + uint32_t length = dataLen; + while (length > 0) { + int32_t amount = PR_Write(fd, data, length); + if (amount < 0) { + return NS_ERROR_FAILURE; + } + length -= amount; + data += amount; + } + + ca_context* ctx = ca_context_get_default(); + if (!ctx) { + return NS_ERROR_OUT_OF_MEMORY; + } + + ca_proplist* p; + ca_proplist_create(&p); + if (!p) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoCString path; + rv = canberraFile->GetNativePath(path); + if (NS_FAILED(rv)) { + return rv; + } + + ca_proplist_sets(p, "media.filename", path.get()); + if (ca_context_play_full(ctx, 0, p, ca_finish_cb, canberraFile) >= 0) { + // Don't delete the temporary file here if ca_context_play_full succeeds + canberraFile.forget(); + } + ca_proplist_destroy(p); + + return NS_OK; +} + +NS_IMETHODIMP nsSound::Beep() { + ::gdk_beep(); + return NS_OK; +} + +NS_IMETHODIMP nsSound::Play(nsIURL* aURL) { + if (!mInited) Init(); + + if (!libcanberra) return NS_ERROR_NOT_AVAILABLE; + + nsresult rv; + if (aURL->SchemeIs("file")) { + ca_context* ctx = ca_context_get_default(); + if (!ctx) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoCString spec; + rv = aURL->GetSpec(spec); + if (NS_FAILED(rv)) { + return rv; + } + gchar* path = g_filename_from_uri(spec.get(), nullptr, nullptr); + if (!path) { + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + } + + ca_context_play(ctx, 0, "media.filename", path, nullptr); + g_free(path); + } else { + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader( + getter_AddRefs(loader), aURL, + this, // aObserver + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + } + + return rv; +} + +NS_IMETHODIMP nsSound::PlayEventSound(uint32_t aEventId) { + if (!mInited) Init(); + + if (!libcanberra) return NS_OK; + + // Do we even want alert sounds? + GtkSettings* settings = gtk_settings_get_default(); + + if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings), + "gtk-enable-event-sounds")) { + gboolean enable_sounds = TRUE; + g_object_get(settings, "gtk-enable-event-sounds", &enable_sounds, nullptr); + + if (!enable_sounds) { + return NS_OK; + } + } + + ca_context* ctx = ca_context_get_default(); + if (!ctx) { + return NS_ERROR_OUT_OF_MEMORY; + } + + switch (aEventId) { + case EVENT_ALERT_DIALOG_OPEN: + ca_context_play(ctx, 0, "event.id", "dialog-warning", nullptr); + break; + case EVENT_CONFIRM_DIALOG_OPEN: + ca_context_play(ctx, 0, "event.id", "dialog-question", nullptr); + break; + case EVENT_NEW_MAIL_RECEIVED: + ca_context_play(ctx, 0, "event.id", "message-new-email", nullptr); + break; + case EVENT_MENU_EXECUTE: + ca_context_play(ctx, 0, "event.id", "menu-click", nullptr); + break; + case EVENT_MENU_POPUP: + ca_context_play(ctx, 0, "event.id", "menu-popup", nullptr); + break; + } + return NS_OK; +} diff --git a/widget/gtk/nsSound.h b/widget/gtk/nsSound.h new file mode 100644 index 0000000000..8f4fe5a04b --- /dev/null +++ b/widget/gtk/nsSound.h @@ -0,0 +1,33 @@ +/* -*- 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 __nsSound_h__ +#define __nsSound_h__ + +#include "nsISound.h" +#include "nsIStreamLoader.h" + +#include <gtk/gtk.h> + +class nsSound : public nsISound, public nsIStreamLoaderObserver { + public: + nsSound(); + + static void Shutdown(); + static already_AddRefed<nsISound> GetInstance(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISOUND + NS_DECL_NSISTREAMLOADEROBSERVER + + private: + virtual ~nsSound(); + + bool mInited; +}; + +#endif /* __nsSound_h__ */ diff --git a/widget/gtk/nsToolkit.cpp b/widget/gtk/nsToolkit.cpp new file mode 100644 index 0000000000..9b4be6fb93 --- /dev/null +++ b/widget/gtk/nsToolkit.cpp @@ -0,0 +1,31 @@ +/* -*- 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 "nscore.h" // needed for 'nullptr' +#include "nsGTKToolkit.h" + +nsGTKToolkit* nsGTKToolkit::gToolkit = nullptr; + +//------------------------------------------------------------------------- +// +// constructor +// +//------------------------------------------------------------------------- +nsGTKToolkit::nsGTKToolkit() : mFocusTimestamp(0) {} + +//------------------------------------------------------------------------------- +// Return the toolkit. If a toolkit does not yet exist, then one will be +// created. +//------------------------------------------------------------------------------- +// static +nsGTKToolkit* nsGTKToolkit::GetToolkit() { + if (!gToolkit) { + gToolkit = new nsGTKToolkit(); + } + + return gToolkit; +} diff --git a/widget/gtk/nsUserIdleServiceGTK.cpp b/widget/gtk/nsUserIdleServiceGTK.cpp new file mode 100644 index 0000000000..882ded8cad --- /dev/null +++ b/widget/gtk/nsUserIdleServiceGTK.cpp @@ -0,0 +1,115 @@ +/* -*- 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 <gtk/gtk.h> + +#include "nsUserIdleServiceGTK.h" +#include "nsDebug.h" +#include "prlink.h" +#include "mozilla/Logging.h" + +using mozilla::LogLevel; + +static mozilla::LazyLogModule sIdleLog("nsIUserIdleService"); + +typedef bool (*_XScreenSaverQueryExtension_fn)(Display* dpy, int* event_base, + int* error_base); + +typedef XScreenSaverInfo* (*_XScreenSaverAllocInfo_fn)(void); + +typedef void (*_XScreenSaverQueryInfo_fn)(Display* dpy, Drawable drw, + XScreenSaverInfo* info); + +static bool sInitialized = false; +static _XScreenSaverQueryExtension_fn _XSSQueryExtension = nullptr; +static _XScreenSaverAllocInfo_fn _XSSAllocInfo = nullptr; +static _XScreenSaverQueryInfo_fn _XSSQueryInfo = nullptr; + +static void Initialize() { + if (!gdk_display_get_default() || + !GDK_IS_X11_DISPLAY(gdk_display_get_default())) { + return; + } + + // This will leak - See comments in ~nsUserIdleServiceGTK(). + PRLibrary* xsslib = PR_LoadLibrary("libXss.so.1"); + if (!xsslib) // ouch. + { + MOZ_LOG(sIdleLog, LogLevel::Warning, ("Failed to find libXss.so!\n")); + return; + } + + _XSSQueryExtension = (_XScreenSaverQueryExtension_fn)PR_FindFunctionSymbol( + xsslib, "XScreenSaverQueryExtension"); + _XSSAllocInfo = (_XScreenSaverAllocInfo_fn)PR_FindFunctionSymbol( + xsslib, "XScreenSaverAllocInfo"); + _XSSQueryInfo = (_XScreenSaverQueryInfo_fn)PR_FindFunctionSymbol( + xsslib, "XScreenSaverQueryInfo"); + + if (!_XSSQueryExtension) + MOZ_LOG(sIdleLog, LogLevel::Warning, + ("Failed to get XSSQueryExtension!\n")); + if (!_XSSAllocInfo) + MOZ_LOG(sIdleLog, LogLevel::Warning, ("Failed to get XSSAllocInfo!\n")); + if (!_XSSQueryInfo) + MOZ_LOG(sIdleLog, LogLevel::Warning, ("Failed to get XSSQueryInfo!\n")); + + sInitialized = true; +} + +nsUserIdleServiceGTK::nsUserIdleServiceGTK() : mXssInfo(nullptr) { + Initialize(); +} + +nsUserIdleServiceGTK::~nsUserIdleServiceGTK() { + if (mXssInfo) XFree(mXssInfo); + +// It is not safe to unload libXScrnSaver until each display is closed because +// the library registers callbacks through XESetCloseDisplay (Bug 397607). +// (Also the library and its functions are scoped for the file not the object.) +#if 0 + if (xsslib) { + PR_UnloadLibrary(xsslib); + xsslib = nullptr; + } +#endif +} + +bool nsUserIdleServiceGTK::PollIdleTime(uint32_t* aIdleTime) { + if (!sInitialized) { + // For some reason, we could not find xscreensaver. + return false; + } + + // Ask xscreensaver about idle time: + *aIdleTime = 0; + + // We might not have a display (cf. in xpcshell) + Display* dplay = GDK_DISPLAY_XDISPLAY(gdk_display_get_default()); + if (!dplay) { + MOZ_LOG(sIdleLog, LogLevel::Warning, ("No display found!\n")); + return false; + } + + if (!_XSSQueryExtension || !_XSSAllocInfo || !_XSSQueryInfo) { + return false; + } + + int event_base, error_base; + if (_XSSQueryExtension(dplay, &event_base, &error_base)) { + if (!mXssInfo) mXssInfo = _XSSAllocInfo(); + if (!mXssInfo) return false; + _XSSQueryInfo(dplay, GDK_ROOT_WINDOW(), mXssInfo); + *aIdleTime = mXssInfo->idle; + return true; + } + // If we get here, we couldn't get to XScreenSaver: + MOZ_LOG(sIdleLog, LogLevel::Warning, ("XSSQueryExtension returned false!\n")); + return false; +} + +bool nsUserIdleServiceGTK::UsePollMode() { return sInitialized; } diff --git a/widget/gtk/nsUserIdleServiceGTK.h b/widget/gtk/nsUserIdleServiceGTK.h new file mode 100644 index 0000000000..9b9ba31846 --- /dev/null +++ b/widget/gtk/nsUserIdleServiceGTK.h @@ -0,0 +1,50 @@ +/* -*- 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 nsUserIdleServiceGTK_h__ +#define nsUserIdleServiceGTK_h__ + +#include "nsUserIdleService.h" +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <gdk/gdkx.h> + +typedef struct { + Window window; // Screen saver window + int state; // ScreenSaver(Off,On,Disabled) + int kind; // ScreenSaver(Blanked,Internal,External) + unsigned long til_or_since; // milliseconds since/til screensaver kicks in + unsigned long idle; // milliseconds idle + unsigned long event_mask; // event stuff +} XScreenSaverInfo; + +class nsUserIdleServiceGTK : public nsUserIdleService { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceGTK, nsUserIdleService) + + virtual bool PollIdleTime(uint32_t* aIdleTime) override; + + static already_AddRefed<nsUserIdleServiceGTK> GetInstance() { + RefPtr<nsUserIdleServiceGTK> idleService = + nsUserIdleService::GetInstance().downcast<nsUserIdleServiceGTK>(); + if (!idleService) { + idleService = new nsUserIdleServiceGTK(); + } + + return idleService.forget(); + } + + private: + ~nsUserIdleServiceGTK(); + XScreenSaverInfo* mXssInfo; + + protected: + nsUserIdleServiceGTK(); + virtual bool UsePollMode() override; +}; + +#endif // nsUserIdleServiceGTK_h__ diff --git a/widget/gtk/nsWaylandDisplay.cpp b/widget/gtk/nsWaylandDisplay.cpp new file mode 100644 index 0000000000..310917a27b --- /dev/null +++ b/widget/gtk/nsWaylandDisplay.cpp @@ -0,0 +1,309 @@ +/* -*- 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 "base/message_loop.h" // for MessageLoop +#include "base/task.h" // for NewRunnableMethod, etc +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPrefs_widget.h" + +namespace mozilla { +namespace widget { + +// nsWaylandDisplay needs to be created for each calling thread(main thread, +// compositor thread and render thread) +#define MAX_DISPLAY_CONNECTIONS 10 + +// An array of active wayland displays. We need a display for every thread +// where is wayland interface used as we need to dispatch waylands events +// there. +static RefPtr<nsWaylandDisplay> gWaylandDisplays[MAX_DISPLAY_CONNECTIONS]; +static StaticMutex gWaylandDisplayArrayWriteMutex; + +// Dispatch events to Compositor/Render queues +void WaylandDispatchDisplays() { + MOZ_ASSERT(NS_IsMainThread(), + "WaylandDispatchDisplays() is supposed to run in main thread"); + for (auto& display : gWaylandDisplays) { + if (display) { + display->DispatchEventQueue(); + } + } +} + +void WaylandDisplayRelease() { + StaticMutexAutoLock lock(gWaylandDisplayArrayWriteMutex); + for (auto& display : gWaylandDisplays) { + if (display) { + display = nullptr; + } + } +} + +// Get WaylandDisplay for given wl_display and actual calling thread. +RefPtr<nsWaylandDisplay> WaylandDisplayGet(GdkDisplay* aGdkDisplay) { + wl_display* waylandDisplay = WaylandDisplayGetWLDisplay(aGdkDisplay); + if (!waylandDisplay) { + return nullptr; + } + + // Search existing display connections for wl_display:thread combination. + for (auto& display : gWaylandDisplays) { + if (display && display->Matches(waylandDisplay)) { + return display; + } + } + + StaticMutexAutoLock arrayLock(gWaylandDisplayArrayWriteMutex); + for (auto& display : gWaylandDisplays) { + if (display == nullptr) { + display = new nsWaylandDisplay(waylandDisplay); + return display; + } + } + + MOZ_CRASH("There's too many wayland display conections!"); + return nullptr; +} + +wl_display* WaylandDisplayGetWLDisplay(GdkDisplay* aGdkDisplay) { + if (!aGdkDisplay) { + aGdkDisplay = gdk_display_get_default(); + if (!aGdkDisplay || GDK_IS_X11_DISPLAY(aGdkDisplay)) { + return nullptr; + } + } + + return gdk_wayland_display_get_wl_display(aGdkDisplay); +} + +void nsWaylandDisplay::SetShm(wl_shm* aShm) { mShm = aShm; } + +void nsWaylandDisplay::SetCompositor(wl_compositor* aCompositor) { + mCompositor = aCompositor; +} + +void nsWaylandDisplay::SetSubcompositor(wl_subcompositor* aSubcompositor) { + mSubcompositor = aSubcompositor; +} + +void nsWaylandDisplay::SetDataDeviceManager( + wl_data_device_manager* aDataDeviceManager) { + mDataDeviceManager = aDataDeviceManager; +} + +void nsWaylandDisplay::SetSeat(wl_seat* aSeat) { mSeat = aSeat; } + +void nsWaylandDisplay::SetPrimarySelectionDeviceManager( + gtk_primary_selection_device_manager* aPrimarySelectionDeviceManager) { + mPrimarySelectionDeviceManagerGtk = aPrimarySelectionDeviceManager; +} + +void nsWaylandDisplay::SetPrimarySelectionDeviceManager( + zwp_primary_selection_device_manager_v1* aPrimarySelectionDeviceManager) { + mPrimarySelectionDeviceManagerZwpV1 = aPrimarySelectionDeviceManager; +} + +void nsWaylandDisplay::SetIdleInhibitManager( + zwp_idle_inhibit_manager_v1* aIdleInhibitManager) { + mIdleInhibitManager = aIdleInhibitManager; +} + +static void global_registry_handler(void* data, wl_registry* registry, + uint32_t id, const char* interface, + uint32_t version) { + auto* display = static_cast<nsWaylandDisplay*>(data); + if (!display) { + return; + } + + if (strcmp(interface, "wl_shm") == 0) { + auto* shm = WaylandRegistryBind<wl_shm>(registry, id, &wl_shm_interface, 1); + wl_proxy_set_queue((struct wl_proxy*)shm, display->GetEventQueue()); + display->SetShm(shm); + } else if (strcmp(interface, "wl_data_device_manager") == 0) { + int data_device_manager_version = MIN(version, 3); + auto* data_device_manager = WaylandRegistryBind<wl_data_device_manager>( + registry, id, &wl_data_device_manager_interface, + data_device_manager_version); + wl_proxy_set_queue((struct wl_proxy*)data_device_manager, + display->GetEventQueue()); + display->SetDataDeviceManager(data_device_manager); + } else if (strcmp(interface, "wl_seat") == 0) { + auto* seat = + WaylandRegistryBind<wl_seat>(registry, id, &wl_seat_interface, 1); + wl_proxy_set_queue((struct wl_proxy*)seat, display->GetEventQueue()); + display->SetSeat(seat); + } else if (strcmp(interface, "gtk_primary_selection_device_manager") == 0) { + auto* primary_selection_device_manager = + WaylandRegistryBind<gtk_primary_selection_device_manager>( + registry, id, >k_primary_selection_device_manager_interface, 1); + wl_proxy_set_queue((struct wl_proxy*)primary_selection_device_manager, + display->GetEventQueue()); + display->SetPrimarySelectionDeviceManager(primary_selection_device_manager); + } else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == + 0) { + auto* primary_selection_device_manager = + WaylandRegistryBind<gtk_primary_selection_device_manager>( + registry, id, &zwp_primary_selection_device_manager_v1_interface, + 1); + wl_proxy_set_queue((struct wl_proxy*)primary_selection_device_manager, + display->GetEventQueue()); + display->SetPrimarySelectionDeviceManager(primary_selection_device_manager); + } else if (strcmp(interface, "zwp_idle_inhibit_manager_v1") == 0) { + auto* idle_inhibit_manager = + WaylandRegistryBind<zwp_idle_inhibit_manager_v1>( + registry, id, &zwp_idle_inhibit_manager_v1_interface, 1); + wl_proxy_set_queue((struct wl_proxy*)idle_inhibit_manager, + display->GetEventQueue()); + display->SetIdleInhibitManager(idle_inhibit_manager); + } else if (strcmp(interface, "wl_compositor") == 0) { + // Requested wl_compositor version 4 as we need wl_surface_damage_buffer(). + auto* compositor = WaylandRegistryBind<wl_compositor>( + registry, id, &wl_compositor_interface, 4); + wl_proxy_set_queue((struct wl_proxy*)compositor, display->GetEventQueue()); + display->SetCompositor(compositor); + } else if (strcmp(interface, "wl_subcompositor") == 0) { + auto* subcompositor = WaylandRegistryBind<wl_subcompositor>( + registry, id, &wl_subcompositor_interface, 1); + wl_proxy_set_queue((struct wl_proxy*)subcompositor, + display->GetEventQueue()); + display->SetSubcompositor(subcompositor); + } +} + +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}; + +bool nsWaylandDisplay::DispatchEventQueue() { + if (mEventQueue) { + wl_display_dispatch_queue_pending(mDisplay, mEventQueue); + } + return true; +} + +void nsWaylandDisplay::SyncEnd() { + wl_callback_destroy(mSyncCallback); + mSyncCallback = nullptr; +} + +static void wayland_sync_callback(void* data, struct wl_callback* callback, + uint32_t time) { + auto display = static_cast<nsWaylandDisplay*>(data); + display->SyncEnd(); +} + +static const struct wl_callback_listener sync_callback_listener = { + .done = wayland_sync_callback}; + +void nsWaylandDisplay::SyncBegin() { + WaitForSyncEnd(); + + // Use wl_display_sync() to synchronize wayland events. + // See dri2_wl_swap_buffers_with_damage() from MESA + // or wl_display_roundtrip_queue() from wayland-client. + struct wl_display* displayWrapper = + static_cast<wl_display*>(wl_proxy_create_wrapper((void*)mDisplay)); + if (!displayWrapper) { + NS_WARNING("Failed to create wl_proxy wrapper!"); + return; + } + + wl_proxy_set_queue((struct wl_proxy*)displayWrapper, mEventQueue); + mSyncCallback = wl_display_sync(displayWrapper); + wl_proxy_wrapper_destroy((void*)displayWrapper); + + if (!mSyncCallback) { + NS_WARNING("Failed to create wl_display_sync callback!"); + return; + } + + wl_callback_add_listener(mSyncCallback, &sync_callback_listener, this); + wl_display_flush(mDisplay); +} + +void nsWaylandDisplay::QueueSyncBegin() { + RefPtr<nsWaylandDisplay> self(this); + NS_DispatchToMainThread( + NS_NewRunnableFunction("nsWaylandDisplay::QueueSyncBegin", + [self]() -> void { self->SyncBegin(); })); +} + +void nsWaylandDisplay::WaitForSyncEnd() { + // We're done here + if (!mSyncCallback) { + return; + } + + while (mSyncCallback != nullptr) { + // TODO: wl_display_dispatch_queue() should not be called while + // glib main loop is iterated at nsAppShell::ProcessNextNativeEvent(). + if (wl_display_dispatch_queue(mDisplay, mEventQueue) == -1) { + NS_WARNING("wl_display_dispatch_queue failed!"); + SyncEnd(); + return; + } + } +} + +bool nsWaylandDisplay::Matches(wl_display* aDisplay) { + return mThreadId == PR_GetCurrentThread() && aDisplay == mDisplay; +} + +nsWaylandDisplay::nsWaylandDisplay(wl_display* aDisplay, bool aLighWrapper) + : mThreadId(PR_GetCurrentThread()), + mDisplay(aDisplay), + mEventQueue(nullptr), + mDataDeviceManager(nullptr), + mCompositor(nullptr), + mSubcompositor(nullptr), + mSeat(nullptr), + mShm(nullptr), + mSyncCallback(nullptr), + mPrimarySelectionDeviceManagerGtk(nullptr), + mPrimarySelectionDeviceManagerZwpV1(nullptr), + mIdleInhibitManager(nullptr), + mRegistry(nullptr), + mExplicitSync(false) { + if (!aLighWrapper) { + mRegistry = wl_display_get_registry(mDisplay); + wl_registry_add_listener(mRegistry, ®istry_listener, this); + } + + if (!NS_IsMainThread()) { + mEventQueue = wl_display_create_queue(mDisplay); + wl_proxy_set_queue((struct wl_proxy*)mRegistry, mEventQueue); + } + + if (!aLighWrapper) { + if (mEventQueue) { + wl_display_roundtrip_queue(mDisplay, mEventQueue); + wl_display_roundtrip_queue(mDisplay, mEventQueue); + } else { + wl_display_roundtrip(mDisplay); + wl_display_roundtrip(mDisplay); + } + } +} + +nsWaylandDisplay::~nsWaylandDisplay() { + wl_registry_destroy(mRegistry); + mRegistry = nullptr; + + if (mEventQueue) { + wl_event_queue_destroy(mEventQueue); + mEventQueue = nullptr; + } + mDisplay = nullptr; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/nsWaylandDisplay.h b/widget/gtk/nsWaylandDisplay.h new file mode 100644 index 0000000000..6beb7ef684 --- /dev/null +++ b/widget/gtk/nsWaylandDisplay.h @@ -0,0 +1,130 @@ +/* -*- 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_WAYLAND_DISPLAY_H__ +#define __MOZ_WAYLAND_DISPLAY_H__ + +#include "DMABufLibWrapper.h" + +#include "mozilla/widget/mozwayland.h" +#include "mozilla/widget/gbm.h" +#include "mozilla/widget/gtk-primary-selection-client-protocol.h" +#include "mozilla/widget/idle-inhibit-unstable-v1-client-protocol.h" +#include "mozilla/widget/linux-dmabuf-unstable-v1-client-protocol.h" +#include "mozilla/widget/primary-selection-unstable-v1-client-protocol.h" + +namespace mozilla { +namespace widget { + +// Our general connection to Wayland display server, +// holds our display connection and runs event loop. +// We have a global nsWaylandDisplay object for each thread. +class nsWaylandDisplay { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsWaylandDisplay) + + // Create nsWaylandDisplay object on top of native Wayland wl_display + // connection. When aLighWrapper is set we don't get wayland registry + // objects and only event loop is provided. + explicit nsWaylandDisplay(wl_display* aDisplay, bool aLighWrapper = false); + + bool DispatchEventQueue(); + + void SyncBegin(); + void QueueSyncBegin(); + void SyncEnd(); + void WaitForSyncEnd(); + + bool Matches(wl_display* aDisplay); + + wl_display* GetDisplay() { return mDisplay; }; + wl_event_queue* GetEventQueue() { return mEventQueue; }; + wl_compositor* GetCompositor(void) { return mCompositor; }; + wl_subcompositor* GetSubcompositor(void) { return mSubcompositor; }; + wl_data_device_manager* GetDataDeviceManager(void) { + return mDataDeviceManager; + }; + wl_seat* GetSeat(void) { return mSeat; }; + wl_shm* GetShm(void) { return mShm; }; + gtk_primary_selection_device_manager* GetPrimarySelectionDeviceManagerGtk( + void) { + return mPrimarySelectionDeviceManagerGtk; + }; + zwp_primary_selection_device_manager_v1* + GetPrimarySelectionDeviceManagerZwpV1(void) { + return mPrimarySelectionDeviceManagerZwpV1; + }; + zwp_idle_inhibit_manager_v1* GetIdleInhibitManager(void) { + return mIdleInhibitManager; + } + + bool IsMainThreadDisplay() { return mEventQueue == nullptr; } + + void SetShm(wl_shm* aShm); + void SetCompositor(wl_compositor* aCompositor); + void SetSubcompositor(wl_subcompositor* aSubcompositor); + void SetDataDeviceManager(wl_data_device_manager* aDataDeviceManager); + void SetSeat(wl_seat* aSeat); + void SetPrimarySelectionDeviceManager( + gtk_primary_selection_device_manager* aPrimarySelectionDeviceManager); + void SetPrimarySelectionDeviceManager( + zwp_primary_selection_device_manager_v1* aPrimarySelectionDeviceManager); + void SetIdleInhibitManager(zwp_idle_inhibit_manager_v1* aIdleInhibitManager); + + bool IsExplicitSyncEnabled() { return mExplicitSync; } + + private: + ~nsWaylandDisplay(); + + PRThread* mThreadId; + wl_display* mDisplay; + wl_event_queue* mEventQueue; + wl_data_device_manager* mDataDeviceManager; + wl_compositor* mCompositor; + wl_subcompositor* mSubcompositor; + wl_seat* mSeat; + wl_shm* mShm; + wl_callback* mSyncCallback; + gtk_primary_selection_device_manager* mPrimarySelectionDeviceManagerGtk; + zwp_primary_selection_device_manager_v1* mPrimarySelectionDeviceManagerZwpV1; + zwp_idle_inhibit_manager_v1* mIdleInhibitManager; + wl_registry* mRegistry; + bool mExplicitSync; +}; + +void WaylandDispatchDisplays(); +void WaylandDisplayRelease(); + +RefPtr<nsWaylandDisplay> WaylandDisplayGet(GdkDisplay* aGdkDisplay = nullptr); +wl_display* WaylandDisplayGetWLDisplay(GdkDisplay* aGdkDisplay = nullptr); + +} // namespace widget +} // namespace mozilla + +template <class T> +static inline T* WaylandRegistryBind(struct wl_registry* wl_registry, + uint32_t name, + const struct wl_interface* interface, + uint32_t version) { + struct wl_proxy* id; + + // When libwayland-client does not provide this symbol, it will be + // linked to the fallback in libmozwayland, which returns NULL. + id = wl_proxy_marshal_constructor_versioned( + (struct wl_proxy*)wl_registry, WL_REGISTRY_BIND, interface, version, name, + interface->name, version, nullptr); + + if (id == nullptr) { + id = wl_proxy_marshal_constructor((struct wl_proxy*)wl_registry, + WL_REGISTRY_BIND, interface, name, + interface->name, version, nullptr); + } + + return reinterpret_cast<T*>(id); +} + +#endif // __MOZ_WAYLAND_DISPLAY_H__ diff --git a/widget/gtk/nsWidgetFactory.cpp b/widget/gtk/nsWidgetFactory.cpp new file mode 100644 index 0000000000..d649c7e0bf --- /dev/null +++ b/widget/gtk/nsWidgetFactory.cpp @@ -0,0 +1,74 @@ +/* -*- 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 "nsWidgetFactory.h" + +#include "mozilla/Components.h" +#include "mozilla/WidgetUtils.h" +#include "NativeKeyBindings.h" +#include "nsWidgetsCID.h" +#include "nsAppShell.h" +#include "nsAppShellSingleton.h" +#include "nsBaseWidget.h" +#include "nsGtkKeyUtils.h" +#include "nsLookAndFeel.h" +#include "nsWindow.h" +#include "nsHTMLFormatConverter.h" +#include "HeadlessClipboard.h" +#include "IMContextWrapper.h" +#ifdef MOZ_X11 +# include "nsClipboard.h" +#endif +#include "TaskbarProgress.h" +#include "nsFilePicker.h" +#include "nsSound.h" +#include "nsGTKToolkit.h" +#include "WakeLockListener.h" + +#include "mozilla/gfx/2D.h" +#include "mozilla/widget/ScreenManager.h" +#include <gtk/gtk.h> + +using namespace mozilla; +using namespace mozilla::widget; + +#ifdef MOZ_X11 +NS_IMPL_COMPONENT_FACTORY(nsIClipboard) { + nsCOMPtr<nsIClipboard> inst; + if (gfxPlatform::IsHeadless()) { + inst = new HeadlessClipboard(); + } else { + auto clipboard = MakeRefPtr<nsClipboard>(); + if (NS_FAILED(clipboard->Init())) { + return nullptr; + } + inst = std::move(clipboard); + } + + return inst.forget().downcast<nsISupports>(); +} +#endif + +nsresult nsWidgetGtk2ModuleCtor() { return nsAppShellInit(); } + +void nsWidgetGtk2ModuleDtor() { + // Shutdown all XP level widget classes. + WidgetUtils::Shutdown(); + + NativeKeyBindings::Shutdown(); + nsLookAndFeel::Shutdown(); + nsFilePicker::Shutdown(); + nsSound::Shutdown(); + nsWindow::ReleaseGlobals(); + IMContextWrapper::Shutdown(); + KeymapWrapper::Shutdown(); + nsGTKToolkit::Shutdown(); + nsAppShellShutdown(); +#ifdef MOZ_ENABLE_DBUS + WakeLockListener::Shutdown(); +#endif +} diff --git a/widget/gtk/nsWidgetFactory.h b/widget/gtk/nsWidgetFactory.h new file mode 100644 index 0000000000..21ea0da667 --- /dev/null +++ b/widget/gtk/nsWidgetFactory.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_gtk_nsWidgetFactory_h +#define widget_gtk_nsWidgetFactory_h + +#include "nscore.h" +#include "nsID.h" + +class nsISupports; + +nsresult nsAppShellConstructor(nsISupports* outer, const nsIID& iid, + void** result); + +nsresult nsWidgetGtk2ModuleCtor(); +void nsWidgetGtk2ModuleDtor(); + +#endif // defined widget_gtk_nsWidgetFactory_h diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp new file mode 100644 index 0000000000..f435464f7a --- /dev/null +++ b/widget/gtk/nsWindow.cpp @@ -0,0 +1,8420 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 "nsWindow.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/EventForwards.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WidgetUtils.h" +#include "mozilla/X11Util.h" +#include "mozilla/XREAppData.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "InputData.h" +#include "nsAppRunner.h" +#include <algorithm> + +#include "GeckoProfiler.h" + +#include "prlink.h" +#include "nsGTKToolkit.h" +#include "nsIRollupListener.h" +#include "nsINode.h" + +#include "nsWidgetsCID.h" +#include "nsDragService.h" +#include "nsIWidgetListener.h" +#include "nsIScreenManager.h" +#include "SystemTimeConverter.h" +#include "nsViewManager.h" +#include "nsMenuPopupFrame.h" +#include "nsXPLookAndFeel.h" + +#include "nsGtkKeyUtils.h" +#include "nsGtkCursors.h" +#include "ScreenHelperGTK.h" + +#include <gtk/gtk.h> +#include <gtk/gtkx.h> + +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +#endif /* MOZ_WAYLAND */ + +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +# include <X11/Xatom.h> +# include <X11/extensions/XShm.h> +# include <X11/extensions/shape.h> +# include <gdk/gdkkeysyms-compat.h> +#endif /* MOZ_X11 */ + +#include <gdk/gdkkeysyms.h> + +#if defined(MOZ_WAYLAND) +# include <gdk/gdkwayland.h> +# include "nsView.h" +#endif + +#include "nsGkAtoms.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" +#include "mozilla/Preferences.h" +#include "nsGfxCIID.h" +#include "nsGtkUtils.h" +#include "nsLayoutUtils.h" +#include "mozilla/layers/LayersTypes.h" +#include "nsIUserIdleServiceInternal.h" +#include "GLContext.h" +#include "gfx2DGlue.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/Accessible.h" +# include "mozilla/a11y/Platform.h" +# include "nsAccessibilityService.h" + +using namespace mozilla; +using namespace mozilla::widget; +#endif + +/* For SetIcon */ +#include "nsAppDirectoryServiceDefs.h" +#include "nsString.h" +#include "nsIFile.h" + +/* SetCursor(imgIContainer*) */ +#include <gdk/gdk.h> +#include <wchar.h> +#include "imgIContainer.h" +#include "nsGfxCIID.h" +#include "nsImageToPixbuf.h" +#include "nsIInterfaceRequestorUtils.h" +#include "ClientLayerManager.h" +#include "nsIGSettingsService.h" + +#include "gfxPlatformGtk.h" +#include "gfxContext.h" +#include "gfxImageSurface.h" +#include "gfxUtils.h" +#include "Layers.h" +#include "GLContextProvider.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/HelpersCairo.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/layers/KnowsCompositor.h" +#include "mozilla/layers/WebRenderBridgeChild.h" +#include "mozilla/layers/WebRenderLayerManager.h" + +#include "mozilla/layers/APZInputBridge.h" +#include "mozilla/layers/IAPZCTreeManager.h" + +#ifdef MOZ_X11 +# include "mozilla/gfx/gfxVars.h" +# include "GLContextGLX.h" // for GLContextGLX::FindVisual() +# include "GLContextEGL.h" // for GLContextEGL::FindVisual() +# include "GtkCompositorWidget.h" +# include "gfxXlibSurface.h" +# include "WindowSurfaceX11Image.h" +# include "WindowSurfaceX11SHM.h" +# include "WindowSurfaceXRender.h" +#endif // MOZ_X11 +#ifdef MOZ_WAYLAND +# include "nsIClipboard.h" +#endif + +#include "nsShmImage.h" +#include "gtkdrawing.h" + +#include "NativeKeyBindings.h" + +#include <dlfcn.h> +#include "nsPresContext.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; +using namespace mozilla::layers; +using mozilla::gl::GLContextEGL; +using mozilla::gl::GLContextGLX; + +// Don't put more than this many rects in the dirty region, just fluff +// out to the bounding-box if there are more +#define MAX_RECTS_IN_REGION 100 + +#if !GTK_CHECK_VERSION(3, 18, 0) + +struct _GdkEventTouchpadPinch { + GdkEventType type; + GdkWindow* window; + gint8 send_event; + gint8 phase; + gint8 n_fingers; + guint32 time; + gdouble x; + gdouble y; + gdouble dx; + gdouble dy; + gdouble angle_delta; + gdouble scale; + gdouble x_root, y_root; + guint state; +}; + +typedef enum { + GDK_TOUCHPAD_GESTURE_PHASE_BEGIN, + GDK_TOUCHPAD_GESTURE_PHASE_UPDATE, + GDK_TOUCHPAD_GESTURE_PHASE_END, + GDK_TOUCHPAD_GESTURE_PHASE_CANCEL +} GdkTouchpadGesturePhase; + +GdkEventMask GDK_TOUCHPAD_GESTURE_MASK = static_cast<GdkEventMask>(1 << 24); +GdkEventType GDK_TOUCHPAD_PINCH = static_cast<GdkEventType>(42); + +#endif + +const gint kEvents = GDK_TOUCHPAD_GESTURE_MASK | GDK_EXPOSURE_MASK | + GDK_STRUCTURE_MASK | GDK_VISIBILITY_NOTIFY_MASK | + GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_SMOOTH_SCROLL_MASK | GDK_TOUCH_MASK | GDK_SCROLL_MASK | + GDK_POINTER_MOTION_MASK | GDK_PROPERTY_CHANGE_MASK; + +#if !GTK_CHECK_VERSION(3, 22, 0) +typedef enum { + GDK_ANCHOR_FLIP_X = 1 << 0, + GDK_ANCHOR_FLIP_Y = 1 << 1, + GDK_ANCHOR_SLIDE_X = 1 << 2, + GDK_ANCHOR_SLIDE_Y = 1 << 3, + GDK_ANCHOR_RESIZE_X = 1 << 4, + GDK_ANCHOR_RESIZE_Y = 1 << 5, + GDK_ANCHOR_FLIP = GDK_ANCHOR_FLIP_X | GDK_ANCHOR_FLIP_Y, + GDK_ANCHOR_SLIDE = GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_SLIDE_Y, + GDK_ANCHOR_RESIZE = GDK_ANCHOR_RESIZE_X | GDK_ANCHOR_RESIZE_Y +} GdkAnchorHints; +#endif + +/* utility functions */ +static bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX, + gdouble aMouseY); +static nsWindow* get_window_for_gtk_widget(GtkWidget* widget); +static nsWindow* get_window_for_gdk_window(GdkWindow* window); +static GtkWidget* get_gtk_widget_for_gdk_window(GdkWindow* window); +static GdkCursor* get_gtk_cursor(nsCursor aCursor); + +static GdkWindow* get_inner_gdk_window(GdkWindow* aWindow, gint x, gint y, + gint* retx, gint* rety); + +static int is_parent_ungrab_enter(GdkEventCrossing* aEvent); +static int is_parent_grab_leave(GdkEventCrossing* aEvent); + +/* callbacks from widgets */ +static gboolean expose_event_cb(GtkWidget* widget, cairo_t* rect); +static gboolean configure_event_cb(GtkWidget* widget, GdkEventConfigure* event); +static void container_unrealize_cb(GtkWidget* widget); +static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation); +static void toplevel_window_size_allocate_cb(GtkWidget* widget, + GtkAllocation* allocation); +static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event); +static gboolean enter_notify_event_cb(GtkWidget* widget, + GdkEventCrossing* event); +static gboolean leave_notify_event_cb(GtkWidget* widget, + GdkEventCrossing* event); +static gboolean motion_notify_event_cb(GtkWidget* widget, + GdkEventMotion* event); +static gboolean button_press_event_cb(GtkWidget* widget, GdkEventButton* event); +static gboolean button_release_event_cb(GtkWidget* widget, + GdkEventButton* event); +static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event); +static gboolean focus_out_event_cb(GtkWidget* widget, GdkEventFocus* event); +static gboolean key_press_event_cb(GtkWidget* widget, GdkEventKey* event); +static gboolean key_release_event_cb(GtkWidget* widget, GdkEventKey* event); +static gboolean property_notify_event_cb(GtkWidget* widget, + GdkEventProperty* event); +static gboolean scroll_event_cb(GtkWidget* widget, GdkEventScroll* event); + +static void hierarchy_changed_cb(GtkWidget* widget, + GtkWidget* previous_toplevel); +static gboolean window_state_event_cb(GtkWidget* widget, + GdkEventWindowState* event); +static void settings_changed_cb(GtkSettings* settings, GParamSpec* pspec, + nsWindow* data); +static void settings_xft_dpi_changed_cb(GtkSettings* settings, + GParamSpec* pspec, nsWindow* data); +static void check_resize_cb(GtkContainer* container, gpointer user_data); +static void screen_composited_changed_cb(GdkScreen* screen, gpointer user_data); +static void widget_composited_changed_cb(GtkWidget* widget, gpointer user_data); + +static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec, + gpointer aPointer); +static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent); +static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent); + +static nsWindow* GetFirstNSWindowForGDKWindow(GdkWindow* aGdkWindow); + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ +#ifdef MOZ_X11 +static GdkFilterReturn popup_take_focus_filter(GdkXEvent* gdk_xevent, + GdkEvent* event, gpointer data); +#endif /* MOZ_X11 */ +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +static gboolean drag_motion_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, guint aTime, gpointer aData); +static void drag_leave_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, guint aTime, + gpointer aData); +static gboolean drag_drop_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, guint aTime, gpointer aData); +static void drag_data_received_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, + GtkSelectionData* aSelectionData, + guint aInfo, guint32 aTime, + gpointer aData); + +/* initialization static functions */ +static nsresult initialize_prefs(void); + +static guint32 sLastUserInputTime = GDK_CURRENT_TIME; +static guint32 sRetryGrabTime; + +static SystemTimeConverter<guint32>& TimeConverter() { + static SystemTimeConverter<guint32> sTimeConverterSingleton; + return sTimeConverterSingleton; +} + +nsWindow::CSDSupportLevel nsWindow::sCSDSupportLevel = CSD_SUPPORT_UNKNOWN; +bool nsWindow::sTransparentMainWindow = false; +static bool sIgnoreChangedSettings = false; + +void nsWindow::WithSettingsChangesIgnored(const std::function<void()>& aFn) { + AutoRestore ar(sIgnoreChangedSettings); + sIgnoreChangedSettings = true; + aFn(); +} + +namespace mozilla { + +class CurrentX11TimeGetter { + public: + explicit CurrentX11TimeGetter(GdkWindow* aWindow) + : mWindow(aWindow), mAsyncUpdateStart() {} + + guint32 GetCurrentTime() const { return gdk_x11_get_server_time(mWindow); } + + void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) { + // Check for in-flight request + if (!mAsyncUpdateStart.IsNull()) { + return; + } + mAsyncUpdateStart = aNow; + + Display* xDisplay = GDK_WINDOW_XDISPLAY(mWindow); + Window xWindow = GDK_WINDOW_XID(mWindow); + unsigned char c = 'a'; + Atom timeStampPropAtom = TimeStampPropAtom(); + XChangeProperty(xDisplay, xWindow, timeStampPropAtom, timeStampPropAtom, 8, + PropModeReplace, &c, 1); + XFlush(xDisplay); + } + + gboolean PropertyNotifyHandler(GtkWidget* aWidget, GdkEventProperty* aEvent) { + if (aEvent->atom != gdk_x11_xatom_to_atom(TimeStampPropAtom())) { + return FALSE; + } + + guint32 eventTime = aEvent->time; + TimeStamp lowerBound = mAsyncUpdateStart; + + TimeConverter().CompensateForBackwardsSkew(eventTime, lowerBound); + mAsyncUpdateStart = TimeStamp(); + return TRUE; + } + + private: + static Atom TimeStampPropAtom() { + return gdk_x11_get_xatom_by_name_for_display(gdk_display_get_default(), + "GDK_TIMESTAMP_PROP"); + } + + // This is safe because this class is stored as a member of mWindow and + // won't outlive it. + GdkWindow* mWindow; + TimeStamp mAsyncUpdateStart; +}; + +} // namespace mozilla + +static NS_DEFINE_IID(kCDragServiceCID, NS_DRAGSERVICE_CID); + +// The window from which the focus manager asks us to dispatch key events. +static nsWindow* gFocusWindow = nullptr; +static bool gBlockActivateEvent = false; +static bool gGlobalsInitialized = false; +static bool gRaiseWindows = true; +static bool gUseWaylandVsync = true; +static bool gUseWaylandUseOpaqueRegion = true; +static bool gUseAspectRatio = true; +static GList* gVisibleWaylandPopupWindows = nullptr; +static uint32_t gLastTouchID = 0; + +#define NS_WINDOW_TITLE_MAX_LENGTH 4095 + +// If after selecting profile window, the startup fail, please refer to +// http://bugzilla.gnome.org/show_bug.cgi?id=88940 + +// needed for imgIContainer cursors +// GdkDisplay* was added in 2.2 +typedef struct _GdkDisplay GdkDisplay; + +#define kWindowPositionSlop 20 + +// cursor cache +static GdkCursor* gCursorCache[eCursorCount]; + +static GtkWidget* gInvisibleContainer = nullptr; + +// Sometimes this actually also includes the state of the modifier keys, but +// only the button state bits are used. +static guint gButtonState; + +static inline int32_t GetBitmapStride(int32_t width) { +#if defined(MOZ_X11) + return (width + 7) / 8; +#else + return cairo_format_stride_for_width(CAIRO_FORMAT_A1, width); +#endif +} + +static inline bool TimestampIsNewerThan(guint32 a, guint32 b) { + // Timestamps are just the least significant bits of a monotonically + // increasing function, and so the use of unsigned overflow arithmetic. + return a - b <= G_MAXUINT32 / 2; +} + +static void UpdateLastInputEventTime(void* aGdkEvent) { + nsCOMPtr<nsIUserIdleServiceInternal> idleService = + do_GetService("@mozilla.org/widget/useridleservice;1"); + if (idleService) { + idleService->ResetIdleTimeOut(0); + } + + guint timestamp = gdk_event_get_time(static_cast<GdkEvent*>(aGdkEvent)); + if (timestamp == GDK_CURRENT_TIME) return; + + sLastUserInputTime = timestamp; +} + +void GetWindowOrigin(GdkWindow* aWindow, int* aX, int* aY) { + if (aWindow) { + gdk_window_get_origin(aWindow, aX, aY); + } + + // TODO(bug 1655924): gdk_window_get_origin is can block waiting for the x + // server for a long time, we would like to use the implementation below + // instead. However, removing the synchronous x server queries causes a race + // condition to surface, causing issues such as bug 1652743 and bug 1653711. +#if 0 + *aX = 0; + *aY = 0; + if (!aWindow) { + return; + } + + GdkWindow* current = aWindow; + while (GdkWindow* parent = gdk_window_get_parent(current)) { + if (parent == current) { + break; + } + + int x = 0; + int y = 0; + gdk_window_get_position(current, &x, &y); + *aX += x; + *aY += y; + + current = parent; + } +#endif +} + +nsWindow::nsWindow() { + mIsTopLevel = false; + mIsDestroyed = false; + mListenForResizes = false; + mNeedsDispatchResized = false; + mIsShown = false; + mNeedsShow = false; + mEnabled = true; + mCreated = false; + mHandleTouchEvent = false; + mIsDragPopup = false; + mIsX11Display = gfxPlatformGtk::GetPlatform()->IsX11Display(); + + mContainer = nullptr; + mGdkWindow = nullptr; + mShell = nullptr; + mCompositorWidgetDelegate = nullptr; + mHasMappedToplevel = false; + mRetryPointerGrab = false; + mWindowType = eWindowType_child; + mSizeState = nsSizeMode_Normal; + mBoundsAreValid = true; + mAspectRatio = 0.0f; + mAspectRatioSaved = 0.0f; + mLastSizeMode = nsSizeMode_Normal; + mSizeConstraints.mMaxSize = GetSafeWindowSize(mSizeConstraints.mMaxSize); + +#ifdef MOZ_X11 + mOldFocusWindow = 0; + + mXDisplay = nullptr; + mXWindow = X11None; + mXVisual = nullptr; + mXDepth = 0; +#endif /* MOZ_X11 */ + +#ifdef MOZ_WAYLAND + mNeedsCompositorResume = false; + mCompositorInitiallyPaused = false; +#endif + mWaitingForMoveToRectCB = false; + mPendingSizeRect = LayoutDeviceIntRect(0, 0, 0, 0); + + if (!gGlobalsInitialized) { + gGlobalsInitialized = true; + + // It's OK if either of these fail, but it may not be one day. + initialize_prefs(); + +#ifdef MOZ_WAYLAND + // Wayland provides clipboard data to application on focus-in event + // so we need to init our clipboard hooks before we create window + // and get focus. + if (!mIsX11Display) { + nsCOMPtr<nsIClipboard> clipboard = + do_GetService("@mozilla.org/widget/clipboard;1"); + NS_ASSERTION(clipboard, "Failed to init clipboard!"); + } +#endif + } + + mLastMotionPressure = 0; + +#ifdef ACCESSIBILITY + mRootAccessible = nullptr; +#endif + + mIsTransparent = false; + mTransparencyBitmap = nullptr; + mTransparencyBitmapForTitlebar = false; + + mTransparencyBitmapWidth = 0; + mTransparencyBitmapHeight = 0; + + mLastScrollEventTime = GDK_CURRENT_TIME; + + mPendingConfigures = 0; + mCSDSupportLevel = CSD_SUPPORT_NONE; + mDrawToContainer = false; + mDrawInTitlebar = false; + mTitlebarBackdropState = false; + + mHasAlphaVisual = false; + mIsPIPWindow = false; + mAlwaysOnTop = false; + + mWindowScaleFactorChanged = true; + mWindowScaleFactor = 1; + + mCompositedScreen = gdk_screen_is_composited(gdk_screen_get_default()); +} + +nsWindow::~nsWindow() { + LOG(("nsWindow::~nsWindow() [%p]\n", (void*)this)); + + delete[] mTransparencyBitmap; + mTransparencyBitmap = nullptr; + + Destroy(); +} + +/* static */ +void nsWindow::ReleaseGlobals() { + for (auto& cursor : gCursorCache) { + if (cursor) { + g_object_unref(cursor); + cursor = nullptr; + } + } +} + +void nsWindow::CommonCreate(nsIWidget* aParent, bool aListenForResizes) { + mParent = aParent; + mListenForResizes = aListenForResizes; + mCreated = true; +} + +void nsWindow::DispatchActivateEvent(void) { + NS_ASSERTION(mContainer || mIsDestroyed, + "DispatchActivateEvent only intended for container windows"); + +#ifdef ACCESSIBILITY + DispatchActivateEventAccessible(); +#endif // ACCESSIBILITY + + if (mWidgetListener) mWidgetListener->WindowActivated(); +} + +void nsWindow::DispatchDeactivateEvent(void) { + if (mWidgetListener) mWidgetListener->WindowDeactivated(); + +#ifdef ACCESSIBILITY + DispatchDeactivateEventAccessible(); +#endif // ACCESSIBILITY +} + +void nsWindow::DispatchResized() { + mNeedsDispatchResized = false; + if (mWidgetListener) { + mWidgetListener->WindowResized(this, mBounds.width, mBounds.height); + } + if (mAttachedWidgetListener) { + mAttachedWidgetListener->WindowResized(this, mBounds.width, mBounds.height); + } +} + +void nsWindow::MaybeDispatchResized() { + if (mNeedsDispatchResized && !mIsDestroyed) { + DispatchResized(); + } +} + +nsIWidgetListener* nsWindow::GetListener() { + return mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener; +} + +nsresult nsWindow::DispatchEvent(WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) { +#ifdef DEBUG + debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "something", 0); +#endif + aStatus = nsEventStatus_eIgnore; + nsIWidgetListener* listener = GetListener(); + if (listener) { + aStatus = listener->HandleEvent(aEvent, mUseAttachedEvents); + } + + return NS_OK; +} + +void nsWindow::OnDestroy(void) { + if (mOnDestroyCalled) return; + + mOnDestroyCalled = true; + + // Prevent deletion. + nsCOMPtr<nsIWidget> kungFuDeathGrip = this; + + // release references to children, device context, toolkit + app shell + nsBaseWidget::OnDestroy(); + + // Remove association between this object and its parent and siblings. + nsBaseWidget::Destroy(); + mParent = nullptr; + + NotifyWindowDestroyed(); +} + +bool nsWindow::AreBoundsSane() { + return mBounds.width > 0 && mBounds.height > 0; +} + +static GtkWidget* EnsureInvisibleContainer() { + if (!gInvisibleContainer) { + // GtkWidgets need to be anchored to a GtkWindow to be realized (to + // have a window). Using GTK_WINDOW_POPUP rather than + // GTK_WINDOW_TOPLEVEL in the hope that POPUP results in less + // initialization and window manager interaction. + GtkWidget* window = gtk_window_new(GTK_WINDOW_POPUP); + gInvisibleContainer = moz_container_new(); + gtk_container_add(GTK_CONTAINER(window), gInvisibleContainer); + gtk_widget_realize(gInvisibleContainer); + } + return gInvisibleContainer; +} + +static void CheckDestroyInvisibleContainer() { + MOZ_ASSERT(gInvisibleContainer, "oh, no"); + + if (!gdk_window_peek_children(gtk_widget_get_window(gInvisibleContainer))) { + // No children, so not in use. + // Make sure to destroy the GtkWindow also. + gtk_widget_destroy(gtk_widget_get_parent(gInvisibleContainer)); + gInvisibleContainer = nullptr; + } +} + +// Change the containing GtkWidget on a sub-hierarchy of GdkWindows belonging +// to aOldWidget and rooted at aWindow, and reparent any child GtkWidgets of +// the GdkWindow hierarchy to aNewWidget. +static void SetWidgetForHierarchy(GdkWindow* aWindow, GtkWidget* aOldWidget, + GtkWidget* aNewWidget) { + gpointer data; + gdk_window_get_user_data(aWindow, &data); + + if (data != aOldWidget) { + if (!GTK_IS_WIDGET(data)) return; + + auto* widget = static_cast<GtkWidget*>(data); + if (gtk_widget_get_parent(widget) != aOldWidget) return; + + // This window belongs to a child widget, which will no longer be a + // child of aOldWidget. + gtk_widget_reparent(widget, aNewWidget); + + return; + } + + GList* children = gdk_window_get_children(aWindow); + for (GList* list = children; list; list = list->next) { + SetWidgetForHierarchy(GDK_WINDOW(list->data), aOldWidget, aNewWidget); + } + g_list_free(children); + + gdk_window_set_user_data(aWindow, aNewWidget); +} + +// Walk the list of child windows and call destroy on them. +void nsWindow::DestroyChildWindows() { + if (!mGdkWindow) return; + + while (GList* children = gdk_window_peek_children(mGdkWindow)) { + GdkWindow* child = GDK_WINDOW(children->data); + nsWindow* kid = get_window_for_gdk_window(child); + if (kid) { + kid->Destroy(); + } else { + // This child is not an nsWindow. + // Destroy the child GtkWidget. + gpointer data; + gdk_window_get_user_data(child, &data); + if (GTK_IS_WIDGET(data)) { + gtk_widget_destroy(static_cast<GtkWidget*>(data)); + } + } + } +} + +void nsWindow::Destroy() { + if (mIsDestroyed || !mCreated) return; + + LOG(("nsWindow::Destroy [%p]\n", (void*)this)); + mIsDestroyed = true; + mCreated = false; + + /** Need to clean our LayerManager up while still alive */ + if (mLayerManager) { + mLayerManager->Destroy(); + } + mLayerManager = nullptr; + +#ifdef MOZ_WAYLAND + // Shut down our local vsync source + if (mWaylandVsyncSource) { + mWaylandVsyncSource->Shutdown(); + mWaylandVsyncSource = nullptr; + } +#endif + + // It is safe to call DestroyeCompositor several times (here and + // in the parent class) since it will take effect only once. + // The reason we call it here is because on gtk platforms we need + // to destroy the compositor before we destroy the gdk window (which + // destroys the the gl context attached to it). + DestroyCompositor(); + +#ifdef MOZ_X11 + // Ensure any resources assigned to the window get cleaned up first + // to avoid double-freeing. + mSurfaceProvider.CleanupResources(); +#endif + + ClearCachedResources(); + + g_signal_handlers_disconnect_by_data(gtk_settings_get_default(), this); + + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + if (rollupListener) { + nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); + if (static_cast<nsIWidget*>(this) == rollupWidget) { + rollupListener->Rollup(0, false, nullptr, nullptr); + } + } + + // dragService will be null after shutdown of the service manager. + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + if (dragService && this == dragService->GetMostRecentDestWindow()) { + dragService->ScheduleLeaveEvent(); + } + + NativeShow(false); + + if (mIMContext) { + mIMContext->OnDestroyWindow(this); + } + + // make sure that we remove ourself as the focus window + if (gFocusWindow == this) { + LOGFOCUS(("automatically losing focus...\n")); + gFocusWindow = nullptr; + } + + GtkWidget* owningWidget = GetMozContainerWidget(); + if (mShell) { + gtk_widget_destroy(mShell); + mShell = nullptr; + mContainer = nullptr; + MOZ_ASSERT(!mGdkWindow, + "mGdkWindow should be NULL when mContainer is destroyed"); + } else if (mContainer) { + gtk_widget_destroy(GTK_WIDGET(mContainer)); + mContainer = nullptr; + MOZ_ASSERT(!mGdkWindow, + "mGdkWindow should be NULL when mContainer is destroyed"); + } else if (mGdkWindow) { + // Destroy child windows to ensure that their mThebesSurfaces are + // released and to remove references from GdkWindows back to their + // container widget. (OnContainerUnrealize() does this when the + // MozContainer widget is destroyed.) + DestroyChildWindows(); + + gdk_window_set_user_data(mGdkWindow, nullptr); + g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", nullptr); + gdk_window_destroy(mGdkWindow); + mGdkWindow = nullptr; + } + + if (gInvisibleContainer && owningWidget == gInvisibleContainer) { + CheckDestroyInvisibleContainer(); + } + +#ifdef ACCESSIBILITY + if (mRootAccessible) { + mRootAccessible = nullptr; + } +#endif + + // Save until last because OnDestroy() may cause us to be deleted. + OnDestroy(); +} + +nsIWidget* nsWindow::GetParent(void) { return mParent; } + +float nsWindow::GetDPI() { + float dpi = 96.0f; + nsCOMPtr<nsIScreen> screen = GetWidgetScreen(); + if (screen) { + screen->GetDpi(&dpi); + } + return dpi; +} + +double nsWindow::GetDefaultScaleInternal() { + return GdkScaleFactor() * gfxPlatformGtk::GetFontScaleFactor(); +} + +DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScale() { +#ifdef MOZ_WAYLAND + if (!mIsX11Display) { + return DesktopToLayoutDeviceScale(GdkScaleFactor()); + } +#endif + + // In Gtk/X11, we manage windows using device pixels. + return DesktopToLayoutDeviceScale(1.0); +} + +DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScaleByScreen() { +#ifdef MOZ_WAYLAND + // In Wayland there's no way to get absolute position of the window and use it + // to determine the screen factor of the monitor on which the window is + // placed. The window is notified of the current scale factor but not at this + // point, so the GdkScaleFactor can return wrong value which can lead to wrong + // popup placement. We need to use parent's window scale factor for the new + // one. + if (!mIsX11Display) { + nsView* view = nsView::GetViewFor(this); + if (view) { + nsView* parentView = view->GetParent(); + if (parentView) { + nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr); + if (parentWidget) { + return DesktopToLayoutDeviceScale( + parentWidget->RoundsWidgetCoordinatesTo()); + } + NS_WARNING("Widget has no parent"); + } + } else { + NS_WARNING("Cannot find widget view"); + } + } +#endif + return nsBaseWidget::GetDesktopToDeviceScale(); +} + +void nsWindow::SetParent(nsIWidget* aNewParent) { + if (!mGdkWindow) { + MOZ_ASSERT_UNREACHABLE("The native window has already been destroyed"); + return; + } + + if (mContainer) { + // FIXME bug 1469183 + NS_ERROR("nsWindow should not have a container here"); + return; + } + + nsCOMPtr<nsIWidget> kungFuDeathGrip = this; + if (mParent) { + mParent->RemoveChild(this); + } + mParent = aNewParent; + + GtkWidget* oldContainer = GetMozContainerWidget(); + if (!oldContainer) { + // The GdkWindows have been destroyed so there is nothing else to + // reparent. + MOZ_ASSERT(gdk_window_is_destroyed(mGdkWindow), + "live GdkWindow with no widget"); + return; + } + + nsWindow* newParent = static_cast<nsWindow*>(aNewParent); + GdkWindow* newParentWindow = nullptr; + GtkWidget* newContainer = nullptr; + if (aNewParent) { + aNewParent->AddChild(this); + newParentWindow = newParent->mGdkWindow; + newContainer = newParent->GetMozContainerWidget(); + } else { + // aNewParent is nullptr, but reparent to a hidden window to avoid + // destroying the GdkWindow and its descendants. + // An invisible container widget is needed to hold descendant + // GtkWidgets. + newContainer = EnsureInvisibleContainer(); + newParentWindow = gtk_widget_get_window(newContainer); + } + + if (!newContainer) { + // The new parent GdkWindow has been destroyed. + MOZ_ASSERT(!newParentWindow || gdk_window_is_destroyed(newParentWindow), + "live GdkWindow with no widget"); + Destroy(); + } else { + if (newContainer != oldContainer) { + MOZ_ASSERT(!gdk_window_is_destroyed(newParentWindow), + "destroyed GdkWindow with widget"); + SetWidgetForHierarchy(mGdkWindow, oldContainer, newContainer); + + if (oldContainer == gInvisibleContainer) { + CheckDestroyInvisibleContainer(); + } + } + + gdk_window_reparent(mGdkWindow, newParentWindow, + DevicePixelsToGdkCoordRoundDown(mBounds.x), + DevicePixelsToGdkCoordRoundDown(mBounds.y)); + } + + bool parentHasMappedToplevel = newParent && newParent->mHasMappedToplevel; + if (mHasMappedToplevel != parentHasMappedToplevel) { + SetHasMappedToplevel(parentHasMappedToplevel); + } +} + +bool nsWindow::WidgetTypeSupportsAcceleration() { + if (IsSmallPopup()) { + return false; + } + // Workaround for Bug 1479135 + // We draw transparent popups on non-compositing screens by SW as we don't + // implement X shape masks in WebRender. + if (mWindowType == eWindowType_popup) { + return mCompositedScreen; + } + return true; +} + +void nsWindow::ReparentNativeWidget(nsIWidget* aNewParent) { + MOZ_ASSERT(aNewParent, "null widget"); + MOZ_ASSERT(!mIsDestroyed, ""); + MOZ_ASSERT(!static_cast<nsWindow*>(aNewParent)->mIsDestroyed, ""); + MOZ_ASSERT(!gdk_window_is_destroyed(mGdkWindow), + "destroyed GdkWindow with widget"); + + MOZ_ASSERT( + !mParent, + "nsWindow::ReparentNativeWidget() works on toplevel windows only."); + + auto* newParent = static_cast<nsWindow*>(aNewParent); + GtkWindow* newParentWidget = GTK_WINDOW(newParent->GetGtkWidget()); + GtkWindow* shell = GTK_WINDOW(mShell); + + if (shell && gtk_window_get_transient_for(shell)) { + gtk_window_set_transient_for(shell, newParentWidget); + } +} + +void nsWindow::SetModal(bool aModal) { + LOG(("nsWindow::SetModal [%p] %d\n", (void*)this, aModal)); + if (mIsDestroyed) return; + if (!mIsTopLevel || !mShell) return; + gtk_window_set_modal(GTK_WINDOW(mShell), aModal ? TRUE : FALSE); +} + +// nsIWidget method, which means IsShown. +bool nsWindow::IsVisible() const { return mIsShown; } + +void nsWindow::RegisterTouchWindow() { + mHandleTouchEvent = true; + mTouches.Clear(); +} + +void nsWindow::ConstrainPosition(bool aAllowSlop, int32_t* aX, int32_t* aY) { + if (!mIsTopLevel || !mShell) return; + + double dpiScale = GetDefaultScale().scale; + + // we need to use the window size in logical screen pixels + int32_t logWidth = std::max(NSToIntRound(mBounds.width / dpiScale), 1); + int32_t logHeight = std::max(NSToIntRound(mBounds.height / dpiScale), 1); + + /* get our playing field. use the current screen, or failing that + for any reason, use device caps for the default screen. */ + nsCOMPtr<nsIScreen> screen; + nsCOMPtr<nsIScreenManager> screenmgr = + do_GetService("@mozilla.org/gfx/screenmanager;1"); + if (screenmgr) { + screenmgr->ScreenForRect(*aX, *aY, logWidth, logHeight, + getter_AddRefs(screen)); + } + + // We don't have any screen so leave the coordinates as is + if (!screen) return; + + nsIntRect screenRect; + if (mSizeMode != nsSizeMode_Fullscreen) { + // For normalized windows, use the desktop work area. + screen->GetAvailRectDisplayPix(&screenRect.x, &screenRect.y, + &screenRect.width, &screenRect.height); + } else { + // For full screen windows, use the desktop. + screen->GetRectDisplayPix(&screenRect.x, &screenRect.y, &screenRect.width, + &screenRect.height); + } + + if (aAllowSlop) { + if (*aX < screenRect.x - logWidth + kWindowPositionSlop) + *aX = screenRect.x - logWidth + kWindowPositionSlop; + else if (*aX >= screenRect.XMost() - kWindowPositionSlop) + *aX = screenRect.XMost() - kWindowPositionSlop; + + if (*aY < screenRect.y - logHeight + kWindowPositionSlop) + *aY = screenRect.y - logHeight + kWindowPositionSlop; + else if (*aY >= screenRect.YMost() - kWindowPositionSlop) + *aY = screenRect.YMost() - kWindowPositionSlop; + } else { + if (*aX < screenRect.x) + *aX = screenRect.x; + else if (*aX >= screenRect.XMost() - logWidth) + *aX = screenRect.XMost() - logWidth; + + if (*aY < screenRect.y) + *aY = screenRect.y; + else if (*aY >= screenRect.YMost() - logHeight) + *aY = screenRect.YMost() - logHeight; + } +} + +void nsWindow::SetSizeConstraints(const SizeConstraints& aConstraints) { + mSizeConstraints.mMinSize = GetSafeWindowSize(aConstraints.mMinSize); + mSizeConstraints.mMaxSize = GetSafeWindowSize(aConstraints.mMaxSize); + + ApplySizeConstraints(); +} + +void nsWindow::AddCSDDecorationSize(int* aWidth, int* aHeight) { + if (mCSDSupportLevel == CSD_SUPPORT_CLIENT && mDrawInTitlebar) { + GtkBorder decorationSize = GetCSDDecorationSize(!mIsTopLevel); + *aWidth += decorationSize.left + decorationSize.right; + *aHeight += decorationSize.top + decorationSize.bottom; + } +} + +void nsWindow::ApplySizeConstraints(void) { + if (mShell) { + GdkGeometry geometry; + geometry.min_width = + DevicePixelsToGdkCoordRoundUp(mSizeConstraints.mMinSize.width); + geometry.min_height = + DevicePixelsToGdkCoordRoundUp(mSizeConstraints.mMinSize.height); + geometry.max_width = + DevicePixelsToGdkCoordRoundDown(mSizeConstraints.mMaxSize.width); + geometry.max_height = + DevicePixelsToGdkCoordRoundDown(mSizeConstraints.mMaxSize.height); + + uint32_t hints = 0; + if (mSizeConstraints.mMinSize != LayoutDeviceIntSize(0, 0)) { + AddCSDDecorationSize(&geometry.min_width, &geometry.min_height); + hints |= GDK_HINT_MIN_SIZE; + LOG(("nsWindow::ApplySizeConstraints [%p] min size %d %d\n", (void*)this, + geometry.min_width, geometry.min_height)); + } + if (mSizeConstraints.mMaxSize != + LayoutDeviceIntSize(NS_MAXSIZE, NS_MAXSIZE)) { + AddCSDDecorationSize(&geometry.max_width, &geometry.max_height); + hints |= GDK_HINT_MAX_SIZE; + LOG(("nsWindow::ApplySizeConstraints [%p] max size %d %d\n", (void*)this, + geometry.max_width, geometry.max_height)); + } + + if (mAspectRatio != 0.0f) { + geometry.min_aspect = mAspectRatio; + geometry.max_aspect = mAspectRatio; + hints |= GDK_HINT_ASPECT; + } + + gtk_window_set_geometry_hints(GTK_WINDOW(mShell), nullptr, &geometry, + GdkWindowHints(hints)); + } +} + +void nsWindow::Show(bool aState) { + if (aState == mIsShown) return; + + // Clear our cached resources when the window is hidden. + if (mIsShown && !aState) { + ClearCachedResources(); + } + + mIsShown = aState; + + LOG(("nsWindow::Show [%p] state %d\n", (void*)this, aState)); + + if (aState) { + // Now that this window is shown, mHasMappedToplevel needs to be + // tracked on viewable descendants. + SetHasMappedToplevel(mHasMappedToplevel); + } + + // Ok, someone called show on a window that isn't sized to a sane + // value. Mark this window as needing to have Show() called on it + // and return. + if ((aState && !AreBoundsSane()) || !mCreated) { + LOG(("\tbounds are insane or window hasn't been created yet\n")); + mNeedsShow = true; + return; + } + + // If someone is hiding this widget, clear any needing show flag. + if (!aState) mNeedsShow = false; + +#ifdef ACCESSIBILITY + if (aState && a11y::ShouldA11yBeEnabled()) CreateRootAccessible(); +#endif + + NativeShow(aState); +} + +void nsWindow::ResizeInt(int aX, int aY, int aWidth, int aHeight, bool aMove, + bool aRepaint) { + LOG(("nsWindow::ResizeInt [%p] x:%d y:%d -> w:%d h:%d repaint %d aMove %d\n", + (void*)this, aX, aY, aWidth, aHeight, aRepaint, aMove)); + + ConstrainSize(&aWidth, &aHeight); + + LOG((" ConstrainSize: w:%d h;%d\n", aWidth, aHeight)); + + // If we used to have insane bounds, we may have skipped actually positioning + // the widget in NativeMoveResizeWaylandPopup, in which case we need to + // actually position it now as well. + const bool hadInsaneWaylandPopupDimensions = + !AreBoundsSane() && IsWaylandPopup(); + + if (aMove) { + mBounds.x = aX; + mBounds.y = aY; + } + + // For top-level windows, aWidth and aHeight should possibly be + // interpreted as frame bounds, but NativeResize treats these as window + // bounds (Bug 581866). + mBounds.SizeTo(aWidth, aHeight); + + // We set correct mBounds in advance here. This can be invalided by state + // event. + mBoundsAreValid = true; + + // Recalculate aspect ratio when resized from DOM + if (mAspectRatio != 0.0) { + LockAspectRatio(true); + } + + if (!mCreated) return; + + if (aMove || mPreferredPopupRectFlushed || hadInsaneWaylandPopupDimensions) { + LOG((" Need also to move, flushed? %d, bounds were insane: %d\n", + mPreferredPopupRectFlushed, hadInsaneWaylandPopupDimensions)); + NativeMoveResize(); + } else { + NativeResize(); + } + + NotifyRollupGeometryChange(); + + // send a resize notification if this is a toplevel + if (mIsTopLevel || mListenForResizes) { + DispatchResized(); + } +} + +void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) { + LOG(("nsWindow::Resize [%p] %f %f\n", (void*)this, aWidth, aHeight)); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t width = NSToIntRound(scale * aWidth); + int32_t height = NSToIntRound(scale * aHeight); + + ResizeInt(0, 0, width, height, /* aMove */ false, aRepaint); +} + +void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) { + LOG(("nsWindow::Resize [%p] %f %f repaint %d\n", (void*)this, aWidth, aHeight, + aRepaint)); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t width = NSToIntRound(scale * aWidth); + int32_t height = NSToIntRound(scale * aHeight); + + int32_t x = NSToIntRound(scale * aX); + int32_t y = NSToIntRound(scale * aY); + + ResizeInt(x, y, width, height, /* aMove */ true, aRepaint); +} + +void nsWindow::Enable(bool aState) { mEnabled = aState; } + +bool nsWindow::IsEnabled() const { return mEnabled; } + +void nsWindow::Move(double aX, double aY) { + LOG(("nsWindow::Move [%p] %f %f\n", (void*)this, aX, aY)); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t x = NSToIntRound(aX * scale); + int32_t y = NSToIntRound(aY * scale); + + if (mWindowType == eWindowType_toplevel || + mWindowType == eWindowType_dialog) { + SetSizeMode(nsSizeMode_Normal); + } + + // Since a popup window's x/y coordinates are in relation to to + // the parent, the parent might have moved so we always move a + // popup window. + if (x == mBounds.x && y == mBounds.y && mWindowType != eWindowType_popup) + return; + + // XXX Should we do some AreBoundsSane check here? + + mBounds.x = x; + mBounds.y = y; + + if (!mCreated) return; + + if (IsWaylandPopup()) { + int32_t p2a = AppUnitsPerCSSPixel() / gfxPlatformGtk::GetFontScaleFactor(); + if (mPreferredPopupRect.x != mBounds.x * p2a && + mPreferredPopupRect.y != mBounds.y * p2a) { + NativeMove(); + NotifyRollupGeometryChange(); + } else { + LOG((" mBounds same as mPreferredPopupRect, no need to move")); + } + } else { + NativeMove(); + NotifyRollupGeometryChange(); + } +} + +bool nsWindow::IsPopup() { + return mIsTopLevel && mWindowType == eWindowType_popup; +} + +bool nsWindow::IsWaylandPopup() { return !mIsX11Display && IsPopup(); } + +void nsWindow::HideWaylandTooltips() { + while (gVisibleWaylandPopupWindows) { + nsWindow* window = + static_cast<nsWindow*>(gVisibleWaylandPopupWindows->data); + if (window->mPopupType != ePopupTypeTooltip) break; + LOG(("nsWindow::HideWaylandTooltips [%p] hidding tooltip [%p].\n", + (void*)this, window)); + window->HideWaylandWindow(); + } +} + +void nsWindow::HideWaylandOpenedPopups() { + while (gVisibleWaylandPopupWindows) { + nsWindow* window = + static_cast<nsWindow*>(gVisibleWaylandPopupWindows->data); + window->HideWaylandWindow(); + } +} + +// Hide popup nsWindows which are no longer in the nsXULPopupManager widget +// chain list. +void nsWindow::CleanupWaylandPopups() { + LOG(("nsWindow::CleanupWaylandPopups...\n")); + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + AutoTArray<nsIWidget*, 5> widgetChain; + pm->GetSubmenuWidgetChain(&widgetChain); + GList* popupList = gVisibleWaylandPopupWindows; + while (popupList) { + LOG((" Looking for %p [nsWindow]\n", popupList->data)); + nsWindow* waylandWnd = static_cast<nsWindow*>(popupList->data); + // Remove only menu popups or empty frames - they are most likely + // already rolledup popups + if (waylandWnd->IsMainMenuWindow() || !waylandWnd->GetFrame()) { + bool popupFound = false; + for (unsigned long i = 0; i < widgetChain.Length(); i++) { + if (waylandWnd == widgetChain[i]) { + popupFound = true; + break; + } + } + if (!popupFound) { + LOG((" nsWindow [%p] not found in PopupManager, hiding it.\n", + waylandWnd)); + waylandWnd->HideWaylandWindow(); + popupList = gVisibleWaylandPopupWindows; + } else { + LOG((" nsWindow [%p] is still open.\n", waylandWnd)); + popupList = popupList->next; + } + } else { + popupList = popupList->next; + } + } +} + +static nsMenuPopupFrame* GetMenuPopupFrame(nsIFrame* aFrame) { + if (aFrame) { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(aFrame); + return menuPopupFrame; + } + return nullptr; +} + +// The MenuList popups are used as dropdown menus for example in WebRTC +// microphone/camera chooser or autocomplete widgets. +bool nsWindow::IsMainMenuWindow() { + nsMenuPopupFrame* menuPopupFrame = GetMenuPopupFrame(GetFrame()); + if (menuPopupFrame) { + LOG((" nsMenuPopupFrame [%p] type: %d IsMenu: %d, IsMenuList: %d\n", + menuPopupFrame, menuPopupFrame->PopupType(), menuPopupFrame->IsMenu(), + menuPopupFrame->IsMenuList())); + return mPopupType == ePopupTypeMenu && !menuPopupFrame->IsMenuList(); + } + return false; +} + +GtkWindow* nsWindow::GetTopmostWindow() { + nsView* view = nsView::GetViewFor(this); + if (view) { + nsView* parentView = view->GetParent(); + if (parentView) { + nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr); + if (parentWidget) { + nsWindow* parentnsWindow = static_cast<nsWindow*>(parentWidget); + LOG((" Topmost window: %p [nsWindow]\n", parentnsWindow)); + return GTK_WINDOW(parentnsWindow->mShell); + } + } + } + return nullptr; +} + +GtkWindow* nsWindow::GetCurrentWindow() { + GtkWindow* parentGtkWindow = nullptr; + // get the last opened window from gVisibleWaylandPopupWindows + if (gVisibleWaylandPopupWindows) { + nsWindow* parentnsWindow = + static_cast<nsWindow*>(gVisibleWaylandPopupWindows->data); + if (parentnsWindow) { + LOG((" Setting parent to last opened window: %p [nsWindow]\n", + parentnsWindow)); + parentGtkWindow = GTK_WINDOW(parentnsWindow->GetGtkWidget()); + } + } + // get the topmost window if the last opened windows are empty + if (!parentGtkWindow) { + parentGtkWindow = GetTopmostWindow(); + } + if (parentGtkWindow && GTK_IS_WINDOW(parentGtkWindow)) { + return GTK_WINDOW(parentGtkWindow); + } else { + LOG((" Failed to get current window for %p: %p\n", this, parentGtkWindow)); + } + return nullptr; +} + +bool nsWindow::IsWidgetOverflowWindow() { + if (this->GetFrame() && this->GetFrame()->GetContent()->GetID()) { + nsCString nodeId; + this->GetFrame()->GetContent()->GetID()->ToUTF8String(nodeId); + return nodeId.Equals("widget-overflow"); + } + return false; +} + +// Wayland keeps strong popup window hierarchy. We need to track active +// (visible) popup windows and make sure we hide popup on the same level +// before we open another one on that level. It means that every open +// popup needs to have an unique parent. +GtkWidget* nsWindow::ConfigureWaylandPopupWindows() { + MOZ_ASSERT(this->mWindowType == eWindowType_popup); + LOG( + ("nsWindow::ConfigureWaylandPopupWindows [%p], frame %p hasRemoteContent " + "%d\n", + (void*)this, this->GetFrame(), this->HasRemoteContent())); +#if DEBUG + if (this->GetFrame() && this->GetFrame()->GetContent()->GetID()) { + nsCString nodeId; + this->GetFrame()->GetContent()->GetID()->ToUTF8String(nodeId); + LOG((" [%p] popup node id=%s\n", this, nodeId.get())); + } +#endif + + if (!GetFrame()) { + LOG((" Window without frame cannot be configured.\n")); + return nullptr; + } + + // Check if we're already configured. + if (gVisibleWaylandPopupWindows && + g_list_find(gVisibleWaylandPopupWindows, this)) { + LOG((" [%p] is already configured.\n", (void*)this)); + return GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(mShell))); + } + + // If we're opening a new window we don't want to attach it to a tooltip + // as it's short lived temporary window. + HideWaylandTooltips(); + + // Cleanup already closed menus + CleanupWaylandPopups(); + + if (gVisibleWaylandPopupWindows && + (HasRemoteContent() || IsWidgetOverflowWindow())) { + nsWindow* openedWindow = + static_cast<nsWindow*>(gVisibleWaylandPopupWindows->data); + LOG((" this [%p], lastOpenedWindow [%p]", this, openedWindow)); + if (openedWindow != this) { + LOG( + (" Hiding all opened popups because the window is remote content or " + "overflow-widget")); + HideWaylandOpenedPopups(); + } + } + + GtkWindow* parentGtkWindow = GetCurrentWindow(); + if (parentGtkWindow) { + MOZ_ASSERT(parentGtkWindow != GTK_WINDOW(this->GetGtkWidget()), + "Cannot set self as parent"); + gtk_window_set_transient_for(GTK_WINDOW(mShell), + GTK_WINDOW(parentGtkWindow)); + // Add current window to the visible popup list + gVisibleWaylandPopupWindows = + g_list_prepend(gVisibleWaylandPopupWindows, this); + LOG((" Parent window for %p: %p [GtkWindow]", this, parentGtkWindow)); + } + + MOZ_ASSERT(parentGtkWindow, "NO parent window for %p: expect popup glitches"); + return GTK_WIDGET(parentGtkWindow); +} + +static void NativeMoveResizeWaylandPopupCallback( + GdkWindow* window, const GdkRectangle* flipped_rect, + const GdkRectangle* final_rect, gboolean flipped_x, gboolean flipped_y, + void* aWindow) { + LOG(("NativeMoveResizeWaylandPopupCallback [%p] flipped_x %d flipped_y %d\n", + aWindow, flipped_x, flipped_y)); + + LOG((" flipped_rect x=%d y=%d width=%d height=%d\n", flipped_rect->x, + flipped_rect->y, flipped_rect->width, flipped_rect->height)); + LOG((" final_rect x=%d y=%d width=%d height=%d\n", final_rect->x, + final_rect->y, final_rect->width, final_rect->height)); + nsWindow* wnd = get_window_for_gdk_window(window); + + wnd->NativeMoveResizeWaylandPopupCB(final_rect, flipped_x, flipped_y); +} + +void nsWindow::NativeMoveResizeWaylandPopupCB(const GdkRectangle* aFinalSize, + bool aFlippedX, bool aFlippedY) { + LOG((" orig mBounds x=%d y=%d width=%d height=%d\n", mBounds.x, mBounds.y, + mBounds.width, mBounds.height)); + + // Remove signal handler because it can also be called from + // xdg_popup_configure + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(mShell)); + if (g_signal_handler_find( + gdkWindow, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + FuncToGpointer(NativeMoveResizeWaylandPopupCallback), this)) { + LOG((" Disconnecting NativeMoveResizeWaylandPopupCallback")); + g_signal_handlers_disconnect_by_func( + gdkWindow, FuncToGpointer(NativeMoveResizeWaylandPopupCallback), this); + } + mWaitingForMoveToRectCB = false; + + // We ignore the callback position data because the another resize has been + // called before the callback have been triggered. + if (mPendingSizeRect.height > 0 || mPendingSizeRect.width > 0) { + LOG( + (" Another resize called during waiting for callback, calling " + "Resize(%d, %d)\n", + mPendingSizeRect.width, mPendingSizeRect.height)); + // Set the preferred size to zero to avoid wrong size of popup because the + // mPreferredPopupRect is used in nsMenuPopupFrame to set dimensions + mPreferredPopupRect = nsRect(0, 0, 0, 0); + + // We need to schedule another resize because the window has been resized + // again before callback was called. + Resize(mPendingSizeRect.width, mPendingSizeRect.height, true); + DispatchResized(); + mPendingSizeRect.width = mPendingSizeRect.height = 0; + return; + } + + GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) { + NS_WARNING("Popup has no parent!"); + return; + } + + // The position of the menu in GTK is relative to it's parent window while + // in mBounds we have position relative to toplevel window. We need to check + // and update mBounds in the toplevel coordinates. + int x_parent, y_parent; + GetWindowOrigin(gtk_widget_get_window(GTK_WIDGET(parentGtkWindow)), &x_parent, + &y_parent); + + LayoutDeviceIntRect newBounds(aFinalSize->x, aFinalSize->y, aFinalSize->width, + aFinalSize->height); + + newBounds.x = GdkCoordToDevicePixels(newBounds.x); + newBounds.y = GdkCoordToDevicePixels(newBounds.y); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t newWidth = NSToIntRound(scale * newBounds.width); + int32_t newHeight = NSToIntRound(scale * newBounds.height); + + LOG((" new mBounds x=%d y=%d width=%d height=%d\n", newBounds.x, + newBounds.y, newWidth, newHeight)); + + bool needsPositionUpdate = + (newBounds.x != mBounds.x || newBounds.y != mBounds.y); + bool needsSizeUpdate = + (newWidth != mBounds.width || newHeight != mBounds.height); + // Update view + + if (needsSizeUpdate) { + LOG((" needSizeUpdate\n")); + int32_t p2a = AppUnitsPerCSSPixel() / gfxPlatformGtk::GetFontScaleFactor(); + mPreferredPopupRect = nsRect(NSIntPixelsToAppUnits(newBounds.x, p2a), + NSIntPixelsToAppUnits(newBounds.y, p2a), + NSIntPixelsToAppUnits(newBounds.width, p2a), + NSIntPixelsToAppUnits(newBounds.height, p2a)); + mPreferredPopupRectFlushed = false; + Resize(newBounds.width, newBounds.height, true); + DispatchResized(); + + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + if (popupFrame) { + RefPtr<PresShell> presShell = popupFrame->PresShell(); + presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::Resize, + NS_FRAME_IS_DIRTY); + } + } + + if (needsPositionUpdate) { + LOG((" needPositionUpdate\n")); + // The newBounds are in coordinates relative to the parent window/popup. + // The NotifyWindowMoved requires the coordinates relative to the toplevel. + // We use the gdk_window_get_origin to get correct coordinates. + gint x = 0, y = 0; + GetWindowOrigin(gtk_widget_get_window(GTK_WIDGET(mShell)), &x, &y); + NotifyWindowMoved(GdkCoordToDevicePixels(x), GdkCoordToDevicePixels(y)); + } +} + +#ifdef MOZ_WAYLAND +static GdkGravity PopupAlignmentToGdkGravity(int8_t aAlignment) { + switch (aAlignment) { + case POPUPALIGNMENT_NONE: + return GDK_GRAVITY_NORTH_WEST; + break; + case POPUPALIGNMENT_TOPLEFT: + return GDK_GRAVITY_NORTH_WEST; + break; + case POPUPALIGNMENT_TOPRIGHT: + return GDK_GRAVITY_NORTH_EAST; + break; + case POPUPALIGNMENT_BOTTOMLEFT: + return GDK_GRAVITY_SOUTH_WEST; + break; + case POPUPALIGNMENT_BOTTOMRIGHT: + return GDK_GRAVITY_SOUTH_EAST; + break; + case POPUPALIGNMENT_LEFTCENTER: + return GDK_GRAVITY_WEST; + break; + case POPUPALIGNMENT_RIGHTCENTER: + return GDK_GRAVITY_EAST; + break; + case POPUPALIGNMENT_TOPCENTER: + return GDK_GRAVITY_NORTH; + break; + case POPUPALIGNMENT_BOTTOMCENTER: + return GDK_GRAVITY_SOUTH; + break; + } + return GDK_GRAVITY_STATIC; +} +#endif + +void nsWindow::NativeMoveResizeWaylandPopup(GdkPoint* aPosition, + GdkRectangle* aSize) { + // Available as of GTK 3.24+ + static auto sGdkWindowMoveToRect = (void (*)( + GdkWindow*, const GdkRectangle*, GdkGravity, GdkGravity, GdkAnchorHints, + gint, gint))dlsym(RTLD_DEFAULT, "gdk_window_move_to_rect"); + LOG(("nsWindow::NativeMoveResizeWaylandPopup [%p]\n", (void*)this)); + + // Compositor may be confused by windows with width/height = 0 + // and positioning such windows leads to Bug 1555866. + if (!AreBoundsSane()) { + LOG((" Bounds are not sane (width: %d height: %d)\n", mBounds.width, + mBounds.height)); + return; + } + + if (aSize) { + gtk_window_resize(GTK_WINDOW(mShell), aSize->width, aSize->height); + } + + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(mShell)); + + // Use standard gtk_window_move() instead of gdk_window_move_to_rect() when: + // - gdk_window_move_to_rect() is not available + // - the widget doesn't have a valid GdkWindow + if (!sGdkWindowMoveToRect || !gdkWindow) { + LOG((" use gtk_window_move(%d, %d)\n", aPosition->x, aPosition->y)); + gtk_window_move(GTK_WINDOW(mShell), aPosition->x, aPosition->y); + return; + } + + GtkWidget* parentWindow = ConfigureWaylandPopupWindows(); + LOG(("nsWindow::NativeMoveResizeWaylandPopup: Set popup parent %p\n", + parentWindow)); + + // Get anchor rectangle + LayoutDeviceIntRect anchorRect(0, 0, 0, 0); + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + + int32_t p2a; + double devPixelsPerCSSPixel = StaticPrefs::layout_css_devPixelsPerPx(); + if (devPixelsPerCSSPixel > 0.0) { + p2a = AppUnitsPerCSSPixel() / devPixelsPerCSSPixel * GdkScaleFactor(); + } else { + p2a = AppUnitsPerCSSPixel() / gfxPlatformGtk::GetFontScaleFactor(); + } + if (popupFrame) { +#ifdef MOZ_WAYLAND + anchorRect = LayoutDeviceIntRect::FromAppUnitsToOutside( + popupFrame->GetAnchorRect(), p2a); +#endif + } + +#ifdef MOZ_WAYLAND + bool hasAnchorRect = true; +#endif + if (anchorRect.width == 0) { + LOG((" No anchor rect given, use aPosition for anchor")); + anchorRect.SetRect(aPosition->x, aPosition->y, 1, 1); +#ifdef MOZ_WAYLAND + hasAnchorRect = false; +#endif + } + LOG((" anchor x %d y %d width %d height %d (absolute coords)\n", + anchorRect.x, anchorRect.y, anchorRect.width, anchorRect.height)); + + // Anchor rect is in the toplevel coordinates but we need to transfer it to + // the coordinates relative to the popup parent for the + // gdk_window_move_to_rect + int x_parent = 0, y_parent = 0; + GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + if (parentGtkWindow) { + GetWindowOrigin(gtk_widget_get_window(GTK_WIDGET(parentGtkWindow)), + &x_parent, &y_parent); + } + LOG((" x_parent %d y_parent %d\n", x_parent, y_parent)); + anchorRect.MoveBy(-x_parent, -y_parent); + GdkRectangle rect = {anchorRect.x, anchorRect.y, anchorRect.width, + anchorRect.height}; + + // Get gravity and flip type + GdkGravity rectAnchor = GDK_GRAVITY_NORTH_WEST; + GdkGravity menuAnchor = GDK_GRAVITY_NORTH_WEST; + FlipType flipType = FlipType_Default; + int8_t position = -1; + if (popupFrame) { +#ifdef MOZ_WAYLAND + rectAnchor = PopupAlignmentToGdkGravity(popupFrame->GetPopupAnchor()); + menuAnchor = PopupAlignmentToGdkGravity(popupFrame->GetPopupAlignment()); + flipType = popupFrame->GetFlipType(); + position = popupFrame->GetAlignmentPosition(); +#endif + } else { + LOG((" NO ANCHOR INFO")); + if (GetTextDirection() == GTK_TEXT_DIR_RTL) { + rectAnchor = GDK_GRAVITY_NORTH_EAST; + menuAnchor = GDK_GRAVITY_NORTH_EAST; + } + } + LOG((" parentRect gravity: %d anchor gravity: %d\n", rectAnchor, menuAnchor)); + + GdkAnchorHints hints = GdkAnchorHints(GDK_ANCHOR_RESIZE); + + // slideHorizontal from nsMenuPopupFrame::SetPopupPosition + if (position >= POPUPPOSITION_BEFORESTART && + position <= POPUPPOSITION_AFTEREND) { + hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE_X); + } + // slideVertical from nsMenuPopupFrame::SetPopupPosition + if (position >= POPUPPOSITION_STARTBEFORE && + position <= POPUPPOSITION_ENDAFTER) { + hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE_Y); + } + + if (popupFrame && rectAnchor == GDK_GRAVITY_CENTER && + menuAnchor == GDK_GRAVITY_CENTER) { + // only slide + hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE); + } else { + switch (flipType) { + case FlipType_Both: + hints = GdkAnchorHints(hints | GDK_ANCHOR_FLIP); + break; + case FlipType_Slide: + hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE); + break; + case FlipType_Default: + hints = GdkAnchorHints(hints | GDK_ANCHOR_FLIP); + break; + default: + break; + } + } + if (!IsMainMenuWindow()) { + // we don't want to slide menus to fit the screen rather resize them + hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE); + } + + // A workaround for https://gitlab.gnome.org/GNOME/gtk/issues/1986 + // gdk_window_move_to_rect() does not reposition visible windows. + static auto sGtkWidgetIsVisible = + (gboolean(*)(GtkWidget*))dlsym(RTLD_DEFAULT, "gtk_widget_is_visible"); + + bool isWidgetVisible = + (sGtkWidgetIsVisible != nullptr) && sGtkWidgetIsVisible(mShell); + if (isWidgetVisible) { + PauseRemoteRenderer(); + gtk_widget_hide(mShell); + } + + LOG((" requested rect: x: %d y: %d width: %d height: %d\n", rect.x, rect.y, + rect.width, rect.height)); + if (aSize) { + LOG((" aSize: x%d y%d w%d h%d\n", aSize->x, aSize->y, aSize->width, + aSize->height)); + } else { + LOG((" No aSize given")); + } + + // Inspired by nsMenuPopupFrame::AdjustPositionForAnchorAlign + nsPoint cursorOffset(0, 0); +#ifdef MOZ_WAYLAND + // Offset is already computed to the tooltips + if (hasAnchorRect && popupFrame && mPopupType != ePopupTypeTooltip) { + nsMargin margin(0, 0, 0, 0); + popupFrame->StyleMargin()->GetMargin(margin); + switch (popupFrame->GetPopupAlignment()) { + case POPUPALIGNMENT_TOPRIGHT: + cursorOffset.MoveBy(-margin.right, margin.top); + break; + case POPUPALIGNMENT_BOTTOMLEFT: + cursorOffset.MoveBy(margin.left, -margin.bottom); + break; + case POPUPALIGNMENT_BOTTOMRIGHT: + cursorOffset.MoveBy(-margin.right, -margin.bottom); + break; + case POPUPALIGNMENT_TOPLEFT: + default: + cursorOffset.MoveBy(margin.left, margin.top); + break; + } + } +#endif + + if (!g_signal_handler_find( + gdkWindow, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + FuncToGpointer(NativeMoveResizeWaylandPopupCallback), this)) { + g_signal_connect(gdkWindow, "moved-to-rect", + G_CALLBACK(NativeMoveResizeWaylandPopupCallback), this); + } + + LOG((" popup window cursor offset x: %d y: %d\n", cursorOffset.x / p2a, + cursorOffset.y / p2a)); + mWaitingForMoveToRectCB = true; + sGdkWindowMoveToRect(gdkWindow, &rect, rectAnchor, menuAnchor, hints, + cursorOffset.x / p2a, cursorOffset.y / p2a); + + if (isWidgetVisible) { + // We show the popup with the same configuration so no need to call + // ConfigureWaylandPopupWindows() before gtk_widget_show(). + gtk_widget_show(mShell); + } +} + +void nsWindow::NativeMove() { + GdkPoint point = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft()); + + LOG(("nsWindow::NativeMove [%p] %d %d\n", (void*)this, point.x, point.y)); + + if (IsWaylandPopup()) { + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size()); + NativeMoveResizeWaylandPopup(&point, &size); + } else if (mIsTopLevel) { + gtk_window_move(GTK_WINDOW(mShell), point.x, point.y); + } else if (mGdkWindow) { + gdk_window_move(mGdkWindow, point.x, point.y); + } +} + +void nsWindow::SetZIndex(int32_t aZIndex) { + nsIWidget* oldPrev = GetPrevSibling(); + + nsBaseWidget::SetZIndex(aZIndex); + + if (GetPrevSibling() == oldPrev) { + return; + } + + NS_ASSERTION(!mContainer, "Expected Mozilla child widget"); + + // We skip the nsWindows that don't have mGdkWindows. + // These are probably in the process of being destroyed. + + if (!GetNextSibling()) { + // We're to be on top. + if (mGdkWindow) gdk_window_raise(mGdkWindow); + } else { + // All the siblings before us need to be below our widget. + for (nsWindow* w = this; w; + w = static_cast<nsWindow*>(w->GetPrevSibling())) { + if (w->mGdkWindow) gdk_window_lower(w->mGdkWindow); + } + } +} + +void nsWindow::SetSizeMode(nsSizeMode aMode) { + LOG(("nsWindow::SetSizeMode [%p] %d\n", (void*)this, aMode)); + + // Save the requested state. + nsBaseWidget::SetSizeMode(aMode); + + // return if there's no shell or our current state is the same as + // the mode we were just set to. + if (!mShell || mSizeState == mSizeMode) { + LOG((" already set")); + return; + } + + switch (aMode) { + case nsSizeMode_Maximized: + LOG((" set maximized")); + gtk_window_maximize(GTK_WINDOW(mShell)); + break; + case nsSizeMode_Minimized: + LOG((" set minimized")); + gtk_window_iconify(GTK_WINDOW(mShell)); + break; + case nsSizeMode_Fullscreen: + LOG((" set fullscreen")); + MakeFullScreen(true); + break; + + default: + LOG((" set normal")); + // nsSizeMode_Normal, really. + if (mSizeState == nsSizeMode_Minimized) + gtk_window_deiconify(GTK_WINDOW(mShell)); + else if (mSizeState == nsSizeMode_Maximized) + gtk_window_unmaximize(GTK_WINDOW(mShell)); + break; + } + + // Request mBounds update from configure event as we may not get + // OnSizeAllocate for size state changes (Bug 1489463). + mBoundsAreValid = false; + + mSizeState = mSizeMode; +} + +static bool GetWindowManagerName(GdkWindow* gdk_window, nsACString& wmName) { + if (!gfxPlatformGtk::GetPlatform()->IsX11Display()) { + return false; + } + + Display* xdisplay = gdk_x11_get_default_xdisplay(); + GdkScreen* screen = gdk_window_get_screen(gdk_window); + Window root_win = GDK_WINDOW_XID(gdk_screen_get_root_window(screen)); + + int actual_format_return; + Atom actual_type_return; + unsigned long nitems_return; + unsigned long bytes_after_return; + unsigned char* prop_return = nullptr; + auto releaseXProperty = MakeScopeExit([&] { + if (prop_return) { + XFree(prop_return); + } + }); + + Atom property = XInternAtom(xdisplay, "_NET_SUPPORTING_WM_CHECK", true); + Atom req_type = XInternAtom(xdisplay, "WINDOW", true); + if (!property || !req_type) { + return false; + } + int result = + XGetWindowProperty(xdisplay, root_win, property, + 0L, // offset + sizeof(Window) / 4, // length + false, // delete + req_type, &actual_type_return, &actual_format_return, + &nitems_return, &bytes_after_return, &prop_return); + + if (result != Success || bytes_after_return != 0 || nitems_return != 1) { + return false; + } + + Window wmWindow = reinterpret_cast<Window*>(prop_return)[0]; + if (!wmWindow) { + return false; + } + + XFree(prop_return); + prop_return = nullptr; + + property = XInternAtom(xdisplay, "_NET_WM_NAME", true); + req_type = XInternAtom(xdisplay, "UTF8_STRING", true); + if (!property || !req_type) { + return false; + } + { + // Suppress fatal errors for a missing window. + ScopedXErrorHandler handler; + result = + XGetWindowProperty(xdisplay, wmWindow, property, + 0L, // offset + INT32_MAX, // length + false, // delete + req_type, &actual_type_return, &actual_format_return, + &nitems_return, &bytes_after_return, &prop_return); + } + + if (result != Success || bytes_after_return != 0) { + return false; + } + + wmName = reinterpret_cast<const char*>(prop_return); + return true; +} + +#define kDesktopMutterSchema "org.gnome.mutter" +#define kDesktopDynamicWorkspacesKey "dynamic-workspaces" + +static bool WorkspaceManagementDisabled(GdkWindow* gdk_window) { + if (Preferences::GetBool("widget.disable-workspace-management", false)) { + return true; + } + if (Preferences::HasUserValue("widget.workspace-management")) { + return Preferences::GetBool("widget.workspace-management"); + } + + static const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (currentDesktop && strstr(currentDesktop, "GNOME")) { + // Gnome uses dynamic workspaces by default so disable workspace management + // in that case. + bool usesDynamicWorkspaces = true; + nsCOMPtr<nsIGSettingsService> gsettings = + do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); + if (gsettings) { + nsCOMPtr<nsIGSettingsCollection> mutterSettings; + gsettings->GetCollectionForSchema(nsLiteralCString(kDesktopMutterSchema), + getter_AddRefs(mutterSettings)); + if (mutterSettings) { + if (NS_SUCCEEDED(mutterSettings->GetBoolean( + nsLiteralCString(kDesktopDynamicWorkspacesKey), + &usesDynamicWorkspaces))) { + } + } + } + return usesDynamicWorkspaces; + } + + // When XDG_CURRENT_DESKTOP is missing, try to get window manager name. + if (!currentDesktop) { + nsAutoCString wmName; + if (GetWindowManagerName(gdk_window, wmName)) { + if (wmName.EqualsLiteral("bspwm")) { + return true; + } + if (wmName.EqualsLiteral("i3")) { + return true; + } + } + } + + return false; +} + +void nsWindow::GetWorkspaceID(nsAString& workspaceID) { + workspaceID.Truncate(); + + if (!mIsX11Display || !mShell) { + return; + } + // Get the gdk window for this widget. + GdkWindow* gdk_window = gtk_widget_get_window(mShell); + if (!gdk_window) { + return; + } + + if (WorkspaceManagementDisabled(gdk_window)) { + return; + } + + GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL); + GdkAtom type_returned; + int format_returned; + int length_returned; + long* wm_desktop; + + if (!gdk_property_get(gdk_window, gdk_atom_intern("_NET_WM_DESKTOP", FALSE), + cardinal_atom, + 0, // offset + INT32_MAX, // length + FALSE, // delete + &type_returned, &format_returned, &length_returned, + (guchar**)&wm_desktop)) { + return; + } + + workspaceID.AppendInt((int32_t)wm_desktop[0]); + g_free(wm_desktop); +} + +void nsWindow::MoveToWorkspace(const nsAString& workspaceIDStr) { + nsresult rv = NS_OK; + int32_t workspaceID = workspaceIDStr.ToInteger(&rv); + if (NS_FAILED(rv) || !workspaceID || !mIsX11Display || !mShell) { + return; + } + + // Get the gdk window for this widget. + GdkWindow* gdk_window = gtk_widget_get_window(mShell); + if (!gdk_window) { + return; + } + + // This code is inspired by some found in the 'gxtuner' project. + // https://github.com/brummer10/gxtuner/blob/792d453da0f3a599408008f0f1107823939d730d/deskpager.cpp#L50 + XEvent xevent; + Display* xdisplay = gdk_x11_get_default_xdisplay(); + GdkScreen* screen = gdk_window_get_screen(gdk_window); + Window root_win = GDK_WINDOW_XID(gdk_screen_get_root_window(screen)); + GdkDisplay* display = gdk_window_get_display(gdk_window); + Atom type = gdk_x11_get_xatom_by_name_for_display(display, "_NET_WM_DESKTOP"); + + xevent.type = ClientMessage; + xevent.xclient.type = ClientMessage; + xevent.xclient.serial = 0; + xevent.xclient.send_event = TRUE; + xevent.xclient.display = xdisplay; + xevent.xclient.window = GDK_WINDOW_XID(gdk_window); + xevent.xclient.message_type = type; + xevent.xclient.format = 32; + xevent.xclient.data.l[0] = workspaceID; + xevent.xclient.data.l[1] = X11CurrentTime; + xevent.xclient.data.l[2] = 0; + xevent.xclient.data.l[3] = 0; + xevent.xclient.data.l[4] = 0; + + XSendEvent(xdisplay, root_win, FALSE, + SubstructureNotifyMask | SubstructureRedirectMask, &xevent); + + XFlush(xdisplay); +} + +typedef void (*SetUserTimeFunc)(GdkWindow* aWindow, guint32 aTimestamp); + +static void SetUserTimeAndStartupIDForActivatedWindow(GtkWidget* aWindow) { + nsGTKToolkit* GTKToolkit = nsGTKToolkit::GetToolkit(); + if (!GTKToolkit) return; + + nsAutoCString desktopStartupID; + GTKToolkit->GetDesktopStartupID(&desktopStartupID); + if (desktopStartupID.IsEmpty()) { + // We don't have the data we need. Fall back to an + // approximation ... using the timestamp of the remote command + // being received as a guess for the timestamp of the user event + // that triggered it. + uint32_t timestamp = GTKToolkit->GetFocusTimestamp(); + if (timestamp) { + gdk_window_focus(gtk_widget_get_window(aWindow), timestamp); + GTKToolkit->SetFocusTimestamp(0); + } + return; + } + + gtk_window_set_startup_id(GTK_WINDOW(aWindow), desktopStartupID.get()); + + // If we used the startup ID, that already contains the focus timestamp; + // we don't want to reuse the timestamp next time we raise the window + GTKToolkit->SetFocusTimestamp(0); + GTKToolkit->SetDesktopStartupID(""_ns); +} + +/* static */ +guint32 nsWindow::GetLastUserInputTime() { + // gdk_x11_display_get_user_time/gtk_get_current_event_time tracks + // button and key presses, DESKTOP_STARTUP_ID used to start the app, + // drop events from external drags, + // WM_DELETE_WINDOW delete events, but not usually mouse motion nor + // button and key releases. Therefore use the most recent of + // gdk_x11_display_get_user_time and the last time that we have seen. + GdkDisplay* gdkDisplay = gdk_display_get_default(); + guint32 timestamp = GDK_IS_X11_DISPLAY(gdkDisplay) + ? gdk_x11_display_get_user_time(gdkDisplay) + : gtk_get_current_event_time(); + + if (sLastUserInputTime != GDK_CURRENT_TIME && + TimestampIsNewerThan(sLastUserInputTime, timestamp)) { + return sLastUserInputTime; + } + + return timestamp; +} + +void nsWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) { + // Make sure that our owning widget has focus. If it doesn't try to + // grab it. Note that we don't set our focus flag in this case. + + LOGFOCUS((" SetFocus %d [%p]\n", aRaise == Raise::Yes, (void*)this)); + + GtkWidget* owningWidget = GetMozContainerWidget(); + if (!owningWidget) return; + + // Raise the window if someone passed in true and the prefs are + // set properly. + GtkWidget* toplevelWidget = gtk_widget_get_toplevel(owningWidget); + + if (gRaiseWindows && aRaise == Raise::Yes && toplevelWidget && + !gtk_widget_has_focus(owningWidget) && + !gtk_widget_has_focus(toplevelWidget)) { + GtkWidget* top_window = GetToplevelWidget(); + if (top_window && (gtk_widget_get_visible(top_window))) { + gdk_window_show_unraised(gtk_widget_get_window(top_window)); + // Unset the urgency hint if possible. + SetUrgencyHint(top_window, false); + } + } + + RefPtr<nsWindow> owningWindow = get_window_for_gtk_widget(owningWidget); + if (!owningWindow) return; + + if (aRaise == Raise::Yes) { + // means request toplevel activation. + + // This is asynchronous. + // If and when the window manager accepts the request, then the focus + // widget will get a focus-in-event signal. + if (gRaiseWindows && owningWindow->mIsShown && owningWindow->mShell && + !gtk_window_is_active(GTK_WINDOW(owningWindow->mShell))) { + if (!mIsX11Display && + Preferences::GetBool("testing.browserTestHarness.running", false)) { + // Wayland does not support focus changes so we need to workaround it + // by window hide/show sequence but only when it's running in testsuite. + owningWindow->NativeShow(false); + owningWindow->NativeShow(true); + return; + } + + uint32_t timestamp = GDK_CURRENT_TIME; + + nsGTKToolkit* GTKToolkit = nsGTKToolkit::GetToolkit(); + if (GTKToolkit) timestamp = GTKToolkit->GetFocusTimestamp(); + + LOGFOCUS((" requesting toplevel activation [%p]\n", (void*)this)); + NS_ASSERTION(owningWindow->mWindowType != eWindowType_popup || mParent, + "Presenting an override-redirect window"); + gtk_window_present_with_time(GTK_WINDOW(owningWindow->mShell), timestamp); + + if (GTKToolkit) GTKToolkit->SetFocusTimestamp(0); + } + return; + } + + // aRaise == No means that keyboard events should be dispatched from this + // widget. + + // Ensure owningWidget is the focused GtkWidget within its toplevel window. + // + // For eWindowType_popup, this GtkWidget may not actually be the one that + // receives the key events as it may be the parent window that is active. + if (!gtk_widget_is_focus(owningWidget)) { + // This is synchronous. It takes focus from a plugin or from a widget + // in an embedder. The focus manager already knows that this window + // is active so gBlockActivateEvent avoids another (unnecessary) + // activate notification. + gBlockActivateEvent = true; + gtk_widget_grab_focus(owningWidget); + gBlockActivateEvent = false; + } + + // If this is the widget that already has focus, return. + if (gFocusWindow == this) { + LOGFOCUS((" already have focus [%p]\n", (void*)this)); + return; + } + + // Set this window to be the focused child window + gFocusWindow = this; + + if (mIMContext) { + mIMContext->OnFocusWindow(this); + } + + LOGFOCUS((" widget now has focus in SetFocus() [%p]\n", (void*)this)); +} + +LayoutDeviceIntRect nsWindow::GetScreenBounds() { + LayoutDeviceIntRect rect; + if (mIsTopLevel && mContainer) { + // use the point including window decorations + gint x, y; + gdk_window_get_root_origin(gtk_widget_get_window(GTK_WIDGET(mContainer)), + &x, &y); + rect.MoveTo(GdkPointToDevicePixels({x, y})); + } else { + rect.MoveTo(WidgetToScreenOffset()); + } + // mBounds.Size() is the window bounds, not the window-manager frame + // bounds (bug 581863). gdk_window_get_frame_extents would give the + // frame bounds, but mBounds.Size() is returned here for consistency + // with Resize. + rect.SizeTo(mBounds.Size()); +#if MOZ_LOGGING + gint scale = GdkScaleFactor(); + LOG(("GetScreenBounds [%p] %d,%d -> %d x %d, unscaled %d,%d -> %d x %d\n", + this, rect.x, rect.y, rect.width, rect.height, rect.x / scale, + rect.y / scale, rect.width / scale, rect.height / scale)); +#endif + return rect; +} + +LayoutDeviceIntSize nsWindow::GetClientSize() { + return LayoutDeviceIntSize(mBounds.width, mBounds.height); +} + +LayoutDeviceIntRect nsWindow::GetClientBounds() { + // GetBounds returns a rect whose top left represents the top left of the + // outer bounds, but whose width/height represent the size of the inner + // bounds (which is messed up). + LayoutDeviceIntRect rect = GetBounds(); + rect.MoveBy(GetClientOffset()); + return rect; +} + +void nsWindow::UpdateClientOffsetFromFrameExtents() { + AUTO_PROFILER_LABEL("nsWindow::UpdateClientOffsetFromFrameExtents", OTHER); + + if (mCSDSupportLevel == CSD_SUPPORT_CLIENT && mDrawInTitlebar) { + return; + } + + if (!mIsTopLevel || !mShell || + gtk_window_get_window_type(GTK_WINDOW(mShell)) == GTK_WINDOW_POPUP) { + mClientOffset = nsIntPoint(0, 0); + return; + } + + GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL); + + GdkAtom type_returned; + int format_returned; + int length_returned; + long* frame_extents; + + if (!gdk_property_get(gtk_widget_get_window(mShell), + gdk_atom_intern("_NET_FRAME_EXTENTS", FALSE), + cardinal_atom, + 0, // offset + 4 * 4, // length + FALSE, // delete + &type_returned, &format_returned, &length_returned, + (guchar**)&frame_extents) || + length_returned / sizeof(glong) != 4) { + mClientOffset = nsIntPoint(0, 0); + } else { + // data returned is in the order left, right, top, bottom + auto left = int32_t(frame_extents[0]); + auto top = int32_t(frame_extents[2]); + g_free(frame_extents); + + mClientOffset = nsIntPoint(left, top); + } + + // Send a WindowMoved notification. This ensures that BrowserParent + // picks up the new client offset and sends it to the child process + // if appropriate. + NotifyWindowMoved(mBounds.x, mBounds.y); + + LOG(("nsWindow::UpdateClientOffsetFromFrameExtents [%p] %d,%d\n", (void*)this, + mClientOffset.x, mClientOffset.y)); +} + +LayoutDeviceIntPoint nsWindow::GetClientOffset() { + return mIsX11Display ? LayoutDeviceIntPoint::FromUnknownPoint(mClientOffset) + : LayoutDeviceIntPoint(0, 0); +} + +gboolean nsWindow::OnPropertyNotifyEvent(GtkWidget* aWidget, + GdkEventProperty* aEvent) { + if (aEvent->atom == gdk_atom_intern("_NET_FRAME_EXTENTS", FALSE)) { + UpdateClientOffsetFromFrameExtents(); + return FALSE; + } + + if (GetCurrentTimeGetter()->PropertyNotifyHandler(aWidget, aEvent)) { + return TRUE; + } + + return FALSE; +} + +static GdkCursor* GetCursorForImage(imgIContainer* aCursorImage, + uint32_t aHotspotX, uint32_t aHotspotY) { + if (!aCursorImage) { + return nullptr; + } + GdkPixbuf* pixbuf = nsImageToPixbuf::ImageToPixbuf(aCursorImage); + if (!pixbuf) { + return nullptr; + } + + int width = gdk_pixbuf_get_width(pixbuf); + int height = gdk_pixbuf_get_height(pixbuf); + + auto CleanupPixBuf = + mozilla::MakeScopeExit([&]() { g_object_unref(pixbuf); }); + + // Reject cursors greater than 128 pixels in some direction, to prevent + // spoofing. + // XXX ideally we should rescale. Also, we could modify the API to + // allow trusted content to set larger cursors. + // + // TODO(emilio, bug 1445844): Unify the solution for this with other + // platforms. + if (width > 128 || height > 128) { + return nullptr; + } + + // Looks like all cursors need an alpha channel (tested on Gtk 2.4.4). This + // is of course not documented anywhere... + // So add one if there isn't one yet + if (!gdk_pixbuf_get_has_alpha(pixbuf)) { + GdkPixbuf* alphaBuf = gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0); + g_object_unref(pixbuf); + pixbuf = alphaBuf; + if (!alphaBuf) { + return nullptr; + } + } + + return gdk_cursor_new_from_pixbuf(gdk_display_get_default(), pixbuf, + aHotspotX, aHotspotY); +} + +void nsWindow::SetCursor(nsCursor aDefaultCursor, imgIContainer* aCursorImage, + uint32_t aHotspotX, uint32_t aHotspotY) { + // if we're not the toplevel window pass up the cursor request to + // the toplevel window to handle it. + if (!mContainer && mGdkWindow) { + nsWindow* window = GetContainerWindow(); + if (!window) return; + + window->SetCursor(aDefaultCursor, aCursorImage, aHotspotX, aHotspotY); + return; + } + + // Only change cursor if it's actually been changed + if (!aCursorImage && aDefaultCursor == mCursor && !mUpdateCursor) { + return; + } + + mUpdateCursor = false; + mCursor = eCursorInvalid; + + // Try to set the cursor image first, and fall back to the numeric cursor. + GdkCursor* newCursor = GetCursorForImage(aCursorImage, aHotspotX, aHotspotY); + if (!newCursor) { + newCursor = get_gtk_cursor(aDefaultCursor); + if (newCursor) { + mCursor = aDefaultCursor; + } + } + + auto CleanupCursor = mozilla::MakeScopeExit([&]() { + // get_gtk_cursor returns a weak reference, which we shouldn't unref. + if (newCursor && mCursor == eCursorInvalid) { + g_object_unref(newCursor); + } + }); + + if (!newCursor || !mContainer) { + return; + } + + gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(mContainer)), + newCursor); +} + +void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) { + if (!mGdkWindow) return; + + GdkRectangle rect = DevicePixelsToGdkRectRoundOut(aRect); + gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE); + + LOGDRAW(("Invalidate (rect) [%p]: %d %d %d %d\n", (void*)this, rect.x, rect.y, + rect.width, rect.height)); +} + +void* nsWindow::GetNativeData(uint32_t aDataType) { + switch (aDataType) { + case NS_NATIVE_WINDOW: + case NS_NATIVE_WIDGET: { + if (!mGdkWindow) return nullptr; + + return mGdkWindow; + } + + case NS_NATIVE_DISPLAY: { +#ifdef MOZ_X11 + GdkDisplay* gdkDisplay = gdk_display_get_default(); + if (gdkDisplay && GDK_IS_X11_DISPLAY(gdkDisplay)) { + return GDK_DISPLAY_XDISPLAY(gdkDisplay); + } +#endif /* MOZ_X11 */ + // Don't bother to return native display on Wayland as it's for + // X11 only NPAPI plugins. + return nullptr; + } + case NS_NATIVE_SHELLWIDGET: + return GetToplevelWidget(); + + case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID: + case NS_NATIVE_SHAREABLE_WINDOW: + if (mIsX11Display) { + return (void*)GDK_WINDOW_XID(gdk_window_get_toplevel(mGdkWindow)); + } + NS_WARNING( + "nsWindow::GetNativeData(): " + "NS_NATIVE_SHAREABLE_WINDOW / NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID is " + "not " + "handled on Wayland!"); + return nullptr; + case NS_RAW_NATIVE_IME_CONTEXT: { + void* pseudoIMEContext = GetPseudoIMEContext(); + if (pseudoIMEContext) { + return pseudoIMEContext; + } + // If IME context isn't available on this widget, we should set |this| + // instead of nullptr. + if (!mIMContext) { + return this; + } + return mIMContext.get(); + } + case NS_NATIVE_OPENGL_CONTEXT: + return nullptr; + case NS_NATIVE_EGL_WINDOW: { + if (mIsX11Display) { + return mGdkWindow ? (void*)GDK_WINDOW_XID(mGdkWindow) : nullptr; + } +#ifdef MOZ_WAYLAND + if (mContainer) { + return moz_container_wayland_get_egl_window(mContainer, + GdkScaleFactor()); + } +#endif + return nullptr; + } + default: + NS_WARNING("nsWindow::GetNativeData called with bad value"); + return nullptr; + } +} + +nsresult nsWindow::SetTitle(const nsAString& aTitle) { + if (!mShell) return NS_OK; + + // convert the string into utf8 and set the title. +#define UTF8_FOLLOWBYTE(ch) (((ch)&0xC0) == 0x80) + NS_ConvertUTF16toUTF8 titleUTF8(aTitle); + if (titleUTF8.Length() > NS_WINDOW_TITLE_MAX_LENGTH) { + // Truncate overlong titles (bug 167315). Make sure we chop after a + // complete sequence by making sure the next char isn't a follow-byte. + uint32_t len = NS_WINDOW_TITLE_MAX_LENGTH; + while (UTF8_FOLLOWBYTE(titleUTF8[len])) --len; + titleUTF8.Truncate(len); + } + gtk_window_set_title(GTK_WINDOW(mShell), (const char*)titleUTF8.get()); + + return NS_OK; +} + +void nsWindow::SetIcon(const nsAString& aIconSpec) { + if (!mShell) return; + + nsAutoCString iconName; + + if (aIconSpec.EqualsLiteral("default")) { + nsAutoString brandName; + WidgetUtils::GetBrandShortName(brandName); + if (brandName.IsEmpty()) { + brandName.AssignLiteral(u"Mozilla"); + } + AppendUTF16toUTF8(brandName, iconName); + ToLowerCase(iconName); + } else { + AppendUTF16toUTF8(aIconSpec, iconName); + } + + nsCOMPtr<nsIFile> iconFile; + nsAutoCString path; + + gint* iconSizes = gtk_icon_theme_get_icon_sizes(gtk_icon_theme_get_default(), + iconName.get()); + bool foundIcon = (iconSizes[0] != 0); + g_free(iconSizes); + + if (!foundIcon) { + // Look for icons with the following suffixes appended to the base name + // The last two entries (for the old XPM format) will be ignored unless + // no icons are found using other suffixes. XPM icons are deprecated. + + const char16_t extensions[9][8] = {u".png", u"16.png", u"32.png", + u"48.png", u"64.png", u"128.png", + u"256.png", u".xpm", u"16.xpm"}; + + for (uint32_t i = 0; i < ArrayLength(extensions); i++) { + // Don't bother looking for XPM versions if we found a PNG. + if (i == ArrayLength(extensions) - 2 && foundIcon) break; + + ResolveIconName(aIconSpec, nsDependentString(extensions[i]), + getter_AddRefs(iconFile)); + if (iconFile) { + iconFile->GetNativePath(path); + GdkPixbuf* icon = gdk_pixbuf_new_from_file(path.get(), nullptr); + if (icon) { + gtk_icon_theme_add_builtin_icon(iconName.get(), + gdk_pixbuf_get_height(icon), icon); + g_object_unref(icon); + foundIcon = true; + } + } + } + } + + // leave the default icon intact if no matching icons were found + if (foundIcon) { + gtk_window_set_icon_name(GTK_WINDOW(mShell), iconName.get()); + } +} + +LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() { + nsIntPoint origin(0, 0); + GetWindowOrigin(mGdkWindow, &origin.x, &origin.y); + + return GdkPointToDevicePixels({origin.x, origin.y}); +} + +void nsWindow::CaptureMouse(bool aCapture) { + LOG(("CaptureMouse %p\n", (void*)this)); + + if (!mGdkWindow) return; + + if (!mContainer) return; + + if (aCapture) { + gtk_grab_add(GTK_WIDGET(mContainer)); + GrabPointer(GetLastUserInputTime()); + } else { + ReleaseGrabs(); + gtk_grab_remove(GTK_WIDGET(mContainer)); + } +} + +void nsWindow::CaptureRollupEvents(nsIRollupListener* aListener, + bool aDoCapture) { + if (!mGdkWindow) return; + + if (!mContainer) return; + + LOG(("CaptureRollupEvents %p %i\n", this, int(aDoCapture))); + + if (aDoCapture) { + gRollupListener = aListener; + // Don't add a grab if a drag is in progress, or if the widget is a drag + // feedback popup. (panels with type="drag"). + if (!mIsDragPopup && !nsWindow::DragInProgress()) { + gtk_grab_add(GTK_WIDGET(mContainer)); + GrabPointer(GetLastUserInputTime()); + } + } else { + if (!nsWindow::DragInProgress()) { + ReleaseGrabs(); + } + // There may not have been a drag in process when aDoCapture was set, + // so make sure to remove any added grab. This is a no-op if the grab + // was not added to this widget. + gtk_grab_remove(GTK_WIDGET(mContainer)); + gRollupListener = nullptr; + } +} + +nsresult nsWindow::GetAttention(int32_t aCycleCount) { + LOG(("nsWindow::GetAttention [%p]\n", (void*)this)); + + GtkWidget* top_window = GetToplevelWidget(); + GtkWidget* top_focused_window = + gFocusWindow ? gFocusWindow->GetToplevelWidget() : nullptr; + + // Don't get attention if the window is focused anyway. + if (top_window && (gtk_widget_get_visible(top_window)) && + top_window != top_focused_window) { + SetUrgencyHint(top_window, true); + } + + return NS_OK; +} + +bool nsWindow::HasPendingInputEvent() { + // This sucks, but gtk/gdk has no way to answer the question we want while + // excluding paint events, and there's no X API that will let us peek + // without blocking or removing. To prevent event reordering, peek + // anything except expose events. Reordering expose and others should be + // ok, hopefully. + bool haveEvent = false; +#ifdef MOZ_X11 + XEvent ev; + if (mIsX11Display) { + Display* display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default()); + haveEvent = XCheckMaskEvent( + display, + KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + EnterWindowMask | LeaveWindowMask | PointerMotionMask | + PointerMotionHintMask | Button1MotionMask | Button2MotionMask | + Button3MotionMask | Button4MotionMask | Button5MotionMask | + ButtonMotionMask | KeymapStateMask | VisibilityChangeMask | + StructureNotifyMask | ResizeRedirectMask | SubstructureNotifyMask | + SubstructureRedirectMask | FocusChangeMask | PropertyChangeMask | + ColormapChangeMask | OwnerGrabButtonMask, + &ev); + if (haveEvent) { + XPutBackEvent(display, &ev); + } + } +#endif + return haveEvent; +} + +#if 0 +# ifdef DEBUG +// Paint flashing code (disabled for cairo - see below) + +# define CAPS_LOCK_IS_ON \ + (KeymapWrapper::AreModifiersCurrentlyActive(KeymapWrapper::CAPS_LOCK)) + +# define WANT_PAINT_FLASHING (debug_WantPaintFlashing() && CAPS_LOCK_IS_ON) + +# ifdef MOZ_X11 +static void +gdk_window_flash(GdkWindow * aGdkWindow, + unsigned int aTimes, + unsigned int aInterval, // Milliseconds + GdkRegion * aRegion) +{ + gint x; + gint y; + gint width; + gint height; + guint i; + GdkGC * gc = 0; + GdkColor white; + + gdk_window_get_geometry(aGdkWindow,nullptr,nullptr,&width,&height); + + gdk_window_get_origin (aGdkWindow, + &x, + &y); + + gc = gdk_gc_new(gdk_get_default_root_window()); + + white.pixel = WhitePixel(gdk_display,DefaultScreen(gdk_display)); + + gdk_gc_set_foreground(gc,&white); + gdk_gc_set_function(gc,GDK_XOR); + gdk_gc_set_subwindow(gc,GDK_INCLUDE_INFERIORS); + + gdk_region_offset(aRegion, x, y); + gdk_gc_set_clip_region(gc, aRegion); + + /* + * Need to do this twice so that the XOR effect can replace + * the original window contents. + */ + for (i = 0; i < aTimes * 2; i++) + { + gdk_draw_rectangle(gdk_get_default_root_window(), + gc, + TRUE, + x, + y, + width, + height); + + gdk_flush(); + + PR_Sleep(PR_MillisecondsToInterval(aInterval)); + } + + gdk_gc_destroy(gc); + + gdk_region_offset(aRegion, -x, -y); +} +# endif /* MOZ_X11 */ +# endif // DEBUG +#endif + +#ifdef cairo_copy_clip_rectangle_list +# error "Looks like we're including Mozilla's cairo instead of system cairo" +#endif +static bool ExtractExposeRegion(LayoutDeviceIntRegion& aRegion, cairo_t* cr) { + cairo_rectangle_list_t* rects = cairo_copy_clip_rectangle_list(cr); + if (rects->status != CAIRO_STATUS_SUCCESS) { + NS_WARNING("Failed to obtain cairo rectangle list."); + return false; + } + + for (int i = 0; i < rects->num_rectangles; i++) { + const cairo_rectangle_t& r = rects->rectangles[i]; + aRegion.Or(aRegion, + LayoutDeviceIntRect::Truncate(r.x, r.y, r.width, r.height)); + LOGDRAW(("\t%f %f %f %f\n", r.x, r.y, r.width, r.height)); + } + + cairo_rectangle_list_destroy(rects); + return true; +} + +#ifdef MOZ_WAYLAND +void nsWindow::MaybeResumeCompositor() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + if (mIsDestroyed || !mNeedsCompositorResume) { + return; + } + + if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) { + MOZ_ASSERT(mCompositorWidgetDelegate); + if (mCompositorWidgetDelegate) { + mCompositorInitiallyPaused = false; + mNeedsCompositorResume = false; + remoteRenderer->SendResumeAsync(); + } + remoteRenderer->SendForcePresent(); + } +} + +void nsWindow::CreateCompositorVsyncDispatcher() { + if (!mWaylandVsyncSource) { + nsBaseWidget::CreateCompositorVsyncDispatcher(); + return; + } + + if (XRE_IsParentProcess()) { + if (!mCompositorVsyncDispatcherLock) { + mCompositorVsyncDispatcherLock = + MakeUnique<Mutex>("mCompositorVsyncDispatcherLock"); + } + MutexAutoLock lock(*mCompositorVsyncDispatcherLock); + if (!mCompositorVsyncDispatcher) { + mCompositorVsyncDispatcher = + new CompositorVsyncDispatcher(mWaylandVsyncSource); + } + } +} +#endif + +gboolean nsWindow::OnExposeEvent(cairo_t* cr) { + // Send any pending resize events so that layout can update. + // May run event loop. + MaybeDispatchResized(); + + if (mIsDestroyed) { + return FALSE; + } + + // Windows that are not visible will be painted after they become visible. + if (!mGdkWindow || !mHasMappedToplevel) { + return FALSE; + } +#ifdef MOZ_WAYLAND + if (!mIsX11Display && !moz_container_wayland_can_draw(mContainer)) { + return FALSE; + } +#endif + + nsIWidgetListener* listener = GetListener(); + if (!listener) return FALSE; + + LOGDRAW(("received expose event [%p] %p 0x%lx (rects follow):\n", this, + mGdkWindow, mIsX11Display ? gdk_x11_window_get_xid(mGdkWindow) : 0)); + LayoutDeviceIntRegion exposeRegion; + if (!ExtractExposeRegion(exposeRegion, cr)) { + return FALSE; + } + + gint scale = GdkScaleFactor(); + LayoutDeviceIntRegion region = exposeRegion; + region.ScaleRoundOut(scale, scale); + + if (GetLayerManager()->AsKnowsCompositor() && mCompositorSession) { + // We need to paint to the screen even if nothing changed, since if we + // don't have a compositing window manager, our pixels could be stale. + GetLayerManager()->SetNeedsComposite(true); + GetLayerManager()->SendInvalidRegion(region.ToUnknownRegion()); + } + + RefPtr<nsWindow> strongThis(this); + + // Dispatch WillPaintWindow notification to allow scripts etc. to run + // before we paint + { + listener->WillPaintWindow(this); + + // If the window has been destroyed during the will paint notification, + // there is nothing left to do. + if (!mGdkWindow) return TRUE; + + // Re-get the listener since the will paint notification might have + // killed it. + listener = GetListener(); + if (!listener) return FALSE; + } + + if (GetLayerManager()->AsKnowsCompositor() && + GetLayerManager()->NeedsComposite()) { + GetLayerManager()->ScheduleComposite(); + GetLayerManager()->SetNeedsComposite(false); + } + + // Our bounds may have changed after calling WillPaintWindow. Clip + // to the new bounds here. The region is relative to this + // window. + region.And(region, LayoutDeviceIntRect(0, 0, mBounds.width, mBounds.height)); + + bool shaped = false; + if (eTransparencyTransparent == GetTransparencyMode()) { + auto window = static_cast<nsWindow*>(GetTopLevelWidget()); + if (mTransparencyBitmapForTitlebar) { + if (mSizeState == nsSizeMode_Normal) { + window->UpdateTitlebarTransparencyBitmap(); + } else { + window->ClearTransparencyBitmap(); + } + } else { + if (mHasAlphaVisual) { + // Remove possible shape mask from when window manger was not + // previously compositing. + window->ClearTransparencyBitmap(); + } else { + shaped = true; + } + } + } + + if (!shaped) { + GList* children = gdk_window_peek_children(mGdkWindow); + while (children) { + GdkWindow* gdkWin = GDK_WINDOW(children->data); + nsWindow* kid = get_window_for_gdk_window(gdkWin); + if (kid && gdk_window_is_visible(gdkWin)) { + AutoTArray<LayoutDeviceIntRect, 1> clipRects; + kid->GetWindowClipRegion(&clipRects); + LayoutDeviceIntRect bounds = kid->GetBounds(); + for (uint32_t i = 0; i < clipRects.Length(); ++i) { + LayoutDeviceIntRect r = clipRects[i] + bounds.TopLeft(); + region.Sub(region, r); + } + } + children = children->next; + } + } + + if (region.IsEmpty()) { + return TRUE; + } + + // If this widget uses OMTC... + if (GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT || + GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR) { + listener->PaintWindow(this, region); + + // Re-get the listener since the will paint notification might have + // killed it. + listener = GetListener(); + if (!listener) return TRUE; + + listener->DidPaintWindow(); + return TRUE; + } + + BufferMode layerBuffering = BufferMode::BUFFERED; + RefPtr<DrawTarget> dt = StartRemoteDrawingInRegion(region, &layerBuffering); + if (!dt || !dt->IsValid()) { + return FALSE; + } + RefPtr<gfxContext> ctx; + IntRect boundsRect = region.GetBounds().ToUnknownRect(); + IntPoint offset(0, 0); + if (dt->GetSize() == boundsRect.Size()) { + offset = boundsRect.TopLeft(); + dt->SetTransform(Matrix::Translation(-offset)); + } + +#ifdef MOZ_X11 + if (shaped) { + // Collapse update area to the bounding box. This is so we only have to + // call UpdateTranslucentWindowAlpha once. After we have dropped + // support for non-Thebes graphics, UpdateTranslucentWindowAlpha will be + // our private interface so we can rework things to avoid this. + dt->PushClipRect(Rect(boundsRect)); + + // The double buffering is done here to extract the shape mask. + // (The shape mask won't be necessary when a visual with an alpha + // channel is used on compositing window managers.) + layerBuffering = BufferMode::BUFFER_NONE; + RefPtr<DrawTarget> destDT = + dt->CreateSimilarDrawTarget(boundsRect.Size(), SurfaceFormat::B8G8R8A8); + if (!destDT || !destDT->IsValid()) { + return FALSE; + } + destDT->SetTransform(Matrix::Translation(-boundsRect.TopLeft())); + ctx = gfxContext::CreatePreservingTransformOrNull(destDT); + } else { + gfxUtils::ClipToRegion(dt, region.ToUnknownRegion()); + ctx = gfxContext::CreatePreservingTransformOrNull(dt); + } + MOZ_ASSERT(ctx); // checked both dt and destDT valid draw target above + +# if 0 + // NOTE: Paint flashing region would be wrong for cairo, since + // cairo inflates the update region, etc. So don't paint flash + // for cairo. +# ifdef DEBUG + // XXX aEvent->region may refer to a newly-invalid area. FIXME + if (0 && WANT_PAINT_FLASHING && gtk_widget_get_window(aEvent)) + gdk_window_flash(mGdkWindow, 1, 100, aEvent->region); +# endif +# endif + +#endif // MOZ_X11 + + bool painted = false; + { + if (GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC) { + if (GetTransparencyMode() == eTransparencyTransparent && + layerBuffering == BufferMode::BUFFER_NONE && mHasAlphaVisual) { + // If our draw target is unbuffered and we use an alpha channel, + // clear the image beforehand to ensure we don't get artifacts from a + // reused SHM image. See bug 1258086. + dt->ClearRect(Rect(boundsRect)); + } + AutoLayerManagerSetup setupLayerManager(this, ctx, layerBuffering); + painted = listener->PaintWindow(this, region); + + // Re-get the listener since the will paint notification might have + // killed it. + listener = GetListener(); + if (!listener) return TRUE; + } + } + +#ifdef MOZ_X11 + // PaintWindow can Destroy us (bug 378273), avoid doing any paint + // operations below if that happened - it will lead to XError and exit(). + if (shaped) { + if (MOZ_LIKELY(!mIsDestroyed)) { + if (painted) { + RefPtr<SourceSurface> surf = ctx->GetDrawTarget()->Snapshot(); + + UpdateAlpha(surf, boundsRect); + + dt->DrawSurface(surf, Rect(boundsRect), + Rect(0, 0, boundsRect.width, boundsRect.height), + DrawSurfaceOptions(SamplingFilter::POINT), + DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + } + } + } + + ctx = nullptr; + dt->PopClip(); + +#endif // MOZ_X11 + + EndRemoteDrawingInRegion(dt, region); + + listener->DidPaintWindow(); + + // Synchronously flush any new dirty areas + cairo_region_t* dirtyArea = gdk_window_get_update_area(mGdkWindow); + + if (dirtyArea) { + gdk_window_invalidate_region(mGdkWindow, dirtyArea, false); + cairo_region_destroy(dirtyArea); + gdk_window_process_updates(mGdkWindow, false); + } + + // check the return value! + return TRUE; +} + +void nsWindow::UpdateAlpha(SourceSurface* aSourceSurface, + nsIntRect aBoundsRect) { + // We need to create our own buffer to force the stride to match the + // expected stride. + int32_t stride = + GetAlignedStride<4>(aBoundsRect.width, BytesPerPixel(SurfaceFormat::A8)); + if (stride == 0) { + return; + } + int32_t bufferSize = stride * aBoundsRect.height; + auto imageBuffer = MakeUniqueFallible<uint8_t[]>(bufferSize); + { + RefPtr<DrawTarget> drawTarget = gfxPlatform::CreateDrawTargetForData( + imageBuffer.get(), aBoundsRect.Size(), stride, SurfaceFormat::A8); + + if (drawTarget) { + drawTarget->DrawSurface(aSourceSurface, + Rect(0, 0, aBoundsRect.width, aBoundsRect.height), + Rect(0, 0, aSourceSurface->GetSize().width, + aSourceSurface->GetSize().height), + DrawSurfaceOptions(SamplingFilter::POINT), + DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + } + } + UpdateTranslucentWindowAlphaInternal(aBoundsRect, imageBuffer.get(), stride); +} + +gboolean nsWindow::OnConfigureEvent(GtkWidget* aWidget, + GdkEventConfigure* aEvent) { + // These events are only received on toplevel windows. + // + // GDK ensures that the coordinates are the client window top-left wrt the + // root window. + // + // GDK calculates the cordinates for real ConfigureNotify events on + // managed windows (that would normally be relative to the parent + // window). + // + // Synthetic ConfigureNotify events are from the window manager and + // already relative to the root window. GDK creates all X windows with + // border_width = 0, so synthetic events also indicate the top-left of + // the client window. + // + // Override-redirect windows are children of the root window so parent + // coordinates are root coordinates. + + LOG(("configure event [%p] %d %d %d %d\n", (void*)this, aEvent->x, aEvent->y, + aEvent->width, aEvent->height)); + + if (mPendingConfigures > 0) { + mPendingConfigures--; + } + + LayoutDeviceIntRect screenBounds = GetScreenBounds(); + + if (mWindowType == eWindowType_toplevel || + mWindowType == eWindowType_dialog) { + // This check avoids unwanted rollup on spurious configure events from + // Cygwin/X (bug 672103). + if (mBounds.x != screenBounds.x || mBounds.y != screenBounds.y) { + CheckForRollup(0, 0, false, true); + } + } + + NS_ASSERTION(GTK_IS_WINDOW(aWidget), + "Configure event on widget that is not a GtkWindow"); + if (gtk_window_get_window_type(GTK_WINDOW(aWidget)) == GTK_WINDOW_POPUP) { + // Override-redirect window + // + // These windows should not be moved by the window manager, and so any + // change in position is a result of our direction. mBounds has + // already been set in std::move() or Resize(), and that is more + // up-to-date than the position in the ConfigureNotify event if the + // event is from an earlier window move. + // + // Skipping the WindowMoved call saves context menus from an infinite + // loop when nsXULPopupManager::PopupMoved moves the window to the new + // position and nsMenuPopupFrame::SetPopupPosition adds + // offsetForContextMenu on each iteration. + + // Our back buffer might have been invalidated while we drew the last + // frame, and its contents might be incorrect. See bug 1280653 comment 7 + // and comment 10. Specifically we must ensure we recomposite the frame + // as soon as possible to avoid the corrupted frame being displayed. + GetLayerManager()->FlushRendering(); + return FALSE; + } + + mBounds.MoveTo(screenBounds.TopLeft()); + + // XXX mozilla will invalidate the entire window after this move + // complete. wtf? + NotifyWindowMoved(mBounds.x, mBounds.y); + + // A GTK app would usually update its client area size in response to + // a "size-allocate" signal. + // However, we need to set mBounds in advance at Resize() + // as JS code expects immediate window size change. + // If Gecko requests a resize from GTK, but subsequently, + // before a corresponding "size-allocate" signal is emitted, the window is + // resized to its former size via other means, such as maximizing, + // then there is no "size-allocate" signal from which to update + // the value of mBounds. Similarly, if Gecko's resize request is refused + // by the window manager, then there will be no "size-allocate" signal. + // In the refused request case, the window manager is required to dispatch + // a ConfigureNotify event. mBounds can then be updated here. + // This seems to also be sufficient to update mBounds when Gecko resizes + // the window from maximized size and then immediately maximizes again. + if (!mBoundsAreValid) { + GtkAllocation allocation = {-1, -1, 0, 0}; + gtk_window_get_size(GTK_WINDOW(mShell), &allocation.width, + &allocation.height); + OnSizeAllocate(&allocation); + } + + return FALSE; +} + +void nsWindow::OnContainerUnrealize() { + // The GdkWindows are about to be destroyed (but not deleted), so remove + // their references back to their container widget while the GdkWindow + // hierarchy is still available. + + if (mGdkWindow) { + DestroyChildWindows(); + + g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", nullptr); + mGdkWindow = nullptr; + } +} + +void nsWindow::OnSizeAllocate(GtkAllocation* aAllocation) { + LOG(("nsWindow::OnSizeAllocate [%p] %d,%d -> %d x %d\n", (void*)this, + aAllocation->x, aAllocation->y, aAllocation->width, + aAllocation->height)); + + // Client offset are updated by _NET_FRAME_EXTENTS on X11 when system titlebar + // is enabled. In either cases (Wayland or system titlebar is off on X11) + // we don't get _NET_FRAME_EXTENTS X11 property notification so we derive + // it from mContainer position. + if (mCSDSupportLevel == CSD_SUPPORT_CLIENT) { + if (!mIsX11Display || (mIsX11Display && mDrawInTitlebar)) { + UpdateClientOffsetFromCSDWindow(); + } + } + + mBoundsAreValid = true; + + LayoutDeviceIntSize size = GdkRectToDevicePixels(*aAllocation).Size(); + if (mBounds.Size() == size) { + LOG((" Already the same size")); + // We were already resized at nsWindow::OnConfigureEvent() so skip it. + return; + } + + // Invalidate the new part of the window now for the pending paint to + // minimize background flashes (GDK does not do this for external resizes + // of toplevels.) + if (mBounds.width < size.width) { + GdkRectangle rect = DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect( + mBounds.width, 0, size.width - mBounds.width, size.height)); + gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE); + } + if (mBounds.height < size.height) { + GdkRectangle rect = DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect( + 0, mBounds.height, size.width, size.height - mBounds.height)); + gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE); + } + + mBounds.SizeTo(size); + +#ifdef MOZ_X11 + // Notify the GtkCompositorWidget of a ClientSizeChange + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize()); + } +#endif + + // Gecko permits running nested event loops during processing of events, + // GtkWindow callers of gtk_widget_size_allocate expect the signal + // handlers to return sometime in the near future. + mNeedsDispatchResized = true; + NS_DispatchToCurrentThread(NewRunnableMethod( + "nsWindow::MaybeDispatchResized", this, &nsWindow::MaybeDispatchResized)); +} + +void nsWindow::OnDeleteEvent() { + if (mWidgetListener) mWidgetListener->RequestWindowClose(this); +} + +void nsWindow::OnEnterNotifyEvent(GdkEventCrossing* aEvent) { + // This skips NotifyVirtual and NotifyNonlinearVirtual enter notify events + // when the pointer enters a child window. If the destination window is a + // Gecko window then we'll catch the corresponding event on that window, + // but we won't notice when the pointer directly enters a foreign (plugin) + // child window without passing over a visible portion of a Gecko window. + if (aEvent->subwindow != nullptr) return; + + // Check before is_parent_ungrab_enter() as the button state may have + // changed while a non-Gecko ancestor window had a pointer grab. + DispatchMissedButtonReleases(aEvent); + + if (is_parent_ungrab_enter(aEvent)) return; + + WidgetMouseEvent event(true, eMouseEnterIntoWidget, this, + WidgetMouseEvent::eReal); + + event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); + + LOG(("OnEnterNotify: %p\n", (void*)this)); + + DispatchInputEvent(&event); +} + +// XXX Is this the right test for embedding cases? +static bool is_top_level_mouse_exit(GdkWindow* aWindow, + GdkEventCrossing* aEvent) { + auto x = gint(aEvent->x_root); + auto y = gint(aEvent->y_root); + GdkDisplay* display = gdk_window_get_display(aWindow); + GdkWindow* winAtPt = gdk_display_get_window_at_pointer(display, &x, &y); + if (!winAtPt) return true; + GdkWindow* topLevelAtPt = gdk_window_get_toplevel(winAtPt); + GdkWindow* topLevelWidget = gdk_window_get_toplevel(aWindow); + return topLevelAtPt != topLevelWidget; +} + +void nsWindow::OnLeaveNotifyEvent(GdkEventCrossing* aEvent) { + // This ignores NotifyVirtual and NotifyNonlinearVirtual leave notify + // events when the pointer leaves a child window. If the destination + // window is a Gecko window then we'll catch the corresponding event on + // that window. + // + // XXXkt However, we will miss toplevel exits when the pointer directly + // leaves a foreign (plugin) child window without passing over a visible + // portion of a Gecko window. + if (aEvent->subwindow != nullptr) return; + + WidgetMouseEvent event(true, eMouseExitFromWidget, this, + WidgetMouseEvent::eReal); + + event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); + + event.mExitFrom = Some(is_top_level_mouse_exit(mGdkWindow, aEvent) + ? WidgetMouseEvent::ePlatformTopLevel + : WidgetMouseEvent::ePlatformChild); + + LOG(("OnLeaveNotify: %p\n", (void*)this)); + + DispatchInputEvent(&event); +} + +template <typename Event> +static LayoutDeviceIntPoint GetRefPoint(nsWindow* aWindow, Event* aEvent) { + if (aEvent->window == aWindow->GetGdkWindow()) { + // we are the window that the event happened on so no need for expensive + // WidgetToScreenOffset + return aWindow->GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + } + // XXX we're never quite sure which GdkWindow the event came from due to our + // custom bubbling in scroll_event_cb(), so use ScreenToWidget to translate + // the screen root coordinates into coordinates relative to this widget. + return aWindow->GdkEventCoordsToDevicePixels(aEvent->x_root, aEvent->y_root) - + aWindow->WidgetToScreenOffset(); +} + +void nsWindow::OnMotionNotifyEvent(GdkEventMotion* aEvent) { + if (mWindowShouldStartDragging) { + mWindowShouldStartDragging = false; + // find the top-level window + GdkWindow* gdk_window = gdk_window_get_toplevel(mGdkWindow); + MOZ_ASSERT(gdk_window, "gdk_window_get_toplevel should not return null"); + + bool canDrag = true; + if (mIsX11Display) { + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=789054 + // To avoid crashes disable double-click on WM without _NET_WM_MOVERESIZE. + // See _should_perform_ewmh_drag() at gdkwindow-x11.c + GdkScreen* screen = gdk_window_get_screen(gdk_window); + GdkAtom atom = gdk_atom_intern("_NET_WM_MOVERESIZE", FALSE); + if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) { + canDrag = false; + } + } + + if (canDrag) { + gdk_window_begin_move_drag(gdk_window, 1, aEvent->x_root, aEvent->y_root, + aEvent->time); + return; + } + } + + // see if we can compress this event + // XXXldb Why skip every other motion event when we have multiple, + // but not more than that? + bool synthEvent = false; +#ifdef MOZ_X11 + XEvent xevent; + + if (mIsX11Display) { + while (XPending(GDK_WINDOW_XDISPLAY(aEvent->window))) { + XEvent peeked; + XPeekEvent(GDK_WINDOW_XDISPLAY(aEvent->window), &peeked); + if (peeked.xany.window != gdk_x11_window_get_xid(aEvent->window) || + peeked.type != MotionNotify) + break; + + synthEvent = true; + XNextEvent(GDK_WINDOW_XDISPLAY(aEvent->window), &xevent); + } + } +#endif /* MOZ_X11 */ + + WidgetMouseEvent event(true, eMouseMove, this, WidgetMouseEvent::eReal); + + gdouble pressure = 0; + gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure); + // Sometime gdk generate 0 pressure value between normal values + // We have to ignore that and use last valid value + if (pressure) mLastMotionPressure = pressure; + event.mPressure = mLastMotionPressure; + + guint modifierState; + if (synthEvent) { +#ifdef MOZ_X11 + event.mRefPoint.x = nscoord(xevent.xmotion.x); + event.mRefPoint.y = nscoord(xevent.xmotion.y); + + modifierState = xevent.xmotion.state; + + event.AssignEventTime(GetWidgetEventTime(xevent.xmotion.time)); +#else + event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + + modifierState = aEvent->state; + + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); +#endif /* MOZ_X11 */ + } else { + event.mRefPoint = GetRefPoint(this, aEvent); + + modifierState = aEvent->state; + + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); + } + + KeymapWrapper::InitInputEvent(event, modifierState); + + DispatchInputEvent(&event); +} + +// If the automatic pointer grab on ButtonPress has deactivated before +// ButtonRelease, and the mouse button is released while the pointer is not +// over any a Gecko window, then the ButtonRelease event will not be received. +// (A similar situation exists when the pointer is grabbed with owner_events +// True as the ButtonRelease may be received on a foreign [plugin] window). +// Use this method to check for released buttons when the pointer returns to a +// Gecko window. +void nsWindow::DispatchMissedButtonReleases(GdkEventCrossing* aGdkEvent) { + guint changed = aGdkEvent->state ^ gButtonState; + // Only consider button releases. + // (Ignore button presses that occurred outside Gecko.) + guint released = changed & gButtonState; + gButtonState = aGdkEvent->state; + + // Loop over each button, excluding mouse wheel buttons 4 and 5 for which + // GDK ignores releases. + for (guint buttonMask = GDK_BUTTON1_MASK; buttonMask <= GDK_BUTTON3_MASK; + buttonMask <<= 1) { + if (released & buttonMask) { + int16_t buttonType; + switch (buttonMask) { + case GDK_BUTTON1_MASK: + buttonType = MouseButton::ePrimary; + break; + case GDK_BUTTON2_MASK: + buttonType = MouseButton::eMiddle; + break; + default: + NS_ASSERTION(buttonMask == GDK_BUTTON3_MASK, + "Unexpected button mask"); + buttonType = MouseButton::eSecondary; + } + + LOG(("Synthesized button %u release on %p\n", guint(buttonType + 1), + (void*)this)); + + // Dispatch a synthesized button up event to tell Gecko about the + // change in state. This event is marked as synthesized so that + // it is not dispatched as a DOM event, because we don't know the + // position, widget, modifiers, or time/order. + WidgetMouseEvent synthEvent(true, eMouseUp, this, + WidgetMouseEvent::eSynthesized); + synthEvent.mButton = buttonType; + DispatchInputEvent(&synthEvent); + } + } +} + +void nsWindow::InitButtonEvent(WidgetMouseEvent& aEvent, + GdkEventButton* aGdkEvent) { + aEvent.mRefPoint = GetRefPoint(this, aGdkEvent); + + guint modifierState = aGdkEvent->state; + // aEvent's state includes the button state from immediately before this + // event. If aEvent is a mousedown or mouseup event, we need to update + // the button state. + guint buttonMask = 0; + switch (aGdkEvent->button) { + case 1: + buttonMask = GDK_BUTTON1_MASK; + break; + case 2: + buttonMask = GDK_BUTTON2_MASK; + break; + case 3: + buttonMask = GDK_BUTTON3_MASK; + break; + } + if (aGdkEvent->type == GDK_BUTTON_RELEASE) { + modifierState &= ~buttonMask; + } else { + modifierState |= buttonMask; + } + + KeymapWrapper::InitInputEvent(aEvent, modifierState); + + aEvent.AssignEventTime(GetWidgetEventTime(aGdkEvent->time)); + + switch (aGdkEvent->type) { + case GDK_2BUTTON_PRESS: + aEvent.mClickCount = 2; + break; + case GDK_3BUTTON_PRESS: + aEvent.mClickCount = 3; + break; + // default is one click + default: + aEvent.mClickCount = 1; + } +} + +static guint ButtonMaskFromGDKButton(guint button) { + return GDK_BUTTON1_MASK << (button - 1); +} + +void nsWindow::DispatchContextMenuEventFromMouseEvent(uint16_t domButton, + GdkEventButton* aEvent) { + if (domButton == MouseButton::eSecondary && MOZ_LIKELY(!mIsDestroyed)) { + WidgetMouseEvent contextMenuEvent(true, eContextMenu, this, + WidgetMouseEvent::eReal); + InitButtonEvent(contextMenuEvent, aEvent); + contextMenuEvent.mPressure = mLastMotionPressure; + DispatchInputEvent(&contextMenuEvent); + } +} + +void nsWindow::OnButtonPressEvent(GdkEventButton* aEvent) { + LOG(("Button %u press on %p\n", aEvent->button, (void*)this)); + + // If you double click in GDK, it will actually generate a second + // GDK_BUTTON_PRESS before sending the GDK_2BUTTON_PRESS, and this is + // different than the DOM spec. GDK puts this in the queue + // programatically, so it's safe to assume that if there's a + // double click in the queue, it was generated so we can just drop + // this click. + GdkEvent* peekedEvent = gdk_event_peek(); + if (peekedEvent) { + GdkEventType type = peekedEvent->any.type; + gdk_event_free(peekedEvent); + if (type == GDK_2BUTTON_PRESS || type == GDK_3BUTTON_PRESS) return; + } + + nsWindow* containerWindow = GetContainerWindow(); + if (!gFocusWindow && containerWindow) { + containerWindow->DispatchActivateEvent(); + } + + // check to see if we should rollup + if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) return; + + gdouble pressure = 0; + gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure); + mLastMotionPressure = pressure; + + uint16_t domButton; + switch (aEvent->button) { + case 1: + domButton = MouseButton::ePrimary; + break; + case 2: + domButton = MouseButton::eMiddle; + break; + case 3: + domButton = MouseButton::eSecondary; + break; + // These are mapped to horizontal scroll + case 6: + case 7: + NS_WARNING("We're not supporting legacy horizontal scroll event"); + return; + // Map buttons 8-9 to back/forward + case 8: + if (!Preferences::GetBool("mousebutton.4th.enabled", true)) { + return; + } + DispatchCommandEvent(nsGkAtoms::Back); + return; + case 9: + if (!Preferences::GetBool("mousebutton.5th.enabled", true)) { + return; + } + DispatchCommandEvent(nsGkAtoms::Forward); + return; + default: + return; + } + + gButtonState |= ButtonMaskFromGDKButton(aEvent->button); + + WidgetMouseEvent event(true, eMouseDown, this, WidgetMouseEvent::eReal); + event.mButton = domButton; + InitButtonEvent(event, aEvent); + event.mPressure = mLastMotionPressure; + + nsEventStatus eventStatus = DispatchInputEvent(&event); + + LayoutDeviceIntPoint refPoint = + GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + if (mDraggableRegion.Contains(refPoint.x, refPoint.y) && + domButton == MouseButton::ePrimary && + eventStatus != nsEventStatus_eConsumeNoDefault) { + mWindowShouldStartDragging = true; + } + + // right menu click on linux should also pop up a context menu + if (!StaticPrefs::ui_context_menus_after_mouseup()) { + DispatchContextMenuEventFromMouseEvent(domButton, aEvent); + } +} + +void nsWindow::OnButtonReleaseEvent(GdkEventButton* aEvent) { + LOG(("Button %u release on %p\n", aEvent->button, (void*)this)); + + if (mWindowShouldStartDragging) { + mWindowShouldStartDragging = false; + } + + uint16_t domButton; + switch (aEvent->button) { + case 1: + domButton = MouseButton::ePrimary; + break; + case 2: + domButton = MouseButton::eMiddle; + break; + case 3: + domButton = MouseButton::eSecondary; + break; + default: + return; + } + + gButtonState &= ~ButtonMaskFromGDKButton(aEvent->button); + + WidgetMouseEvent event(true, eMouseUp, this, WidgetMouseEvent::eReal); + event.mButton = domButton; + InitButtonEvent(event, aEvent); + gdouble pressure = 0; + gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure); + event.mPressure = pressure ? pressure : mLastMotionPressure; + + // The mRefPoint is manipulated in DispatchInputEvent, we're saving it + // to use it for the doubleclick position check. + LayoutDeviceIntPoint pos = event.mRefPoint; + + nsEventStatus eventStatus = DispatchInputEvent(&event); + + bool defaultPrevented = (eventStatus == nsEventStatus_eConsumeNoDefault); + // Check if mouse position in titlebar and doubleclick happened to + // trigger restore/maximize. + if (!defaultPrevented && mDrawInTitlebar && + event.mButton == MouseButton::ePrimary && event.mClickCount == 2 && + mDraggableRegion.Contains(pos.x, pos.y)) { + if (mSizeState == nsSizeMode_Maximized) { + SetSizeMode(nsSizeMode_Normal); + } else { + SetSizeMode(nsSizeMode_Maximized); + } + } + mLastMotionPressure = pressure; + + // right menu click on linux should also pop up a context menu + if (StaticPrefs::ui_context_menus_after_mouseup()) { + DispatchContextMenuEventFromMouseEvent(domButton, aEvent); + } + + // Open window manager menu on PIP window to allow user + // to place it on top / all workspaces. + if (mIsPIPWindow && aEvent->button == 3) { + static auto sGdkWindowShowWindowMenu = + (gboolean(*)(GdkWindow * window, GdkEvent*)) + dlsym(RTLD_DEFAULT, "gdk_window_show_window_menu"); + if (sGdkWindowShowWindowMenu) { + sGdkWindowShowWindowMenu(mGdkWindow, (GdkEvent*)aEvent); + } + } +} + +void nsWindow::OnContainerFocusInEvent(GdkEventFocus* aEvent) { + LOGFOCUS(("OnContainerFocusInEvent [%p]\n", (void*)this)); + + // Unset the urgency hint, if possible + GtkWidget* top_window = GetToplevelWidget(); + if (top_window && (gtk_widget_get_visible(top_window))) + SetUrgencyHint(top_window, false); + + // Return if being called within SetFocus because the focus manager + // already knows that the window is active. + if (gBlockActivateEvent) { + LOGFOCUS(("activated notification is blocked [%p]\n", (void*)this)); + return; + } + + // If keyboard input will be accepted, the focus manager will call + // SetFocus to set the correct window. + gFocusWindow = nullptr; + + DispatchActivateEvent(); + + if (!gFocusWindow) { + // We don't really have a window for dispatching key events, but + // setting a non-nullptr value here prevents OnButtonPressEvent() from + // dispatching an activation notification if the widget is already + // active. + gFocusWindow = this; + } + + LOGFOCUS(("Events sent from focus in event [%p]\n", (void*)this)); +} + +void nsWindow::OnContainerFocusOutEvent(GdkEventFocus* aEvent) { + LOGFOCUS(("OnContainerFocusOutEvent [%p]\n", (void*)this)); + + if (mWindowType == eWindowType_toplevel || + mWindowType == eWindowType_dialog) { + nsCOMPtr<nsIDragService> dragService = do_GetService(kCDragServiceCID); + nsCOMPtr<nsIDragSession> dragSession; + dragService->GetCurrentSession(getter_AddRefs(dragSession)); + + // Rollup popups when a window is focused out unless a drag is occurring. + // This check is because drags grab the keyboard and cause a focus out on + // versions of GTK before 2.18. + bool shouldRollup = !dragSession; + if (!shouldRollup) { + // we also roll up when a drag is from a different application + nsCOMPtr<nsINode> sourceNode; + dragSession->GetSourceNode(getter_AddRefs(sourceNode)); + shouldRollup = (sourceNode == nullptr); + } + + if (shouldRollup) { + CheckForRollup(0, 0, false, true); + } + } + + if (gFocusWindow) { + RefPtr<nsWindow> kungFuDeathGrip = gFocusWindow; + if (gFocusWindow->mIMContext) { + gFocusWindow->mIMContext->OnBlurWindow(gFocusWindow); + } + gFocusWindow = nullptr; + } + + DispatchDeactivateEvent(); + + if (IsChromeWindowTitlebar()) { + // DispatchDeactivateEvent() ultimately results in a call to + // BrowsingContext::SetIsActiveBrowserWindow(), which resets + // the state. We call UpdateMozWindowActive() to keep it in + // sync with GDK_WINDOW_STATE_FOCUSED. + UpdateMozWindowActive(); + } + + LOGFOCUS(("Done with container focus out [%p]\n", (void*)this)); +} + +bool nsWindow::DispatchCommandEvent(nsAtom* aCommand) { + nsEventStatus status; + WidgetCommandEvent appCommandEvent(true, aCommand, this); + DispatchEvent(&appCommandEvent, status); + return TRUE; +} + +bool nsWindow::DispatchContentCommandEvent(EventMessage aMsg) { + nsEventStatus status; + WidgetContentCommandEvent event(true, aMsg, this); + DispatchEvent(&event, status); + return TRUE; +} + +WidgetEventTime nsWindow::GetWidgetEventTime(guint32 aEventTime) { + return WidgetEventTime(aEventTime, GetEventTimeStamp(aEventTime)); +} + +TimeStamp nsWindow::GetEventTimeStamp(guint32 aEventTime) { + if (MOZ_UNLIKELY(!mGdkWindow)) { + // nsWindow has been Destroy()ed. + return TimeStamp::Now(); + } + if (aEventTime == 0) { + // Some X11 and GDK events may be received with a time of 0 to indicate + // that they are synthetic events. Some input method editors do this. + // In this case too, just return the current timestamp. + return TimeStamp::Now(); + } + + TimeStamp eventTimeStamp; + + if (!mIsX11Display) { + // Wayland compositors use monotonic time to set timestamps. + int64_t timestampTime = g_get_monotonic_time() / 1000; + guint32 refTimeTruncated = guint32(timestampTime); + + timestampTime -= refTimeTruncated - aEventTime; + int64_t tick = + BaseTimeDurationPlatformUtils::TicksFromMilliseconds(timestampTime); + eventTimeStamp = TimeStamp::FromSystemTime(tick); + } else { + CurrentX11TimeGetter* getCurrentTime = GetCurrentTimeGetter(); + MOZ_ASSERT(getCurrentTime, + "Null current time getter despite having a window"); + eventTimeStamp = + TimeConverter().GetTimeStampFromSystemTime(aEventTime, *getCurrentTime); + } + return eventTimeStamp; +} + +mozilla::CurrentX11TimeGetter* nsWindow::GetCurrentTimeGetter() { + MOZ_ASSERT(mGdkWindow, "Expected mGdkWindow to be set"); + if (MOZ_UNLIKELY(!mCurrentTimeGetter)) { + mCurrentTimeGetter = MakeUnique<CurrentX11TimeGetter>(mGdkWindow); + } + return mCurrentTimeGetter.get(); +} + +gboolean nsWindow::OnKeyPressEvent(GdkEventKey* aEvent) { + LOGFOCUS(("OnKeyPressEvent [%p]\n", (void*)this)); + + RefPtr<nsWindow> self(this); + KeymapWrapper::HandleKeyPressEvent(self, aEvent); + return TRUE; +} + +gboolean nsWindow::OnKeyReleaseEvent(GdkEventKey* aEvent) { + LOGFOCUS(("OnKeyReleaseEvent [%p]\n", (void*)this)); + + RefPtr<nsWindow> self(this); + if (NS_WARN_IF(!KeymapWrapper::HandleKeyReleaseEvent(self, aEvent))) { + return FALSE; + } + return TRUE; +} + +void nsWindow::OnScrollEvent(GdkEventScroll* aEvent) { + // check to see if we should rollup + if (CheckForRollup(aEvent->x_root, aEvent->y_root, true, false)) return; + // check for duplicate legacy scroll event, see GNOME bug 726878 + if (aEvent->direction != GDK_SCROLL_SMOOTH && + mLastScrollEventTime == aEvent->time) { + LOG(("[%d] duplicate legacy scroll event %d\n", aEvent->time, + aEvent->direction)); + return; + } + WidgetWheelEvent wheelEvent(true, eWheel, this); + wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_LINE; + switch (aEvent->direction) { + case GDK_SCROLL_SMOOTH: { + // As of GTK 3.4, all directional scroll events are provided by + // the GDK_SCROLL_SMOOTH direction on XInput2 and Wayland devices. + mLastScrollEventTime = aEvent->time; + + // Special handling for touchpads to support flings + // (also known as kinetic/inertial/momentum scrolling) + GdkDevice* device = gdk_event_get_source_device((GdkEvent*)aEvent); + GdkInputSource source = gdk_device_get_source(device); + if (source == GDK_SOURCE_TOUCHSCREEN || source == GDK_SOURCE_TOUCHPAD) { + if (StaticPrefs::apz_gtk_kinetic_scroll_enabled() && + gtk_check_version(3, 20, 0) == nullptr) { + static auto sGdkEventIsScrollStopEvent = + (gboolean(*)(const GdkEvent*))dlsym( + RTLD_DEFAULT, "gdk_event_is_scroll_stop_event"); + + LOG(("[%d] pan smooth event dx=%f dy=%f inprogress=%d\n", + aEvent->time, aEvent->delta_x, aEvent->delta_y, mPanInProgress)); + PanGestureInput::PanGestureType eventType = + PanGestureInput::PANGESTURE_PAN; + if (sGdkEventIsScrollStopEvent((GdkEvent*)aEvent)) { + eventType = PanGestureInput::PANGESTURE_END; + mPanInProgress = false; + } else if (!mPanInProgress) { + eventType = PanGestureInput::PANGESTURE_START; + mPanInProgress = true; + } + + LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent); + PanGestureInput panEvent( + eventType, aEvent->time, GetEventTimeStamp(aEvent->time), + ScreenPoint(touchPoint.x, touchPoint.y), + ScreenPoint(aEvent->delta_x, aEvent->delta_y), + KeymapWrapper::ComputeKeyModifiers(aEvent->state)); + panEvent.mDeltaType = PanGestureInput::PANDELTA_PAGE; + panEvent.mSimulateMomentum = true; + + DispatchPanGestureInput(panEvent); + + return; + } + + // Older GTK doesn't support stop events, so we can't support fling + wheelEvent.mScrollType = WidgetWheelEvent::SCROLL_ASYNCHRONOUSELY; + } + + // TODO - use a more appropriate scrolling unit than lines. + // Multiply event deltas by 3 to emulate legacy behaviour. + wheelEvent.mDeltaX = aEvent->delta_x * 3; + wheelEvent.mDeltaY = aEvent->delta_y * 3; + wheelEvent.mIsNoLineOrPageDelta = true; + + break; + } + case GDK_SCROLL_UP: + wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = -3; + break; + case GDK_SCROLL_DOWN: + wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = 3; + break; + case GDK_SCROLL_LEFT: + wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = -1; + break; + case GDK_SCROLL_RIGHT: + wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = 1; + break; + } + + wheelEvent.mRefPoint = GetRefPoint(this, aEvent); + + KeymapWrapper::InitInputEvent(wheelEvent, aEvent->state); + + wheelEvent.AssignEventTime(GetWidgetEventTime(aEvent->time)); + + DispatchInputEvent(&wheelEvent); +} + +void nsWindow::OnWindowStateEvent(GtkWidget* aWidget, + GdkEventWindowState* aEvent) { + LOG( + ("nsWindow::OnWindowStateEvent [%p] for %p changed 0x%x new_window_state " + "0x%x\n", + (void*)this, aWidget, aEvent->changed_mask, aEvent->new_window_state)); + + if (IS_MOZ_CONTAINER(aWidget)) { + // This event is notifying the container widget of changes to the + // toplevel window. Just detect changes affecting whether windows are + // viewable. + // + // (A visibility notify event is sent to each window that becomes + // viewable when the toplevel is mapped, but we can't rely on that for + // setting mHasMappedToplevel because these toplevel window state + // events are asynchronous. The windows in the hierarchy now may not + // be the same windows as when the toplevel was mapped, so they may + // not get VisibilityNotify events.) + bool mapped = !(aEvent->new_window_state & + (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_WITHDRAWN)); + if (mHasMappedToplevel != mapped) { + SetHasMappedToplevel(mapped); + } + LOG(("\tquick return because IS_MOZ_CONTAINER(aWidget) is true\n")); + return; + } + // else the widget is a shell widget. + + // The block below is a bit evil. + // + // When a window is resized before it is shown, gtk_window_resize() delays + // resizes until the window is shown. If gtk_window_state_event() sees a + // GDK_WINDOW_STATE_MAXIMIZED change [1] before the window is shown, then + // gtk_window_compute_configure_request_size() ignores the values from the + // resize [2]. See bug 1449166 for an example of how this could happen. + // + // [1] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L7967 + // [2] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L9377 + // + // In order to provide a sensible size for the window when the user exits + // maximized state, we hide the GDK_WINDOW_STATE_MAXIMIZED change from + // gtk_window_state_event() so as to trick GTK into using the values from + // gtk_window_resize() in its configure request. + // + // We instead notify gtk_window_state_event() of the maximized state change + // once the window is shown. + // + // See https://gitlab.gnome.org/GNOME/gtk/issues/1044 + // + // This may be fixed in Gtk 3.24+ but some DE still have this issue + // (Bug 1624199) so let's remove it for Wayland only. + if (mIsX11Display) { + if (!mIsShown) { + aEvent->changed_mask = static_cast<GdkWindowState>( + aEvent->changed_mask & ~GDK_WINDOW_STATE_MAXIMIZED); + } else if (aEvent->changed_mask & GDK_WINDOW_STATE_WITHDRAWN && + aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) { + aEvent->changed_mask = static_cast<GdkWindowState>( + aEvent->changed_mask | GDK_WINDOW_STATE_MAXIMIZED); + } + } + + // This is a workaround for https://gitlab.gnome.org/GNOME/gtk/issues/1395 + // Gtk+ controls window active appearance by window-state-event signal. + if (IsChromeWindowTitlebar() && + (aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED)) { + // Emulate what Gtk+ does at gtk_window_state_event(). + // We can't check GTK_STATE_FLAG_BACKDROP directly as it's set by Gtk+ + // *after* this window-state-event handler. + mTitlebarBackdropState = + !(aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED); + + // keep IsActiveBrowserWindow in sync with GDK_WINDOW_STATE_FOCUSED + UpdateMozWindowActive(); + + ForceTitlebarRedraw(); + } + + // We don't care about anything but changes in the maximized/icon/fullscreen + // states but we need a workaround for bug in Wayland: + // https://gitlab.gnome.org/GNOME/gtk/issues/67 + // Under wayland the gtk_window_iconify implementation does NOT synthetize + // window_state_event where the GDK_WINDOW_STATE_ICONIFIED is set. + // During restore we won't get aEvent->changed_mask with + // the GDK_WINDOW_STATE_ICONIFIED so to detect that change we use the stored + // mSizeState and obtaining a focus. + bool waylandWasIconified = + (!mIsX11Display && aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED && + aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED && + mSizeState == nsSizeMode_Minimized); + if (!waylandWasIconified && + (aEvent->changed_mask & + (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED | + GDK_WINDOW_STATE_TILED | GDK_WINDOW_STATE_FULLSCREEN)) == 0) { + LOG(("\tearly return because no interesting bits changed\n")); + return; + } + + if (aEvent->new_window_state & GDK_WINDOW_STATE_ICONIFIED) { + LOG(("\tIconified\n")); + mSizeState = nsSizeMode_Minimized; +#ifdef ACCESSIBILITY + DispatchMinimizeEventAccessible(); +#endif // ACCESSIBILITY + } else if (aEvent->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) { + LOG(("\tFullscreen\n")); + mSizeState = nsSizeMode_Fullscreen; + } else if (aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) { + LOG(("\tMaximized\n")); + mSizeState = nsSizeMode_Maximized; +#ifdef ACCESSIBILITY + DispatchMaximizeEventAccessible(); +#endif // ACCESSIBILITY + } else { + LOG(("\tNormal\n")); + mSizeState = nsSizeMode_Normal; +#ifdef ACCESSIBILITY + DispatchRestoreEventAccessible(); +#endif // ACCESSIBILITY + } + + if (aEvent->new_window_state & GDK_WINDOW_STATE_TILED) { + LOG(("\tTiled\n")); + mIsTiled = true; + } else { + LOG(("\tNot tiled\n")); + mIsTiled = false; + } + + if (mWidgetListener) { + mWidgetListener->SizeModeChanged(mSizeState); + if (aEvent->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) { + mWidgetListener->FullscreenChanged(aEvent->new_window_state & + GDK_WINDOW_STATE_FULLSCREEN); + } + } + + if (mDrawInTitlebar && mTransparencyBitmapForTitlebar) { + if (mSizeState == nsSizeMode_Normal && !mIsTiled) { + UpdateTitlebarTransparencyBitmap(); + } else { + ClearTransparencyBitmap(); + } + } +} + +void nsWindow::ThemeChanged() { + // Everything could've changed. + NotifyThemeChanged(ThemeChangeKind::StyleAndLayout); + + if (!mGdkWindow || MOZ_UNLIKELY(mIsDestroyed)) return; + + // Dispatch theme change notification to all child windows + GList* children = gdk_window_peek_children(mGdkWindow); + while (children) { + GdkWindow* gdkWin = GDK_WINDOW(children->data); + + auto* win = (nsWindow*)g_object_get_data(G_OBJECT(gdkWin), "nsWindow"); + + if (win && win != this) { // guard against infinite recursion + RefPtr<nsWindow> kungFuDeathGrip = win; + win->ThemeChanged(); + } + + children = children->next; + } + + IMContextWrapper::OnThemeChanged(); +} + +void nsWindow::OnDPIChanged() { + if (mWidgetListener) { + if (PresShell* presShell = mWidgetListener->GetPresShell()) { + presShell->BackingScaleFactorChanged(); + // Update menu's font size etc. + // This affects style / layout because it affects system font sizes. + presShell->ThemeChanged(ThemeChangeKind::StyleAndLayout); + } + mWidgetListener->UIResolutionChanged(); + } +} + +void nsWindow::OnCheckResize() { mPendingConfigures++; } + +void nsWindow::OnCompositedChanged() { + // Update CSD after the change in alpha visibility. This only affects + // system metrics, not other theme shenanigans. + NotifyThemeChanged(ThemeChangeKind::MediaQueriesOnly); + mCompositedScreen = gdk_screen_is_composited(gdk_screen_get_default()); +} + +void nsWindow::OnScaleChanged(GtkAllocation* aAllocation) { + LOG(("nsWindow::OnScaleChanged [%p] %d,%d -> %d x %d\n", (void*)this, + aAllocation->x, aAllocation->y, aAllocation->width, + aAllocation->height)); + + // Force scale factor recalculation + mWindowScaleFactorChanged = true; + + // This eventually propagate new scale to the PuppetWidgets + OnDPIChanged(); + + // configure_event is already fired before scale-factor signal, + // but size-allocate isn't fired by changing scale + OnSizeAllocate(aAllocation); + + // Client offset are updated by _NET_FRAME_EXTENTS on X11 when system titlebar + // is enabled. In ither cases (Wayland or system titlebar is off on X11) + // we don't get _NET_FRAME_EXTENTS X11 property notification so we derive + // it from mContainer position. + if (mCSDSupportLevel == CSD_SUPPORT_CLIENT) { + if (!mIsX11Display || (mIsX11Display && mDrawInTitlebar)) { + UpdateClientOffsetFromCSDWindow(); + } + } + +#ifdef MOZ_WAYLAND + // We need to update scale when scale of egl window is changed. + if (mContainer && moz_container_wayland_has_egl_window(mContainer)) { + moz_container_wayland_set_scale_factor(mContainer); + } +#endif +} + +void nsWindow::DispatchDragEvent(EventMessage aMsg, + const LayoutDeviceIntPoint& aRefPoint, + guint aTime) { + WidgetDragEvent event(true, aMsg, this); + + InitDragEvent(event); + + event.mRefPoint = aRefPoint; + event.AssignEventTime(GetWidgetEventTime(aTime)); + + DispatchInputEvent(&event); +} + +void nsWindow::OnDragDataReceivedEvent(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, + GtkSelectionData* aSelectionData, + guint aInfo, guint aTime, + gpointer aData) { + LOGDRAG(("nsWindow::OnDragDataReceived(%p)\n", (void*)this)); + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + dragService->TargetDataReceived(aWidget, aDragContext, aX, aY, aSelectionData, + aInfo, aTime); +} + +nsWindow* nsWindow::GetTransientForWindowIfPopup() { + if (mWindowType != eWindowType_popup) { + return nullptr; + } + GtkWindow* toplevel = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + if (toplevel) { + return get_window_for_gtk_widget(GTK_WIDGET(toplevel)); + } + return nullptr; +} + +bool nsWindow::IsHandlingTouchSequence(GdkEventSequence* aSequence) { + return mHandleTouchEvent && mTouches.Contains(aSequence); +} + +gboolean nsWindow::OnTouchpadPinchEvent(GdkEventTouchpadPinch* aEvent) { + if (StaticPrefs::apz_gtk_touchpad_pinch_enabled()) { + PinchGestureInput::PinchGestureType pinchGestureType = + PinchGestureInput::PINCHGESTURE_SCALE; + ScreenCoord CurrentSpan; + ScreenCoord PreviousSpan; + + switch (aEvent->phase) { + case GDK_TOUCHPAD_GESTURE_PHASE_BEGIN: + pinchGestureType = PinchGestureInput::PINCHGESTURE_START; + CurrentSpan = aEvent->scale; + + // Assign PreviousSpan --> 0.999 to make mDeltaY field of the + // WidgetWheelEvent that this PinchGestureInput event will be converted + // to not equal Zero as our discussion because we observed that the + // scale of the PHASE_BEGIN event is 1. + PreviousSpan = 0.999; + mLastPinchEventSpan = aEvent->scale; + break; + + case GDK_TOUCHPAD_GESTURE_PHASE_UPDATE: + pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE; + if (aEvent->scale == mLastPinchEventSpan) { + return FALSE; + } + CurrentSpan = aEvent->scale; + PreviousSpan = mLastPinchEventSpan; + mLastPinchEventSpan = aEvent->scale; + break; + + case GDK_TOUCHPAD_GESTURE_PHASE_END: + pinchGestureType = PinchGestureInput::PINCHGESTURE_END; + CurrentSpan = aEvent->scale; + PreviousSpan = mLastPinchEventSpan; + break; + + default: + return FALSE; + } + + LayoutDeviceIntPoint touchpadPoint = GetRefPoint(this, aEvent); + PinchGestureInput event( + pinchGestureType, PinchGestureInput::TRACKPAD, aEvent->time, + GetEventTimeStamp(aEvent->time), ExternalPoint(0, 0), + ScreenPoint(touchpadPoint.x, touchpadPoint.y), CurrentSpan, + PreviousSpan, KeymapWrapper::ComputeKeyModifiers(aEvent->state)); + + DispatchPinchGestureInput(event); + } + return TRUE; +} + +gboolean nsWindow::OnTouchEvent(GdkEventTouch* aEvent) { + if (!mHandleTouchEvent) { + // If a popup window was spawned (e.g. as the result of a long-press) + // and touch events got diverted to that window within a touch sequence, + // ensure the touch event gets sent to the original window instead. We + // keep the checks here very conservative so that we only redirect + // events in this specific scenario. + nsWindow* targetWindow = GetTransientForWindowIfPopup(); + if (targetWindow && + targetWindow->IsHandlingTouchSequence(aEvent->sequence)) { + return targetWindow->OnTouchEvent(aEvent); + } + + return FALSE; + } + + EventMessage msg; + switch (aEvent->type) { + case GDK_TOUCH_BEGIN: + msg = eTouchStart; + break; + case GDK_TOUCH_UPDATE: + msg = eTouchMove; + break; + case GDK_TOUCH_END: + msg = eTouchEnd; + break; + case GDK_TOUCH_CANCEL: + msg = eTouchCancel; + break; + default: + return FALSE; + } + + LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent); + + int32_t id; + RefPtr<dom::Touch> touch; + if (mTouches.Remove(aEvent->sequence, getter_AddRefs(touch))) { + id = touch->mIdentifier; + } else { + id = ++gLastTouchID & 0x7FFFFFFF; + } + + touch = + new dom::Touch(id, touchPoint, LayoutDeviceIntPoint(1, 1), 0.0f, 0.0f); + + WidgetTouchEvent event(true, msg, this); + KeymapWrapper::InitInputEvent(event, aEvent->state); + event.mTime = aEvent->time; + + if (aEvent->type == GDK_TOUCH_BEGIN || aEvent->type == GDK_TOUCH_UPDATE) { + mTouches.Put(aEvent->sequence, std::move(touch)); + // add all touch points to event object + for (auto iter = mTouches.Iter(); !iter.Done(); iter.Next()) { + event.mTouches.AppendElement(new dom::Touch(*iter.UserData())); + } + } else if (aEvent->type == GDK_TOUCH_END || + aEvent->type == GDK_TOUCH_CANCEL) { + *event.mTouches.AppendElement() = std::move(touch); + } + + DispatchInputEvent(&event); + return TRUE; +} + +// Return true if toplevel window is transparent. +// It's transparent when we're running on composited screens +// and we can draw main window without system titlebar. +bool nsWindow::IsToplevelWindowTransparent() { + static bool transparencyConfigured = false; + + if (!transparencyConfigured) { + if (gdk_screen_is_composited(gdk_screen_get_default())) { + // Some Gtk+ themes use non-rectangular toplevel windows. To fully + // support such themes we need to make toplevel window transparent + // with ARGB visual. + // It may cause performanance issue so make it configurable + // and enable it by default for selected window managers. + if (Preferences::HasUserValue("mozilla.widget.use-argb-visuals")) { + // argb visual is explicitly required so use it + sTransparentMainWindow = + Preferences::GetBool("mozilla.widget.use-argb-visuals"); + } else { + // Enable transparent toplevel window if we can draw main window + // without system titlebar as Gtk+ themes use titlebar round corners. + sTransparentMainWindow = GetSystemCSDSupportLevel() != CSD_SUPPORT_NONE; + } + } + transparencyConfigured = true; + } + + return sTransparentMainWindow; +} + +static GdkWindow* CreateGdkWindow(GdkWindow* parent, GtkWidget* widget) { + GdkWindowAttr attributes; + gint attributes_mask = GDK_WA_VISUAL; + + attributes.event_mask = kEvents; + + attributes.width = 1; + attributes.height = 1; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual(widget); + attributes.window_type = GDK_WINDOW_CHILD; + + GdkWindow* window = gdk_window_new(parent, &attributes, attributes_mask); + gdk_window_set_user_data(window, widget); + + return window; +} + +// Configure GL visual on X11. We add alpha silently +// if we use WebRender to workaround NVIDIA specific Bug 1663273. +bool nsWindow::ConfigureX11GLVisual(bool aUseAlpha) { + if (!mIsX11Display) { + return false; + } + + // If using WebRender on X11, we need to select a visual with a depth + // buffer, as well as an alpha channel if transparency is requested. This + // must be done before the widget is realized. + bool useWebRender = gfx::gfxVars::UseWebRender(); + auto* screen = gtk_widget_get_screen(mShell); + int visualId = 0; + bool haveVisual; + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1663003 + // We need to use GLX to get visual even on EGL until + // EGL can provide compositable visual: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/149 + if ((true /* !gfx::gfxVars::UseEGL() */)) { + auto* display = GDK_DISPLAY_XDISPLAY(gtk_widget_get_display(mShell)); + int screenNumber = GDK_SCREEN_XNUMBER(screen); + haveVisual = GLContextGLX::FindVisual(display, screenNumber, useWebRender, + aUseAlpha || useWebRender, &visualId); + } else { + haveVisual = GLContextEGL::FindVisual(useWebRender, + aUseAlpha || useWebRender, &visualId); + } + + GdkVisual* gdkVisual = nullptr; + if (haveVisual) { + // If we're using CSD, rendering will go through mContainer, but + // it will inherit this visual as it is a child of mShell. + gdkVisual = gdk_x11_screen_lookup_visual(screen, visualId); + } + if (!gdkVisual) { + NS_WARNING("We're missing X11 Visual!"); + if (aUseAlpha || useWebRender) { + // We try to use a fallback alpha visual + GdkScreen* screen = gtk_widget_get_screen(mShell); + gdkVisual = gdk_screen_get_rgba_visual(screen); + } + } + if (gdkVisual) { + // TODO: We use alpha visual even on non-compositing screens (Bug 1479135). + gtk_widget_set_visual(mShell, gdkVisual); + mHasAlphaVisual = aUseAlpha; + } + + return true; +} + +nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData) { +#ifdef MOZ_LOGGING + if (this->GetFrame() && this->GetFrame()->GetContent()) { + nsCString nodeName = + NS_ConvertUTF16toUTF8(this->GetFrame()->GetContent()->NodeName()); + LOG(("nsWindow::Create: creating [%p]: nodename %s\n", this, + nodeName.get())); + } +#endif + // only set the base parent if we're going to be a dialog or a + // toplevel + nsIWidget* baseParent = + aInitData && (aInitData->mWindowType == eWindowType_dialog || + aInitData->mWindowType == eWindowType_toplevel || + aInitData->mWindowType == eWindowType_invisible) + ? nullptr + : aParent; + +#ifdef ACCESSIBILITY + // Send a DBus message to check whether a11y is enabled + a11y::PreInit(); +#endif + + // Ensure that the toolkit is created. + nsGTKToolkit::GetToolkit(); + + // initialize all the common bits of this class + BaseCreate(baseParent, aInitData); + + // Do we need to listen for resizes? + bool listenForResizes = false; + ; + if (aNativeParent || (aInitData && aInitData->mListenForResizes)) + listenForResizes = true; + + // and do our common creation + CommonCreate(aParent, listenForResizes); + + // save our bounds + mBounds = aRect; + LOG((" mBounds: x:%d y:%d w:%d h:%d\n", mBounds.x, mBounds.y, mBounds.width, + mBounds.height)); + + mPreferredPopupRectFlushed = false; + + ConstrainSize(&mBounds.width, &mBounds.height); + + GtkWidget* eventWidget = nullptr; + bool popupNeedsAlphaVisual = (mWindowType == eWindowType_popup && + (aInitData && aInitData->mSupportTranslucency)); + + // Figure out our parent window - only used for eWindowType_child + GtkWidget* parentMozContainer = nullptr; + GtkContainer* parentGtkContainer = nullptr; + GdkWindow* parentGdkWindow = nullptr; + nsWindow* parentnsWindow = nullptr; + + if (aParent) { + parentnsWindow = static_cast<nsWindow*>(aParent); + parentGdkWindow = parentnsWindow->mGdkWindow; + } else if (aNativeParent && GDK_IS_WINDOW(aNativeParent)) { + parentGdkWindow = GDK_WINDOW(aNativeParent); + parentnsWindow = get_window_for_gdk_window(parentGdkWindow); + if (!parentnsWindow) return NS_ERROR_FAILURE; + + } else if (aNativeParent && GTK_IS_CONTAINER(aNativeParent)) { + parentGtkContainer = GTK_CONTAINER(aNativeParent); + } + + if (parentGdkWindow) { + // get the widget for the window - it should be a moz container + parentMozContainer = parentnsWindow->GetMozContainerWidget(); + if (!parentMozContainer) return NS_ERROR_FAILURE; + } + // ^^ only used for eWindowType_child + + if (!mIsX11Display) { + if (mWindowType == eWindowType_child) { + // eWindowType_child is not supported on Wayland. Just switch to toplevel + // as a workaround. + mWindowType = eWindowType_toplevel; + } else if (mWindowType == eWindowType_popup && !aNativeParent && !aParent) { + // Workaround for Wayland where the popup windows always need to have + // parent window. For example webrtc ui is a popup window without parent. + mWindowType = eWindowType_toplevel; + } + } + + mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop; + mIsPIPWindow = aInitData && aInitData->mPIPWindow; + + // ok, create our windows + switch (mWindowType) { + case eWindowType_dialog: + case eWindowType_popup: + case eWindowType_toplevel: + case eWindowType_invisible: { + mIsTopLevel = true; + + // Popups that are not noautohide are only temporary. The are used + // for menus and the like and disappear when another window is used. + // For most popups, use the standard GtkWindowType GTK_WINDOW_POPUP, + // which will use a Window with the override-redirect attribute + // (for temporary windows). + // For long-lived windows, their stacking order is managed by the + // window manager, as indicated by GTK_WINDOW_TOPLEVEL. + // For Wayland we have to always use GTK_WINDOW_POPUP to control + // popup window position. + GtkWindowType type = GTK_WINDOW_TOPLEVEL; + if (mWindowType == eWindowType_popup) { + type = (mIsX11Display && aInitData->mNoAutoHide) ? GTK_WINDOW_TOPLEVEL + : GTK_WINDOW_POPUP; + } + mShell = gtk_window_new(type); + + // Ensure gfxPlatform is initialized, since that is what initializes + // gfxVars, used below. + Unused << gfxPlatform::GetPlatform(); + + if (mWindowType == eWindowType_toplevel || + mWindowType == eWindowType_dialog) { + bool isPopup = mIsPIPWindow || mWindowType == eWindowType_dialog; + mCSDSupportLevel = GetSystemCSDSupportLevel(isPopup); + } + + // Don't use transparency for PictureInPicture windows. + bool toplevelNeedsAlphaVisual = false; + if (mWindowType == eWindowType_toplevel && !mIsPIPWindow) { + toplevelNeedsAlphaVisual = IsToplevelWindowTransparent(); + } + + bool isGLVisualSet = false; + bool isAccelerated = ComputeShouldAccelerate(); +#ifdef MOZ_X11 + if (isAccelerated) { + isGLVisualSet = ConfigureX11GLVisual(popupNeedsAlphaVisual || + toplevelNeedsAlphaVisual); + } +#endif + if (!isGLVisualSet && + (popupNeedsAlphaVisual || toplevelNeedsAlphaVisual)) { + // We're running on composited screen so we can use alpha visual + // for both toplevel and popups. + if (mCompositedScreen) { + GdkVisual* visual = + gdk_screen_get_rgba_visual(gtk_widget_get_screen(mShell)); + if (visual) { + gtk_widget_set_visual(mShell, visual); + mHasAlphaVisual = true; + } + } + } + + // Use X shape mask to draw round corners of Firefox titlebar. + // We don't use shape masks any more as we switched to ARGB visual + // by default and non-compositing screens use solid-csd decorations + // without round corners. + // Leave the shape mask code here as it can be used to draw round + // corners on EGL (https://gitlab.freedesktop.org/mesa/mesa/-/issues/149) + // or when custom titlebar theme is used. + mTransparencyBitmapForTitlebar = TitlebarUseShapeMask(); + + // We have a toplevel window with transparency. + // Calls to UpdateTitlebarTransparencyBitmap() from OnExposeEvent() + // occur before SetTransparencyMode() receives eTransparencyTransparent + // from layout, so set mIsTransparent here. + if (mWindowType == eWindowType_toplevel && + (mHasAlphaVisual || mTransparencyBitmapForTitlebar)) { + mIsTransparent = true; + } + + // We only move a general managed toplevel window if someone has + // actually placed the window somewhere. If no placement has taken + // place, we just let the window manager Do The Right Thing. + NativeResize(); + + if (mWindowType == eWindowType_dialog) { + mGtkWindowRoleName = "Dialog"; + + SetDefaultIcon(); + gtk_window_set_type_hint(GTK_WINDOW(mShell), + GDK_WINDOW_TYPE_HINT_DIALOG); + + if (parentnsWindow) { + gtk_window_set_transient_for( + GTK_WINDOW(mShell), GTK_WINDOW(parentnsWindow->GetGtkWidget())); + LOG(( + "nsWindow::Create(): dialog [%p], parent window %p [GdkWindow]\n", + this, aNativeParent)); + } + + } else if (mWindowType == eWindowType_popup) { + mGtkWindowRoleName = "Popup"; + + if (aInitData->mNoAutoHide) { + // ... but the window manager does not decorate this window, + // nor provide a separate taskbar icon. + if (mBorderStyle == eBorderStyle_default) { + gtk_window_set_decorated(GTK_WINDOW(mShell), FALSE); + } else { + bool decorate = mBorderStyle & eBorderStyle_title; + gtk_window_set_decorated(GTK_WINDOW(mShell), decorate); + if (decorate) { + gtk_window_set_deletable(GTK_WINDOW(mShell), + mBorderStyle & eBorderStyle_close); + } + } + gtk_window_set_skip_taskbar_hint(GTK_WINDOW(mShell), TRUE); + // Element focus is managed by the parent window so the + // WM_HINTS input field is set to False to tell the window + // manager not to set input focus to this window ... + gtk_window_set_accept_focus(GTK_WINDOW(mShell), FALSE); +#ifdef MOZ_X11 + // ... but when the window manager offers focus through + // WM_TAKE_FOCUS, focus is requested on the parent window. + if (mIsX11Display) { + gtk_widget_realize(mShell); + gdk_window_add_filter(gtk_widget_get_window(mShell), + popup_take_focus_filter, nullptr); + } +#endif + } + + GdkWindowTypeHint gtkTypeHint; + if (aInitData->mIsDragPopup) { + gtkTypeHint = GDK_WINDOW_TYPE_HINT_DND; + mIsDragPopup = true; + } else { + switch (aInitData->mPopupHint) { + case ePopupTypeMenu: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU; + break; + case ePopupTypeTooltip: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP; + break; + default: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY; + break; + } + } + gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint); + if (parentnsWindow) { + LOG(("nsWindow::Create() [%p]: parent window for popup: %p\n", this, + parentnsWindow)); + gtk_window_set_transient_for( + GTK_WINDOW(mShell), GTK_WINDOW(parentnsWindow->GetGtkWidget())); + } + + // We need realized mShell at NativeMove(). + gtk_widget_realize(mShell); + + // With popup windows, we want to control their position, so don't + // wait for the window manager to place them (which wouldn't + // happen with override-redirect windows anyway). + NativeMove(); + } else { // must be eWindowType_toplevel + mGtkWindowRoleName = "Toplevel"; + SetDefaultIcon(); + + if (mIsPIPWindow) { + gtk_window_set_type_hint(GTK_WINDOW(mShell), + GDK_WINDOW_TYPE_HINT_UTILITY); + } + + // each toplevel window gets its own window group + GtkWindowGroup* group = gtk_window_group_new(); + gtk_window_group_add_window(group, GTK_WINDOW(mShell)); + g_object_unref(group); + } + + if (mAlwaysOnTop) { + gtk_window_set_keep_above(GTK_WINDOW(mShell), TRUE); + } + + // Create a container to hold child windows and child GtkWidgets. + GtkWidget* container = moz_container_new(); + mContainer = MOZ_CONTAINER(container); +#ifdef MOZ_WAYLAND + if (!mIsX11Display && isAccelerated) { + mCompositorInitiallyPaused = true; + RefPtr<nsWindow> self(this); + moz_container_wayland_add_initial_draw_callback( + mContainer, [self]() -> void { + self->mNeedsCompositorResume = true; + self->MaybeResumeCompositor(); + }); + } +#endif + + // "csd" style is set when widget is realized so we need to call + // it explicitly now. + gtk_widget_realize(mShell); + + /* There are several cases here: + * + * 1) We're running on Gtk+ without client side decorations. + * Content is rendered to mShell window and we listen + * to the Gtk+ events on mShell + * 2) We're running on Gtk+ and client side decorations + * are drawn by Gtk+ to mShell. Content is rendered to mContainer + * and we listen to the Gtk+ events on mContainer. + * 3) We're running on Wayland. All gecko content is rendered + * to mContainer and we listen to the Gtk+ events on mContainer. + */ + GtkStyleContext* style = gtk_widget_get_style_context(mShell); + mDrawToContainer = !mIsX11Display || + (mCSDSupportLevel == CSD_SUPPORT_CLIENT) || + gtk_style_context_has_class(style, "csd"); + eventWidget = (mDrawToContainer) ? container : mShell; + + // Prevent GtkWindow from painting a background to avoid flickering. + gtk_widget_set_app_paintable(eventWidget, TRUE); + + gtk_widget_add_events(eventWidget, kEvents); + if (mDrawToContainer) { + gtk_widget_add_events(mShell, GDK_PROPERTY_CHANGE_MASK); + gtk_widget_set_app_paintable(mShell, TRUE); + } + if (mTransparencyBitmapForTitlebar) { + moz_container_force_default_visual(mContainer); + } + + // If we draw to mContainer window then configure it now because + // gtk_container_add() realizes the child widget. + gtk_widget_set_has_window(container, mDrawToContainer); + + gtk_container_add(GTK_CONTAINER(mShell), container); + + // alwaysontop windows are generally used for peripheral indicators, + // so we don't focus them by default. + if (mAlwaysOnTop) { + gtk_window_set_focus_on_map(GTK_WINDOW(mShell), FALSE); + } + + gtk_widget_realize(container); + + // make sure this is the focus widget in the container + gtk_widget_show(container); + + if (!mAlwaysOnTop) { + gtk_widget_grab_focus(container); + } + + // the drawing window + mGdkWindow = gtk_widget_get_window(eventWidget); + + if (mWindowType == eWindowType_popup) { + // gdk does not automatically set the cursor for "temporary" + // windows, which are what gtk uses for popups. + + mCursor = eCursor_wait; // force SetCursor to actually set the + // cursor, even though our internal state + // indicates that we already have the + // standard cursor. + SetCursor(eCursor_standard, nullptr, 0, 0); + + if (aInitData->mNoAutoHide) { + gint wmd = ConvertBorderStyles(mBorderStyle); + if (wmd != -1) + gdk_window_set_decorations(mGdkWindow, (GdkWMDecoration)wmd); + } + + // If the popup ignores mouse events, set an empty input shape. + SetWindowMouseTransparent(aInitData->mMouseTransparent); + } + } break; + + case eWindowType_plugin: + case eWindowType_plugin_ipc_chrome: + case eWindowType_plugin_ipc_content: + MOZ_ASSERT_UNREACHABLE("Unexpected eWindowType_plugin*"); + return NS_ERROR_FAILURE; + + case eWindowType_child: { + if (parentMozContainer) { + mGdkWindow = CreateGdkWindow(parentGdkWindow, parentMozContainer); + mHasMappedToplevel = parentnsWindow->mHasMappedToplevel; + } else if (parentGtkContainer) { + // This MozContainer has its own window for drawing and receives + // events because there is no mShell widget (corresponding to this + // nsWindow). + GtkWidget* container = moz_container_new(); + mContainer = MOZ_CONTAINER(container); + eventWidget = container; + gtk_widget_add_events(eventWidget, kEvents); + gtk_container_add(parentGtkContainer, container); + gtk_widget_realize(container); + + mGdkWindow = gtk_widget_get_window(container); + } else { + NS_WARNING( + "Warning: tried to create a new child widget with no parent!"); + return NS_ERROR_FAILURE; + } + } break; + default: + break; + } + + // label the drawing window with this object so we can find our way home + g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", this); + if (mDrawToContainer) { + // Also label mShell toplevel window, + // property_notify_event_cb callback also needs to find its way home + g_object_set_data(G_OBJECT(gtk_widget_get_window(mShell)), "nsWindow", + this); + } + + if (mContainer) g_object_set_data(G_OBJECT(mContainer), "nsWindow", this); + + if (mShell) g_object_set_data(G_OBJECT(mShell), "nsWindow", this); + + // attach listeners for events + if (mShell) { + g_signal_connect(mShell, "configure_event", G_CALLBACK(configure_event_cb), + nullptr); + g_signal_connect(mShell, "delete_event", G_CALLBACK(delete_event_cb), + nullptr); + g_signal_connect(mShell, "window_state_event", + G_CALLBACK(window_state_event_cb), nullptr); + g_signal_connect(mShell, "check-resize", G_CALLBACK(check_resize_cb), + nullptr); + g_signal_connect(mShell, "composited-changed", + G_CALLBACK(widget_composited_changed_cb), nullptr); + g_signal_connect(mShell, "property-notify-event", + G_CALLBACK(property_notify_event_cb), nullptr); + + if (mWindowType == eWindowType_toplevel) { + g_signal_connect_after(mShell, "size_allocate", + G_CALLBACK(toplevel_window_size_allocate_cb), + nullptr); + } + + GdkScreen* screen = gtk_widget_get_screen(mShell); + if (!g_signal_handler_find(screen, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + FuncToGpointer(screen_composited_changed_cb), + 0)) { + g_signal_connect(screen, "composited-changed", + G_CALLBACK(screen_composited_changed_cb), nullptr); + } + + GtkSettings* default_settings = gtk_settings_get_default(); + g_signal_connect_after(default_settings, "notify::gtk-theme-name", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-font-name", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-enable-animations", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-decoration-layout", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-xft-dpi", + G_CALLBACK(settings_xft_dpi_changed_cb), this); + // For remote LookAndFeel, to refresh the content processes' copies: + g_signal_connect_after(default_settings, "notify::gtk-cursor-blink-time", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-cursor-blink", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, + "notify::gtk-entry-select-on-focus", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, + "notify::gtk-primary-button-warps-slider", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-menu-popup-delay", + G_CALLBACK(settings_changed_cb), this); + g_signal_connect_after(default_settings, "notify::gtk-dnd-drag-threshold", + G_CALLBACK(settings_changed_cb), this); + } + + if (mContainer) { + // Widget signals + g_signal_connect(mContainer, "unrealize", + G_CALLBACK(container_unrealize_cb), nullptr); + g_signal_connect_after(mContainer, "size_allocate", + G_CALLBACK(size_allocate_cb), nullptr); + g_signal_connect(mContainer, "hierarchy-changed", + G_CALLBACK(hierarchy_changed_cb), nullptr); + g_signal_connect(mContainer, "notify::scale-factor", + G_CALLBACK(scale_changed_cb), nullptr); + // Initialize mHasMappedToplevel. + hierarchy_changed_cb(GTK_WIDGET(mContainer), nullptr); + // Expose, focus, key, and drag events are sent even to GTK_NO_WINDOW + // widgets. + g_signal_connect(G_OBJECT(mContainer), "draw", G_CALLBACK(expose_event_cb), + nullptr); + g_signal_connect(mContainer, "focus_in_event", + G_CALLBACK(focus_in_event_cb), nullptr); + g_signal_connect(mContainer, "focus_out_event", + G_CALLBACK(focus_out_event_cb), nullptr); + g_signal_connect(mContainer, "key_press_event", + G_CALLBACK(key_press_event_cb), nullptr); + g_signal_connect(mContainer, "key_release_event", + G_CALLBACK(key_release_event_cb), nullptr); + + gtk_drag_dest_set((GtkWidget*)mContainer, (GtkDestDefaults)0, nullptr, 0, + (GdkDragAction)0); + + g_signal_connect(mContainer, "drag_motion", + G_CALLBACK(drag_motion_event_cb), nullptr); + g_signal_connect(mContainer, "drag_leave", G_CALLBACK(drag_leave_event_cb), + nullptr); + g_signal_connect(mContainer, "drag_drop", G_CALLBACK(drag_drop_event_cb), + nullptr); + g_signal_connect(mContainer, "drag_data_received", + G_CALLBACK(drag_data_received_event_cb), nullptr); + +#ifdef MOZ_X11 + if (mIsX11Display) { + GtkWidget* widgets[] = {GTK_WIDGET(mContainer), + !mDrawToContainer ? mShell : nullptr}; + for (size_t i = 0; i < ArrayLength(widgets) && widgets[i]; ++i) { + // Double buffering is controlled by the window's owning + // widget. Disable double buffering for painting directly to the + // X Window. + gtk_widget_set_double_buffered(widgets[i], FALSE); + } + } +#endif + + // We create input contexts for all containers, except for + // toplevel popup windows + if (mWindowType != eWindowType_popup) { + mIMContext = new IMContextWrapper(this); + } + } else if (!mIMContext) { + nsWindow* container = GetContainerWindow(); + if (container) { + mIMContext = container->mIMContext; + } + } + + if (eventWidget) { + // These events are sent to the owning widget of the relevant window + // and propagate up to the first widget that handles the events, so we + // need only connect on mShell, if it exists, to catch events on its + // window and windows of mContainer. + g_signal_connect(eventWidget, "enter-notify-event", + G_CALLBACK(enter_notify_event_cb), nullptr); + g_signal_connect(eventWidget, "leave-notify-event", + G_CALLBACK(leave_notify_event_cb), nullptr); + g_signal_connect(eventWidget, "motion-notify-event", + G_CALLBACK(motion_notify_event_cb), nullptr); + g_signal_connect(eventWidget, "button-press-event", + G_CALLBACK(button_press_event_cb), nullptr); + g_signal_connect(eventWidget, "button-release-event", + G_CALLBACK(button_release_event_cb), nullptr); + g_signal_connect(eventWidget, "scroll-event", G_CALLBACK(scroll_event_cb), + nullptr); + if (gtk_check_version(3, 18, 0) == nullptr) { + g_signal_connect(eventWidget, "event", G_CALLBACK(generic_event_cb), + nullptr); + } + g_signal_connect(eventWidget, "touch-event", G_CALLBACK(touch_event_cb), + nullptr); + } + + LOG(("nsWindow [%p] %s %s\n", (void*)this, + mWindowType == eWindowType_toplevel ? "Toplevel" : "Popup", + mIsPIPWindow ? "PIP window" : "")); + if (mShell) { + LOG(("\tmShell %p mContainer %p mGdkWindow %p 0x%lx\n", mShell, mContainer, + mGdkWindow, mIsX11Display ? gdk_x11_window_get_xid(mGdkWindow) : 0)); + } else if (mContainer) { + LOG(("\tmContainer %p mGdkWindow %p\n", mContainer, mGdkWindow)); + } else if (mGdkWindow) { + LOG(("\tmGdkWindow %p parent %p\n", mGdkWindow, + gdk_window_get_parent(mGdkWindow))); + } + + // resize so that everything is set to the right dimensions + if (!mIsTopLevel) + Resize(mBounds.x, mBounds.y, mBounds.width, mBounds.height, false); + +#ifdef MOZ_X11 + if (mIsX11Display && mGdkWindow) { + mXDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow); + mXWindow = gdk_x11_window_get_xid(mGdkWindow); + + GdkVisual* gdkVisual = gdk_window_get_visual(mGdkWindow); + mXVisual = gdk_x11_visual_get_xvisual(gdkVisual); + mXDepth = gdk_visual_get_depth(gdkVisual); + bool shaped = popupNeedsAlphaVisual && !mHasAlphaVisual; + + mSurfaceProvider.Initialize(mXDisplay, mXWindow, mXVisual, mXDepth, shaped); + + if (mIsTopLevel) { + // Set window manager hint to keep fullscreen windows composited. + // + // If the window were to get unredirected, there could be visible + // tearing because Gecko does not align its framebuffer updates with + // vblank. + SetCompositorHint(GTK_WIDGET_COMPOSIDED_ENABLED); + } + } +# ifdef MOZ_WAYLAND + else if (!mIsX11Display) { + mSurfaceProvider.Initialize(this); + WaylandStartVsync(); + } +# endif +#endif + + // Set default application name when it's empty. + if (mGtkWindowAppName.IsEmpty()) { + mGtkWindowAppName = gAppData->name; + } + RefreshWindowClass(); + + return NS_OK; +} + +void nsWindow::RefreshWindowClass(void) { + GdkWindow* gdkWindow = gtk_widget_get_window(mShell); + if (!gdkWindow) { + return; + } + + if (!mGtkWindowRoleName.IsEmpty()) { + gdk_window_set_role(gdkWindow, mGtkWindowRoleName.get()); + } + +#ifdef MOZ_X11 + if (!mGtkWindowAppName.IsEmpty() && mIsX11Display) { + XClassHint* class_hint = XAllocClassHint(); + if (!class_hint) { + return; + } + const char* res_class = gdk_get_program_class(); + if (!res_class) return; + + class_hint->res_name = const_cast<char*>(mGtkWindowAppName.get()); + class_hint->res_class = const_cast<char*>(res_class); + + // Can't use gtk_window_set_wmclass() for this; it prints + // a warning & refuses to make the change. + GdkDisplay* display = gdk_display_get_default(); + XSetClassHint(GDK_DISPLAY_XDISPLAY(display), + gdk_x11_window_get_xid(gdkWindow), class_hint); + XFree(class_hint); + } +#endif /* MOZ_X11 */ +} + +void nsWindow::SetWindowClass(const nsAString& xulWinType) { + if (!mShell) return; + + char* res_name = ToNewCString(xulWinType, mozilla::fallible); + if (!res_name) return; + + const char* role = nullptr; + + // Parse res_name into a name and role. Characters other than + // [A-Za-z0-9_-] are converted to '_'. Anything after the first + // colon is assigned to role; if there's no colon, assign the + // whole thing to both role and res_name. + for (char* c = res_name; *c; c++) { + if (':' == *c) { + *c = 0; + role = c + 1; + } else if (!isascii(*c) || (!isalnum(*c) && ('_' != *c) && ('-' != *c))) + *c = '_'; + } + res_name[0] = toupper(res_name[0]); + if (!role) role = res_name; + + mGtkWindowAppName = res_name; + mGtkWindowRoleName = role; + free(res_name); + + RefreshWindowClass(); +} + +void nsWindow::NativeResize() { + if (!AreBoundsSane()) { + // If someone has set this so that the needs show flag is false + // and it needs to be hidden, update the flag and hide the + // window. This flag will be cleared the next time someone + // hides the window or shows it. It also prevents us from + // calling NativeShow(false) excessively on the window which + // causes unneeded X traffic. + if (!mNeedsShow && mIsShown) { + mNeedsShow = true; + NativeShow(false); + } + return; + } + + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size()); + + LOG(("nsWindow::NativeResize [%p] %d %d\n", (void*)this, size.width, + size.height)); + + if (mIsTopLevel) { + MOZ_ASSERT(size.width > 0 && size.height > 0, + "Can't resize window smaller than 1x1."); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + if (mWaitingForMoveToRectCB) { + LOG(("Waiting for move to rect, schedulling ")); + mPendingSizeRect = mBounds; + } + } else if (mContainer) { + GtkWidget* widget = GTK_WIDGET(mContainer); + GtkAllocation allocation, prev_allocation; + gtk_widget_get_allocation(widget, &prev_allocation); + allocation.x = prev_allocation.x; + allocation.y = prev_allocation.y; + allocation.width = size.width; + allocation.height = size.height; + gtk_widget_size_allocate(widget, &allocation); + } else if (mGdkWindow) { + gdk_window_resize(mGdkWindow, size.width, size.height); + } + +#ifdef MOZ_X11 + // Notify the GtkCompositorWidget of a ClientSizeChange + // This is different than OnSizeAllocate to catch initial sizing + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize()); + } +#endif + + // Does it need to be shown because bounds were previously insane? + if (mNeedsShow && mIsShown) { + NativeShow(true); + } +} + +void nsWindow::NativeMoveResize() { + if (!AreBoundsSane()) { + // If someone has set this so that the needs show flag is false + // and it needs to be hidden, update the flag and hide the + // window. This flag will be cleared the next time someone + // hides the window or shows it. It also prevents us from + // calling NativeShow(false) excessively on the window which + // causes unneeded X traffic. + if (!mNeedsShow && mIsShown) { + mNeedsShow = true; + NativeShow(false); + } + NativeMove(); + + return; + } + + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size()); + GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft()); + + LOG(("nsWindow::NativeMoveResize [%p] %d %d %d %d\n", (void*)this, topLeft.x, + topLeft.y, size.width, size.height)); + + if (IsWaylandPopup()) { + NativeMoveResizeWaylandPopup(&topLeft, &size); + } else { + if (mIsTopLevel) { + // x and y give the position of the window manager frame top-left. + gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y); + // This sets the client window size. + MOZ_ASSERT(size.width > 0 && size.height > 0, + "Can't resize window smaller than 1x1."); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + } else if (mContainer) { + GtkAllocation allocation; + allocation.x = topLeft.x; + allocation.y = topLeft.y; + allocation.width = size.width; + allocation.height = size.height; + gtk_widget_size_allocate(GTK_WIDGET(mContainer), &allocation); + } else if (mGdkWindow) { + gdk_window_move_resize(mGdkWindow, topLeft.x, topLeft.y, size.width, + size.height); + } + } + +#ifdef MOZ_X11 + // Notify the GtkCompositorWidget of a ClientSizeChange + // This is different than OnSizeAllocate to catch initial sizing + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize()); + } +#endif + + // Does it need to be shown because bounds were previously insane? + if (mNeedsShow && mIsShown) { + NativeShow(true); + } +} + +void nsWindow::PauseRemoteRenderer() { +#ifdef MOZ_WAYLAND + if (!mIsDestroyed) { + if (mContainer) { + // Because wl_egl_window is destroyed on moz_container_unmap(), + // the current compositor cannot use it anymore. To avoid crash, + // pause the compositor and destroy EGLSurface & resume the compositor + // and re-create EGLSurface on next expose event. + + // moz_container_wayland_has_egl_window() could not be used here, since + // there is a case that resume compositor is not completed yet. + + CompositorBridgeChild* remoteRenderer = GetRemoteRenderer(); + bool needsCompositorPause = !mNeedsCompositorResume && !!remoteRenderer && + mCompositorWidgetDelegate; + if (needsCompositorPause) { + // XXX slow sync IPC + remoteRenderer->SendPause(); + // Re-request initial draw callback + RefPtr<nsWindow> self(this); + moz_container_wayland_add_initial_draw_callback( + mContainer, [self]() -> void { + self->mNeedsCompositorResume = true; + self->MaybeResumeCompositor(); + }); + } else { + DestroyLayerManager(); + } + } + } +#endif +} + +void nsWindow::HideWaylandWindow() { + if (mWindowType == eWindowType_popup) { + LOG(("nsWindow::HideWaylandWindow: popup [%p]\n", this)); + GList* foundWindow = g_list_find(gVisibleWaylandPopupWindows, this); + if (foundWindow) { + gVisibleWaylandPopupWindows = + g_list_delete_link(gVisibleWaylandPopupWindows, foundWindow); + } + } + PauseRemoteRenderer(); + gtk_widget_hide(mShell); +} + +void nsWindow::WaylandStartVsync() { +#ifdef MOZ_WAYLAND + // only use for toplevel windows for now - see bug 1619246 + if (!gUseWaylandVsync || mWindowType != eWindowType_toplevel) { + return; + } + + if (!mWaylandVsyncSource) { + mWaylandVsyncSource = new mozilla::WaylandVsyncSource(mContainer); + } + WaylandVsyncSource::WaylandDisplay& display = + static_cast<WaylandVsyncSource::WaylandDisplay&>( + mWaylandVsyncSource->GetGlobalDisplay()); + display.EnableMonitor(); +#endif +} + +void nsWindow::WaylandStopVsync() { +#ifdef MOZ_WAYLAND + if (mWaylandVsyncSource) { + // The widget is going to be hidden, so clear the surface of our + // vsync source. + WaylandVsyncSource::WaylandDisplay& display = + static_cast<WaylandVsyncSource::WaylandDisplay&>( + mWaylandVsyncSource->GetGlobalDisplay()); + display.DisableMonitor(); + } +#endif +} + +void nsWindow::NativeShow(bool aAction) { + if (aAction) { + // unset our flag now that our window has been shown + mNeedsShow = false; + + if (mIsTopLevel) { + // Set up usertime/startupID metadata for the created window. + if (mWindowType != eWindowType_invisible) { + SetUserTimeAndStartupIDForActivatedWindow(mShell); + } + // Update popup window hierarchy run-time on Wayland. + if (IsWaylandPopup()) { + if (!ConfigureWaylandPopupWindows()) { + mNeedsShow = true; + return; + } + } + + LOG((" calling gtk_widget_show(mShell)\n")); + gtk_widget_show(mShell); + if (!mIsX11Display) { + WaylandStartVsync(); + } + } else if (mContainer) { + LOG((" calling gtk_widget_show(mContainer)\n")); + gtk_widget_show(GTK_WIDGET(mContainer)); + } else if (mGdkWindow) { + LOG((" calling gdk_window_show_unraised\n")); + gdk_window_show_unraised(mGdkWindow); + } + } else { + // There's a chance that when the popup will be shown again it might be + // resized because parent could be moved meanwhile. + mPreferredPopupRect = nsRect(0, 0, 0, 0); + mPreferredPopupRectFlushed = false; + if (!mIsX11Display) { + WaylandStopVsync(); + if (IsWaylandPopup() && IsMainMenuWindow()) { + CleanupWaylandPopups(); + } + HideWaylandWindow(); + } else if (mIsTopLevel) { + // Workaround window freezes on GTK versions before 3.21.2 by + // ensuring that configure events get dispatched to windows before + // they are unmapped. See bug 1225044. + if (gtk_check_version(3, 21, 2) != nullptr && mPendingConfigures > 0) { + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(mShell), &allocation); + + GdkEventConfigure event; + PodZero(&event); + event.type = GDK_CONFIGURE; + event.window = mGdkWindow; + event.send_event = TRUE; + event.x = allocation.x; + event.y = allocation.y; + event.width = allocation.width; + event.height = allocation.height; + + auto shellClass = GTK_WIDGET_GET_CLASS(mShell); + for (unsigned int i = 0; i < mPendingConfigures; i++) { + Unused << shellClass->configure_event(mShell, &event); + } + mPendingConfigures = 0; + } + gtk_widget_hide(mShell); + + ClearTransparencyBitmap(); // Release some resources + } else if (mContainer) { + gtk_widget_hide(GTK_WIDGET(mContainer)); + } else if (mGdkWindow) { + gdk_window_hide(mGdkWindow); + } + } +} + +void nsWindow::SetHasMappedToplevel(bool aState) { + // Even when aState == mHasMappedToplevel (as when this method is called + // from Show()), child windows need to have their state checked, so don't + // return early. + bool oldState = mHasMappedToplevel; + mHasMappedToplevel = aState; + + // mHasMappedToplevel is not updated for children of windows that are + // hidden; GDK knows not to send expose events for these windows. The + // state is recorded on the hidden window itself, but, for child trees of + // hidden windows, their state essentially becomes disconnected from their + // hidden parent. When the hidden parent gets shown, the child trees are + // reconnected, and the state of the window being shown can be easily + // propagated. + if (!mIsShown || !mGdkWindow) return; + + if (aState && !oldState) { + // Check that a grab didn't fail due to the window not being + // viewable. + EnsureGrabs(); + } + + for (GList* children = gdk_window_peek_children(mGdkWindow); children; + children = children->next) { + GdkWindow* gdkWin = GDK_WINDOW(children->data); + nsWindow* child = get_window_for_gdk_window(gdkWin); + + if (child && child->mHasMappedToplevel != aState) { + child->SetHasMappedToplevel(aState); + } + } +} + +LayoutDeviceIntSize nsWindow::GetSafeWindowSize(LayoutDeviceIntSize aSize) { + // The X protocol uses CARD32 for window sizes, but the server (1.11.3) + // reads it as CARD16. Sizes of pixmaps, used for drawing, are (unsigned) + // CARD16 in the protocol, but the server's ProcCreatePixmap returns + // BadAlloc if dimensions cannot be represented by signed shorts. + // Because we are creating Cairo surfaces to represent window buffers, + // we also must ensure that the window can fit in a Cairo surface. + LayoutDeviceIntSize result = aSize; + int32_t maxSize = 32767; + if (mLayerManager && mLayerManager->AsKnowsCompositor()) { + maxSize = std::min(maxSize, + mLayerManager->AsKnowsCompositor()->GetMaxTextureSize()); + } + if (result.width > maxSize) { + result.width = maxSize; + } + if (result.height > maxSize) { + result.height = maxSize; + } + return result; +} + +void nsWindow::EnsureGrabs(void) { + if (mRetryPointerGrab) GrabPointer(sRetryGrabTime); +} + +void nsWindow::CleanLayerManagerRecursive(void) { + if (mLayerManager) { + mLayerManager->Destroy(); + mLayerManager = nullptr; + } + + DestroyCompositor(); + + GList* children = gdk_window_peek_children(mGdkWindow); + for (GList* list = children; list; list = list->next) { + nsWindow* window = get_window_for_gdk_window(GDK_WINDOW(list->data)); + if (window) { + window->CleanLayerManagerRecursive(); + } + } +} + +void nsWindow::SetTransparencyMode(nsTransparencyMode aMode) { + if (!mShell) { + // Pass the request to the toplevel window + GtkWidget* topWidget = GetToplevelWidget(); + if (!topWidget) return; + + nsWindow* topWindow = get_window_for_gtk_widget(topWidget); + if (!topWindow) return; + + topWindow->SetTransparencyMode(aMode); + return; + } + + bool isTransparent = aMode == eTransparencyTransparent; + + if (mIsTransparent == isTransparent) { + return; + } + + if (mWindowType != eWindowType_popup) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=1344839 reported + // problems cleaning the layer manager for toplevel windows. + // Ignore the request so as to workaround that. + // mIsTransparent is set in Create() if transparency may be required. + if (isTransparent) { + NS_WARNING("Transparent mode not supported on non-popup windows."); + } + return; + } + + if (!isTransparent) { + ClearTransparencyBitmap(); + } // else the new default alpha values are "all 1", so we don't + // need to change anything yet + + mIsTransparent = isTransparent; + + if (!mHasAlphaVisual) { + // The choice of layer manager depends on + // GtkCompositorWidgetInitData::Shaped(), which will need to change, so + // clean out the old layer manager. + CleanLayerManagerRecursive(); + } +} + +nsTransparencyMode nsWindow::GetTransparencyMode() { + if (!mShell) { + // Pass the request to the toplevel window + GtkWidget* topWidget = GetToplevelWidget(); + if (!topWidget) { + return eTransparencyOpaque; + } + + nsWindow* topWindow = get_window_for_gtk_widget(topWidget); + if (!topWindow) { + return eTransparencyOpaque; + } + + return topWindow->GetTransparencyMode(); + } + + return mIsTransparent ? eTransparencyTransparent : eTransparencyOpaque; +} + +void nsWindow::SetWindowMouseTransparent(bool aIsTransparent) { + if (!mGdkWindow) { + return; + } + + cairo_rectangle_int_t emptyRect = {0, 0, 0, 0}; + cairo_region_t* region = + aIsTransparent ? cairo_region_create_rectangle(&emptyRect) : nullptr; + + gdk_window_input_shape_combine_region(mGdkWindow, region, 0, 0); + if (region) { + cairo_region_destroy(region); + } +} + +// For setting the draggable titlebar region from CSS +// with -moz-window-dragging: drag. +void nsWindow::UpdateWindowDraggingRegion( + const LayoutDeviceIntRegion& aRegion) { + if (mDraggableRegion != aRegion) { + mDraggableRegion = aRegion; + } +} + +// See subtract_corners_from_region() at gtk/gtkwindow.c +// We need to subtract corners from toplevel window opaque region +// to draw transparent corners of default Gtk titlebar. +// Both implementations (cairo_region_t and wl_region) needs to be synced. +static void SubtractTitlebarCorners(cairo_region_t* aRegion, int aX, int aY, + int aWindowWidth) { + cairo_rectangle_int_t rect = {aX, aY, TITLEBAR_SHAPE_MASK_HEIGHT, + TITLEBAR_SHAPE_MASK_HEIGHT}; + cairo_region_subtract_rectangle(aRegion, &rect); + rect = { + aX + aWindowWidth - TITLEBAR_SHAPE_MASK_HEIGHT, + aY, + TITLEBAR_SHAPE_MASK_HEIGHT, + TITLEBAR_SHAPE_MASK_HEIGHT, + }; + cairo_region_subtract_rectangle(aRegion, &rect); +} + +void nsWindow::UpdateTopLevelOpaqueRegion(void) { + if (!mCompositedScreen) { + return; + } + + GdkWindow* window = + (mDrawToContainer) ? gtk_widget_get_window(mShell) : mGdkWindow; + if (!window) { + return; + } + MOZ_ASSERT(gdk_window_get_window_type(window) == GDK_WINDOW_TOPLEVEL); + + int x = 0; + int y = 0; + + if (mDrawToContainer) { + gdk_window_get_position(mGdkWindow, &x, &y); + } + + int width = DevicePixelsToGdkCoordRoundDown(mBounds.width); + int height = DevicePixelsToGdkCoordRoundDown(mBounds.height); + + cairo_region_t* region = cairo_region_create(); + cairo_rectangle_int_t rect = {x, y, width, height}; + cairo_region_union_rectangle(region, &rect); + + bool subtractCorners = DoDrawTilebarCorners(); + if (subtractCorners) { + SubtractTitlebarCorners(region, x, y, width); + } + + gdk_window_set_opaque_region(window, region); + + cairo_region_destroy(region); + +#ifdef MOZ_WAYLAND + // We don't set opaque region to mozContainer by default due to Bug 1615098. + if (!mIsX11Display && gUseWaylandUseOpaqueRegion) { + moz_container_wayland_update_opaque_region(mContainer, subtractCorners); + } +#endif +} + +bool nsWindow::IsChromeWindowTitlebar() { + return mDrawInTitlebar && !mIsPIPWindow && + mWindowType == eWindowType_toplevel; +} + +bool nsWindow::DoDrawTilebarCorners() { + return IsChromeWindowTitlebar() && mSizeState == nsSizeMode_Normal && + !mIsTiled; +} + +nsresult nsWindow::ConfigureChildren( + const nsTArray<Configuration>& aConfigurations) { + // If this is a remotely updated widget we receive clipping, position, and + // size information from a source other than our owner. Don't let our parent + // update this information. + if (mWindowType == eWindowType_plugin_ipc_chrome) { + return NS_OK; + } + + for (uint32_t i = 0; i < aConfigurations.Length(); ++i) { + const Configuration& configuration = aConfigurations[i]; + auto* w = static_cast<nsWindow*>(configuration.mChild.get()); + NS_ASSERTION(w->GetParent() == this, "Configured widget is not a child"); + w->SetWindowClipRegion(configuration.mClipRegion, true); + if (w->mBounds.Size() != configuration.mBounds.Size()) { + w->Resize(configuration.mBounds.x, configuration.mBounds.y, + configuration.mBounds.width, configuration.mBounds.height, + true); + } else if (w->mBounds.TopLeft() != configuration.mBounds.TopLeft()) { + w->Move(configuration.mBounds.x, configuration.mBounds.y); + } + w->SetWindowClipRegion(configuration.mClipRegion, false); + } + return NS_OK; +} + +nsresult nsWindow::SetWindowClipRegion( + const nsTArray<LayoutDeviceIntRect>& aRects, bool aIntersectWithExisting) { + const nsTArray<LayoutDeviceIntRect>* newRects = &aRects; + + AutoTArray<LayoutDeviceIntRect, 1> intersectRects; + if (aIntersectWithExisting) { + AutoTArray<LayoutDeviceIntRect, 1> existingRects; + GetWindowClipRegion(&existingRects); + + LayoutDeviceIntRegion existingRegion = RegionFromArray(existingRects); + LayoutDeviceIntRegion newRegion = RegionFromArray(aRects); + LayoutDeviceIntRegion intersectRegion; + intersectRegion.And(newRegion, existingRegion); + + // If mClipRects is null we haven't set a clip rect yet, so we + // need to set the clip even if it is equal. + if (mClipRects && intersectRegion.IsEqual(existingRegion)) { + return NS_OK; + } + + if (!intersectRegion.IsEqual(newRegion)) { + ArrayFromRegion(intersectRegion, intersectRects); + newRects = &intersectRects; + } + } + + if (IsWindowClipRegionEqual(*newRects)) return NS_OK; + + StoreWindowClipRegion(*newRects); + + if (!mGdkWindow) return NS_OK; + + cairo_region_t* region = cairo_region_create(); + for (uint32_t i = 0; i < newRects->Length(); ++i) { + const LayoutDeviceIntRect& r = newRects->ElementAt(i); + cairo_rectangle_int_t rect = {r.x, r.y, r.width, r.height}; + cairo_region_union_rectangle(region, &rect); + } + + gdk_window_shape_combine_region(mGdkWindow, region, 0, 0); + cairo_region_destroy(region); + + return NS_OK; +} + +void nsWindow::ResizeTransparencyBitmap() { + if (!mTransparencyBitmap) return; + + if (mBounds.width == mTransparencyBitmapWidth && + mBounds.height == mTransparencyBitmapHeight) + return; + + int32_t newRowBytes = GetBitmapStride(mBounds.width); + int32_t newSize = newRowBytes * mBounds.height; + auto* newBits = new gchar[newSize]; + // fill new mask with "transparent", first + memset(newBits, 0, newSize); + + // Now copy the intersection of the old and new areas into the new mask + int32_t copyWidth = std::min(mBounds.width, mTransparencyBitmapWidth); + int32_t copyHeight = std::min(mBounds.height, mTransparencyBitmapHeight); + int32_t oldRowBytes = GetBitmapStride(mTransparencyBitmapWidth); + int32_t copyBytes = GetBitmapStride(copyWidth); + + int32_t i; + gchar* fromPtr = mTransparencyBitmap; + gchar* toPtr = newBits; + for (i = 0; i < copyHeight; i++) { + memcpy(toPtr, fromPtr, copyBytes); + fromPtr += oldRowBytes; + toPtr += newRowBytes; + } + + delete[] mTransparencyBitmap; + mTransparencyBitmap = newBits; + mTransparencyBitmapWidth = mBounds.width; + mTransparencyBitmapHeight = mBounds.height; +} + +static bool ChangedMaskBits(gchar* aMaskBits, int32_t aMaskWidth, + int32_t aMaskHeight, const nsIntRect& aRect, + uint8_t* aAlphas, int32_t aStride) { + int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost(); + int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth); + for (y = aRect.y; y < yMax; y++) { + gchar* maskBytes = aMaskBits + y * maskBytesPerRow; + uint8_t* alphas = aAlphas; + for (x = aRect.x; x < xMax; x++) { + bool newBit = *alphas > 0x7f; + alphas++; + + gchar maskByte = maskBytes[x >> 3]; + bool maskBit = (maskByte & (1 << (x & 7))) != 0; + + if (maskBit != newBit) { + return true; + } + } + aAlphas += aStride; + } + + return false; +} + +static void UpdateMaskBits(gchar* aMaskBits, int32_t aMaskWidth, + int32_t aMaskHeight, const nsIntRect& aRect, + uint8_t* aAlphas, int32_t aStride) { + int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost(); + int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth); + for (y = aRect.y; y < yMax; y++) { + gchar* maskBytes = aMaskBits + y * maskBytesPerRow; + uint8_t* alphas = aAlphas; + for (x = aRect.x; x < xMax; x++) { + bool newBit = *alphas > 0x7f; + alphas++; + + gchar mask = 1 << (x & 7); + gchar maskByte = maskBytes[x >> 3]; + // Note: '-newBit' turns 0 into 00...00 and 1 into 11...11 + maskBytes[x >> 3] = (maskByte & ~mask) | (-newBit & mask); + } + aAlphas += aStride; + } +} + +void nsWindow::ApplyTransparencyBitmap() { +#ifdef MOZ_X11 + // We use X11 calls where possible, because GDK handles expose events + // for shaped windows in a way that's incompatible with us (Bug 635903). + // It doesn't occur when the shapes are set through X. + Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow); + Window xDrawable = GDK_WINDOW_XID(mGdkWindow); + Pixmap maskPixmap = XCreateBitmapFromData( + xDisplay, xDrawable, mTransparencyBitmap, mTransparencyBitmapWidth, + mTransparencyBitmapHeight); + XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap, + ShapeSet); + XFreePixmap(xDisplay, maskPixmap); +#else + cairo_surface_t* maskBitmap; + maskBitmap = cairo_image_surface_create_for_data( + (unsigned char*)mTransparencyBitmap, CAIRO_FORMAT_A1, + mTransparencyBitmapWidth, mTransparencyBitmapHeight, + GetBitmapStride(mTransparencyBitmapWidth)); + if (!maskBitmap) return; + + cairo_region_t* maskRegion = gdk_cairo_region_create_from_surface(maskBitmap); + gtk_widget_shape_combine_region(mShell, maskRegion); + cairo_region_destroy(maskRegion); + cairo_surface_destroy(maskBitmap); +#endif // MOZ_X11 +} + +void nsWindow::ClearTransparencyBitmap() { + if (!mTransparencyBitmap) return; + + delete[] mTransparencyBitmap; + mTransparencyBitmap = nullptr; + mTransparencyBitmapWidth = 0; + mTransparencyBitmapHeight = 0; + + if (!mShell) return; + +#ifdef MOZ_X11 + if (!mGdkWindow) return; + + Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow); + Window xWindow = gdk_x11_window_get_xid(mGdkWindow); + + XShapeCombineMask(xDisplay, xWindow, ShapeBounding, 0, 0, X11None, ShapeSet); +#endif +} + +nsresult nsWindow::UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect, + uint8_t* aAlphas, + int32_t aStride) { + if (!mShell) { + // Pass the request to the toplevel window + GtkWidget* topWidget = GetToplevelWidget(); + if (!topWidget) return NS_ERROR_FAILURE; + + nsWindow* topWindow = get_window_for_gtk_widget(topWidget); + if (!topWindow) return NS_ERROR_FAILURE; + + return topWindow->UpdateTranslucentWindowAlphaInternal(aRect, aAlphas, + aStride); + } + + NS_ASSERTION(mIsTransparent, "Window is not transparent"); + NS_ASSERTION(!mTransparencyBitmapForTitlebar, + "Transparency bitmap is already used for titlebar rendering"); + + if (mTransparencyBitmap == nullptr) { + int32_t size = GetBitmapStride(mBounds.width) * mBounds.height; + mTransparencyBitmap = new gchar[size]; + memset(mTransparencyBitmap, 255, size); + mTransparencyBitmapWidth = mBounds.width; + mTransparencyBitmapHeight = mBounds.height; + } else { + ResizeTransparencyBitmap(); + } + + nsIntRect rect; + rect.IntersectRect(aRect, nsIntRect(0, 0, mBounds.width, mBounds.height)); + + if (!ChangedMaskBits(mTransparencyBitmap, mBounds.width, mBounds.height, rect, + aAlphas, aStride)) + // skip the expensive stuff if the mask bits haven't changed; hopefully + // this is the common case + return NS_OK; + + UpdateMaskBits(mTransparencyBitmap, mBounds.width, mBounds.height, rect, + aAlphas, aStride); + + if (!mNeedsShow) { + ApplyTransparencyBitmap(); + } + return NS_OK; +} + +bool nsWindow::GetTitlebarRect(mozilla::gfx::Rect& aRect) { + if (!mGdkWindow || !mDrawInTitlebar) { + return false; + } + + aRect = gfx::Rect(0, 0, mBounds.width, TITLEBAR_SHAPE_MASK_HEIGHT); + return true; +} + +void nsWindow::UpdateTitlebarTransparencyBitmap() { + NS_ASSERTION(mTransparencyBitmapForTitlebar, + "Transparency bitmap is already used to draw window shape"); + + if (!mGdkWindow || !mDrawInTitlebar || + (mBounds.width == mTransparencyBitmapWidth && + mBounds.height == mTransparencyBitmapHeight)) { + return; + } + + bool maskCreate = + !mTransparencyBitmap || mBounds.width > mTransparencyBitmapWidth; + + bool maskUpdate = + !mTransparencyBitmap || mBounds.width != mTransparencyBitmapWidth; + + if (maskCreate) { + if (mTransparencyBitmap) { + delete[] mTransparencyBitmap; + } + int32_t size = GetBitmapStride(mBounds.width) * TITLEBAR_SHAPE_MASK_HEIGHT; + mTransparencyBitmap = new gchar[size]; + mTransparencyBitmapWidth = mBounds.width; + } else { + mTransparencyBitmapWidth = mBounds.width; + } + mTransparencyBitmapHeight = mBounds.height; + + if (maskUpdate) { + cairo_surface_t* surface = cairo_image_surface_create( + CAIRO_FORMAT_A8, mTransparencyBitmapWidth, TITLEBAR_SHAPE_MASK_HEIGHT); + if (!surface) return; + + cairo_t* cr = cairo_create(surface); + + GtkWidgetState state; + memset((void*)&state, 0, sizeof(state)); + GdkRectangle rect = {0, 0, mTransparencyBitmapWidth, + TITLEBAR_SHAPE_MASK_HEIGHT}; + + moz_gtk_widget_paint(MOZ_GTK_HEADER_BAR, cr, &rect, &state, 0, + GTK_TEXT_DIR_NONE); + + cairo_destroy(cr); + cairo_surface_mark_dirty(surface); + cairo_surface_flush(surface); + + UpdateMaskBits( + mTransparencyBitmap, mTransparencyBitmapWidth, + TITLEBAR_SHAPE_MASK_HEIGHT, + nsIntRect(0, 0, mTransparencyBitmapWidth, TITLEBAR_SHAPE_MASK_HEIGHT), + cairo_image_surface_get_data(surface), + cairo_format_stride_for_width(CAIRO_FORMAT_A8, + mTransparencyBitmapWidth)); + + cairo_surface_destroy(surface); + } + + if (!mNeedsShow) { + Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow); + Window xDrawable = GDK_WINDOW_XID(mGdkWindow); + + Pixmap maskPixmap = XCreateBitmapFromData( + xDisplay, xDrawable, mTransparencyBitmap, mTransparencyBitmapWidth, + TITLEBAR_SHAPE_MASK_HEIGHT); + + XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap, + ShapeSet); + + if (mTransparencyBitmapHeight > TITLEBAR_SHAPE_MASK_HEIGHT) { + XRectangle rect = {0, 0, (unsigned short)mTransparencyBitmapWidth, + (unsigned short)(mTransparencyBitmapHeight - + TITLEBAR_SHAPE_MASK_HEIGHT)}; + XShapeCombineRectangles(xDisplay, xDrawable, ShapeBounding, 0, + TITLEBAR_SHAPE_MASK_HEIGHT, &rect, 1, ShapeUnion, + 0); + } + + XFreePixmap(xDisplay, maskPixmap); + } +} + +void nsWindow::GrabPointer(guint32 aTime) { + LOG(("GrabPointer time=0x%08x retry=%d\n", (unsigned int)aTime, + mRetryPointerGrab)); + + mRetryPointerGrab = false; + sRetryGrabTime = aTime; + + // If the window isn't visible, just set the flag to retry the + // grab. When this window becomes visible, the grab will be + // retried. + if (!mHasMappedToplevel) { + LOG(("GrabPointer: window not visible\n")); + mRetryPointerGrab = true; + return; + } + + if (!mGdkWindow) return; + + if (!mIsX11Display) { + // Don't to the grab on Wayland as it causes a regression + // from Bug 1377084. + return; + } + + gint retval; + // Note that we need GDK_TOUCH_MASK below to work around a GDK/X11 bug that + // causes touch events that would normally be received by this client on + // other windows to be discarded during the grab. + retval = gdk_pointer_grab( + mGdkWindow, TRUE, + (GdkEventMask)(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | + GDK_POINTER_MOTION_MASK | GDK_TOUCH_MASK), + (GdkWindow*)nullptr, nullptr, aTime); + + if (retval == GDK_GRAB_NOT_VIEWABLE) { + LOG(("GrabPointer: window not viewable; will retry\n")); + mRetryPointerGrab = true; + } else if (retval != GDK_GRAB_SUCCESS) { + LOG(("GrabPointer: pointer grab failed: %i\n", retval)); + // A failed grab indicates that another app has grabbed the pointer. + // Check for rollup now, because, without the grab, we likely won't + // get subsequent button press events. Do this with an event so that + // popups don't rollup while potentially adjusting the grab for + // this popup. + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod("nsWindow::CheckForRollupDuringGrab", this, + &nsWindow::CheckForRollupDuringGrab); + NS_DispatchToCurrentThread(event.forget()); + } +} + +void nsWindow::ReleaseGrabs(void) { + LOG(("ReleaseGrabs\n")); + + mRetryPointerGrab = false; + + if (!mIsX11Display) { + // Don't to the ungrab on Wayland as it causes a regression + // from Bug 1377084. + return; + } + + gdk_pointer_ungrab(GDK_CURRENT_TIME); +} + +GtkWidget* nsWindow::GetToplevelWidget() { + if (mShell) { + return mShell; + } + + GtkWidget* widget = GetMozContainerWidget(); + if (!widget) return nullptr; + + return gtk_widget_get_toplevel(widget); +} + +GtkWidget* nsWindow::GetMozContainerWidget() { + if (!mGdkWindow) return nullptr; + + if (mContainer) return GTK_WIDGET(mContainer); + + GtkWidget* owningWidget = get_gtk_widget_for_gdk_window(mGdkWindow); + return owningWidget; +} + +nsWindow* nsWindow::GetContainerWindow() { + GtkWidget* owningWidget = GetMozContainerWidget(); + if (!owningWidget) return nullptr; + + nsWindow* window = get_window_for_gtk_widget(owningWidget); + NS_ASSERTION(window, "No nsWindow for container widget"); + return window; +} + +void nsWindow::SetUrgencyHint(GtkWidget* top_window, bool state) { + if (!top_window) return; + + gdk_window_set_urgency_hint(gtk_widget_get_window(top_window), state); +} + +void nsWindow::SetDefaultIcon(void) { SetIcon(u"default"_ns); } + +gint nsWindow::ConvertBorderStyles(nsBorderStyle aStyle) { + gint w = 0; + + if (aStyle == eBorderStyle_default) return -1; + + // note that we don't handle eBorderStyle_close yet + if (aStyle & eBorderStyle_all) w |= GDK_DECOR_ALL; + if (aStyle & eBorderStyle_border) w |= GDK_DECOR_BORDER; + if (aStyle & eBorderStyle_resizeh) w |= GDK_DECOR_RESIZEH; + if (aStyle & eBorderStyle_title) w |= GDK_DECOR_TITLE; + if (aStyle & eBorderStyle_menu) w |= GDK_DECOR_MENU; + if (aStyle & eBorderStyle_minimize) w |= GDK_DECOR_MINIMIZE; + if (aStyle & eBorderStyle_maximize) w |= GDK_DECOR_MAXIMIZE; + + return w; +} + +class FullscreenTransitionWindow final : public nsISupports { + public: + NS_DECL_ISUPPORTS + + explicit FullscreenTransitionWindow(GtkWidget* aWidget); + + GtkWidget* mWindow; + + private: + ~FullscreenTransitionWindow(); +}; + +NS_IMPL_ISUPPORTS0(FullscreenTransitionWindow) + +FullscreenTransitionWindow::FullscreenTransitionWindow(GtkWidget* aWidget) { + mWindow = gtk_window_new(GTK_WINDOW_POPUP); + GtkWindow* gtkWin = GTK_WINDOW(mWindow); + + gtk_window_set_type_hint(gtkWin, GDK_WINDOW_TYPE_HINT_SPLASHSCREEN); + gtk_window_set_transient_for(gtkWin, GTK_WINDOW(aWidget)); + gtk_window_set_decorated(gtkWin, false); + + GdkWindow* gdkWin = gtk_widget_get_window(aWidget); + GdkScreen* screen = gtk_widget_get_screen(aWidget); + gint monitorNum = gdk_screen_get_monitor_at_window(screen, gdkWin); + GdkRectangle monitorRect; + gdk_screen_get_monitor_geometry(screen, monitorNum, &monitorRect); + gtk_window_set_screen(gtkWin, screen); + gtk_window_move(gtkWin, monitorRect.x, monitorRect.y); + MOZ_ASSERT(monitorRect.width > 0 && monitorRect.height > 0, + "Can't resize window smaller than 1x1."); + gtk_window_resize(gtkWin, monitorRect.width, monitorRect.height); + + GdkColor bgColor; + bgColor.red = bgColor.green = bgColor.blue = 0; + gtk_widget_modify_bg(mWindow, GTK_STATE_NORMAL, &bgColor); + + gtk_window_set_opacity(gtkWin, 0.0); + gtk_widget_show(mWindow); +} + +FullscreenTransitionWindow::~FullscreenTransitionWindow() { + gtk_widget_destroy(mWindow); +} + +class FullscreenTransitionData { + public: + FullscreenTransitionData(nsIWidget::FullscreenTransitionStage aStage, + uint16_t aDuration, nsIRunnable* aCallback, + FullscreenTransitionWindow* aWindow) + : mStage(aStage), + mStartTime(TimeStamp::Now()), + mDuration(TimeDuration::FromMilliseconds(aDuration)), + mCallback(aCallback), + mWindow(aWindow) {} + + static const guint sInterval = 1000 / 30; // 30fps + static gboolean TimeoutCallback(gpointer aData); + + private: + nsIWidget::FullscreenTransitionStage mStage; + TimeStamp mStartTime; + TimeDuration mDuration; + nsCOMPtr<nsIRunnable> mCallback; + RefPtr<FullscreenTransitionWindow> mWindow; +}; + +/* static */ +gboolean FullscreenTransitionData::TimeoutCallback(gpointer aData) { + bool finishing = false; + auto data = static_cast<FullscreenTransitionData*>(aData); + gdouble opacity = (TimeStamp::Now() - data->mStartTime) / data->mDuration; + if (opacity >= 1.0) { + opacity = 1.0; + finishing = true; + } + if (data->mStage == nsIWidget::eAfterFullscreenToggle) { + opacity = 1.0 - opacity; + } + gtk_window_set_opacity(GTK_WINDOW(data->mWindow->mWindow), opacity); + + if (!finishing) { + return TRUE; + } + NS_DispatchToMainThread(data->mCallback.forget()); + delete data; + return FALSE; +} + +/* virtual */ +bool nsWindow::PrepareForFullscreenTransition(nsISupports** aData) { + if (!mCompositedScreen) { + return false; + } + *aData = do_AddRef(new FullscreenTransitionWindow(mShell)).take(); + return true; +} + +/* virtual */ +void nsWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage, + uint16_t aDuration, + nsISupports* aData, + nsIRunnable* aCallback) { + auto data = static_cast<FullscreenTransitionWindow*>(aData); + // This will be released at the end of the last timeout callback for it. + auto transitionData = + new FullscreenTransitionData(aStage, aDuration, aCallback, data); + g_timeout_add_full(G_PRIORITY_HIGH, FullscreenTransitionData::sInterval, + FullscreenTransitionData::TimeoutCallback, transitionData, + nullptr); +} + +already_AddRefed<nsIScreen> nsWindow::GetWidgetScreen() { + nsCOMPtr<nsIScreenManager> screenManager; + screenManager = do_GetService("@mozilla.org/gfx/screenmanager;1"); + if (!screenManager) { + return nullptr; + } + + // GetScreenBounds() is slow for the GTK port so we override and use + // mBounds directly. + LayoutDeviceIntRect bounds = mBounds; + if (!mIsTopLevel) { + bounds.MoveTo(WidgetToScreenOffset()); + } + + DesktopIntRect deskBounds = RoundedToInt(bounds / GetDesktopToDeviceScale()); + nsCOMPtr<nsIScreen> screen; + screenManager->ScreenForRect(deskBounds.x, deskBounds.y, deskBounds.width, + deskBounds.height, getter_AddRefs(screen)); + return screen.forget(); +} + +RefPtr<VsyncSource> nsWindow::GetVsyncSource() { +#ifdef MOZ_WAYLAND + if (mWaylandVsyncSource) { + return mWaylandVsyncSource; + } +#endif + + return nullptr; +} + +static bool IsFullscreenSupported(GtkWidget* aShell) { +#ifdef MOZ_X11 + GdkScreen* screen = gtk_widget_get_screen(aShell); + GdkAtom atom = gdk_atom_intern("_NET_WM_STATE_FULLSCREEN", FALSE); + if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) { + return false; + } +#endif + return true; +} + +nsresult nsWindow::MakeFullScreen(bool aFullScreen, nsIScreen* aTargetScreen) { + LOG(("nsWindow::MakeFullScreen [%p] aFullScreen %d\n", (void*)this, + aFullScreen)); + + if (mIsX11Display && !IsFullscreenSupported(mShell)) { + return NS_ERROR_NOT_AVAILABLE; + } + + bool wasFullscreen = mSizeState == nsSizeMode_Fullscreen; + if (aFullScreen != wasFullscreen && mWidgetListener) { + mWidgetListener->FullscreenWillChange(aFullScreen); + } + + if (aFullScreen) { + if (mSizeMode != nsSizeMode_Fullscreen) mLastSizeMode = mSizeMode; + + mSizeMode = nsSizeMode_Fullscreen; + + if (mIsPIPWindow) { + gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_NORMAL); + if (gUseAspectRatio) { + mAspectRatioSaved = mAspectRatio; + mAspectRatio = 0.0f; + ApplySizeConstraints(); + } + } + + gtk_window_fullscreen(GTK_WINDOW(mShell)); + } else { + mSizeMode = mLastSizeMode; + gtk_window_unfullscreen(GTK_WINDOW(mShell)); + + if (mIsPIPWindow) { + gtk_window_set_type_hint(GTK_WINDOW(mShell), + GDK_WINDOW_TYPE_HINT_UTILITY); + if (gUseAspectRatio) { + mAspectRatio = mAspectRatioSaved; + // ApplySizeConstraints(); + } + } + } + + NS_ASSERTION(mLastSizeMode != nsSizeMode_Fullscreen, + "mLastSizeMode should never be fullscreen"); + return NS_OK; +} + +void nsWindow::SetWindowDecoration(nsBorderStyle aStyle) { + LOG(("nsWindow::SetWindowDecoration() [%p] Border style %x\n", (void*)this, + aStyle)); + + if (!mShell) { + // Pass the request to the toplevel window + GtkWidget* topWidget = GetToplevelWidget(); + if (!topWidget) return; + + nsWindow* topWindow = get_window_for_gtk_widget(topWidget); + if (!topWindow) return; + + topWindow->SetWindowDecoration(aStyle); + return; + } + + // We can't use mGdkWindow directly here as it can be + // derived from mContainer which is not a top-level GdkWindow. + GdkWindow* window = gtk_widget_get_window(mShell); + + // Sawfish, metacity, and presumably other window managers get + // confused if we change the window decorations while the window + // is visible. + bool wasVisible = false; + if (gdk_window_is_visible(window)) { + gdk_window_hide(window); + wasVisible = true; + } + + gint wmd = ConvertBorderStyles(aStyle); + if (wmd != -1) gdk_window_set_decorations(window, (GdkWMDecoration)wmd); + + if (wasVisible) gdk_window_show(window); + + // For some window managers, adding or removing window decorations + // requires unmapping and remapping our toplevel window. Go ahead + // and flush the queue here so that we don't end up with a BadWindow + // error later when this happens (when the persistence timer fires + // and GetWindowPos is called) +#ifdef MOZ_X11 + if (mIsX11Display) { + XSync(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), X11False); + } else +#endif /* MOZ_X11 */ + { + gdk_flush(); + } +} + +void nsWindow::HideWindowChrome(bool aShouldHide) { + SetWindowDecoration(aShouldHide ? eBorderStyle_none : mBorderStyle); +} + +bool nsWindow::CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel, + bool aAlwaysRollup) { + nsIRollupListener* rollupListener = GetActiveRollupListener(); + nsCOMPtr<nsIWidget> rollupWidget; + if (rollupListener) { + rollupWidget = rollupListener->GetRollupWidget(); + } + if (!rollupWidget) { + nsBaseWidget::gRollupListener = nullptr; + return false; + } + + bool retVal = false; + auto* currentPopup = + (GdkWindow*)rollupWidget->GetNativeData(NS_NATIVE_WINDOW); + if (aAlwaysRollup || !is_mouse_in_window(currentPopup, aMouseX, aMouseY)) { + bool rollup = true; + if (aIsWheel) { + rollup = rollupListener->ShouldRollupOnMouseWheelEvent(); + retVal = rollupListener->ShouldConsumeOnMouseWheelEvent(); + } + // if we're dealing with menus, we probably have submenus and + // we don't want to rollup if the click is in a parent menu of + // the current submenu + uint32_t popupsToRollup = UINT32_MAX; + if (!aAlwaysRollup) { + AutoTArray<nsIWidget*, 5> widgetChain; + uint32_t sameTypeCount = + rollupListener->GetSubmenuWidgetChain(&widgetChain); + for (unsigned long i = 0; i < widgetChain.Length(); ++i) { + nsIWidget* widget = widgetChain[i]; + auto* currWindow = (GdkWindow*)widget->GetNativeData(NS_NATIVE_WINDOW); + if (is_mouse_in_window(currWindow, aMouseX, aMouseY)) { + // don't roll up if the mouse event occurred within a + // menu of the same type. If the mouse event occurred + // in a menu higher than that, roll up, but pass the + // number of popups to Rollup so that only those of the + // same type close up. + if (i < sameTypeCount) { + rollup = false; + } else { + popupsToRollup = sameTypeCount; + } + break; + } + } // foreach parent menu widget + } // if rollup listener knows about menus + + // if we've determined that we should still rollup, do it. + bool usePoint = !aIsWheel && !aAlwaysRollup; + IntPoint point; + if (usePoint) { + LayoutDeviceIntPoint p = GdkEventCoordsToDevicePixels(aMouseX, aMouseY); + point = p.ToUnknownPoint(); + } + if (rollup && + rollupListener->Rollup(popupsToRollup, true, + usePoint ? &point : nullptr, nullptr)) { + retVal = true; + } + } + return retVal; +} + +/* static */ +bool nsWindow::DragInProgress(void) { + nsCOMPtr<nsIDragService> dragService = do_GetService(kCDragServiceCID); + + if (!dragService) return false; + + nsCOMPtr<nsIDragSession> currentDragSession; + dragService->GetCurrentSession(getter_AddRefs(currentDragSession)); + + return currentDragSession != nullptr; +} + +static bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX, + gdouble aMouseY) { + gint x = 0; + gint y = 0; + gint w, h; + + gint offsetX = 0; + gint offsetY = 0; + + GdkWindow* window = aWindow; + + while (window) { + gint tmpX = 0; + gint tmpY = 0; + + gdk_window_get_position(window, &tmpX, &tmpY); + GtkWidget* widget = get_gtk_widget_for_gdk_window(window); + + // if this is a window, compute x and y given its origin and our + // offset + if (GTK_IS_WINDOW(widget)) { + x = tmpX + offsetX; + y = tmpY + offsetY; + break; + } + + offsetX += tmpX; + offsetY += tmpY; + window = gdk_window_get_parent(window); + } + + w = gdk_window_get_width(aWindow); + h = gdk_window_get_height(aWindow); + + if (aMouseX > x && aMouseX < x + w && aMouseY > y && aMouseY < y + h) + return true; + + return false; +} + +static nsWindow* get_window_for_gtk_widget(GtkWidget* widget) { + gpointer user_data = g_object_get_data(G_OBJECT(widget), "nsWindow"); + + return static_cast<nsWindow*>(user_data); +} + +static nsWindow* get_window_for_gdk_window(GdkWindow* window) { + gpointer user_data = g_object_get_data(G_OBJECT(window), "nsWindow"); + + return static_cast<nsWindow*>(user_data); +} + +static GtkWidget* get_gtk_widget_for_gdk_window(GdkWindow* window) { + gpointer user_data = nullptr; + gdk_window_get_user_data(window, &user_data); + + return GTK_WIDGET(user_data); +} + +static GdkCursor* get_gtk_cursor(nsCursor aCursor) { + GdkCursor* gdkcursor = nullptr; + uint8_t newType = 0xff; + + if ((gdkcursor = gCursorCache[aCursor])) { + return gdkcursor; + } + + GdkDisplay* defaultDisplay = gdk_display_get_default(); + + // The strategy here is to use standard GDK cursors, and, if not available, + // load by standard name with gdk_cursor_new_from_name. + // Spec is here: http://www.freedesktop.org/wiki/Specifications/cursor-spec/ + switch (aCursor) { + case eCursor_standard: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_LEFT_PTR); + break; + case eCursor_wait: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_WATCH); + break; + case eCursor_select: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_XTERM); + break; + case eCursor_hyperlink: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_HAND2); + break; + case eCursor_n_resize: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_TOP_SIDE); + break; + case eCursor_s_resize: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_BOTTOM_SIDE); + break; + case eCursor_w_resize: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_LEFT_SIDE); + break; + case eCursor_e_resize: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_RIGHT_SIDE); + break; + case eCursor_nw_resize: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_TOP_LEFT_CORNER); + break; + case eCursor_se_resize: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_BOTTOM_RIGHT_CORNER); + break; + case eCursor_ne_resize: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_TOP_RIGHT_CORNER); + break; + case eCursor_sw_resize: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_BOTTOM_LEFT_CORNER); + break; + case eCursor_crosshair: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_CROSSHAIR); + break; + case eCursor_move: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_FLEUR); + break; + case eCursor_help: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_QUESTION_ARROW); + break; + case eCursor_copy: // CSS3 + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "copy"); + if (!gdkcursor) newType = MOZ_CURSOR_COPY; + break; + case eCursor_alias: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "alias"); + if (!gdkcursor) newType = MOZ_CURSOR_ALIAS; + break; + case eCursor_context_menu: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "context-menu"); + if (!gdkcursor) newType = MOZ_CURSOR_CONTEXT_MENU; + break; + case eCursor_cell: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_PLUS); + break; + // Those two aren’t standardized. Trying both KDE’s and GNOME’s names + case eCursor_grab: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "openhand"); + if (!gdkcursor) newType = MOZ_CURSOR_HAND_GRAB; + break; + case eCursor_grabbing: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "closedhand"); + if (!gdkcursor) + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "grabbing"); + if (!gdkcursor) newType = MOZ_CURSOR_HAND_GRABBING; + break; + case eCursor_spinning: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "progress"); + if (!gdkcursor) newType = MOZ_CURSOR_SPINNING; + break; + case eCursor_zoom_in: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "zoom-in"); + if (!gdkcursor) newType = MOZ_CURSOR_ZOOM_IN; + break; + case eCursor_zoom_out: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "zoom-out"); + if (!gdkcursor) newType = MOZ_CURSOR_ZOOM_OUT; + break; + case eCursor_not_allowed: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "not-allowed"); + if (!gdkcursor) // nonstandard, yet common + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "crossed_circle"); + if (!gdkcursor) newType = MOZ_CURSOR_NOT_ALLOWED; + break; + case eCursor_no_drop: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "no-drop"); + if (!gdkcursor) // this nonstandard sequence makes it work on KDE and + // GNOME + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "forbidden"); + if (!gdkcursor) + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "circle"); + if (!gdkcursor) newType = MOZ_CURSOR_NOT_ALLOWED; + break; + case eCursor_vertical_text: + newType = MOZ_CURSOR_VERTICAL_TEXT; + break; + case eCursor_all_scroll: + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_FLEUR); + break; + case eCursor_nesw_resize: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "size_bdiag"); + if (!gdkcursor) newType = MOZ_CURSOR_NESW_RESIZE; + break; + case eCursor_nwse_resize: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "size_fdiag"); + if (!gdkcursor) newType = MOZ_CURSOR_NWSE_RESIZE; + break; + case eCursor_ns_resize: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_SB_V_DOUBLE_ARROW); + break; + case eCursor_ew_resize: + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_SB_H_DOUBLE_ARROW); + break; + // Here, two better fitting cursors exist in some cursor themes. Try those + // first + case eCursor_row_resize: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "split_v"); + if (!gdkcursor) + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_SB_V_DOUBLE_ARROW); + break; + case eCursor_col_resize: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "split_h"); + if (!gdkcursor) + gdkcursor = + gdk_cursor_new_for_display(defaultDisplay, GDK_SB_H_DOUBLE_ARROW); + break; + case eCursor_none: + newType = MOZ_CURSOR_NONE; + break; + default: + NS_ASSERTION(aCursor, "Invalid cursor type"); + gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_LEFT_PTR); + break; + } + + // If by now we don't have a xcursor, this means we have to make a custom + // one. First, we try creating a named cursor based on the hash of our + // custom bitmap, as libXcursor has some magic to convert bitmapped cursors + // to themed cursors + if (newType != 0xFF && GtkCursors[newType].hash) { + gdkcursor = + gdk_cursor_new_from_name(defaultDisplay, GtkCursors[newType].hash); + } + + // If we still don't have a xcursor, we now really create a bitmap cursor + if (newType != 0xff && !gdkcursor) { + GdkPixbuf* cursor_pixbuf = + gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 32, 32); + if (!cursor_pixbuf) return nullptr; + + guchar* data = gdk_pixbuf_get_pixels(cursor_pixbuf); + + // Read data from GtkCursors and compose RGBA surface from 1bit bitmap and + // mask GtkCursors bits and mask are 32x32 monochrome bitmaps (1 bit for + // each pixel) so it's 128 byte array (4 bytes for are one bitmap row and + // there are 32 rows here). + const unsigned char* bits = GtkCursors[newType].bits; + const unsigned char* mask_bits = GtkCursors[newType].mask_bits; + + for (int i = 0; i < 128; i++) { + char bit = *bits++; + char mask = *mask_bits++; + for (int j = 0; j < 8; j++) { + unsigned char pix = ~(((bit >> j) & 0x01) * 0xff); + *data++ = pix; + *data++ = pix; + *data++ = pix; + *data++ = (((mask >> j) & 0x01) * 0xff); + } + } + + gdkcursor = gdk_cursor_new_from_pixbuf( + gdk_display_get_default(), cursor_pixbuf, GtkCursors[newType].hot_x, + GtkCursors[newType].hot_y); + + g_object_unref(cursor_pixbuf); + } + + gCursorCache[aCursor] = gdkcursor; + + return gdkcursor; +} + +// gtk callbacks + +void draw_window_of_widget(GtkWidget* widget, GdkWindow* aWindow, cairo_t* cr) { + if (gtk_cairo_should_draw_window(cr, aWindow)) { + RefPtr<nsWindow> window = get_window_for_gdk_window(aWindow); + if (!window) { + NS_WARNING("Cannot get nsWindow from GtkWidget"); + } else { + cairo_save(cr); + gtk_cairo_transform_to_window(cr, widget, aWindow); + // TODO - window->OnExposeEvent() can destroy this or other windows, + // do we need to handle it somehow? + window->OnExposeEvent(cr); + cairo_restore(cr); + } + } + + GList* children = gdk_window_get_children(aWindow); + GList* child = children; + while (child) { + GdkWindow* window = GDK_WINDOW(child->data); + gpointer windowWidget; + gdk_window_get_user_data(window, &windowWidget); + if (windowWidget == widget) { + draw_window_of_widget(widget, window, cr); + } + child = g_list_next(child); + } + g_list_free(children); +} + +/* static */ +gboolean expose_event_cb(GtkWidget* widget, cairo_t* cr) { + draw_window_of_widget(widget, gtk_widget_get_window(widget), cr); + + // A strong reference is already held during "draw" signal emission, + // but GTK+ 3.4 wants the object to live a little longer than that + // (bug 1225970). + g_object_ref(widget); + g_idle_add( + [](gpointer data) -> gboolean { + g_object_unref(data); + return G_SOURCE_REMOVE; + }, + widget); + + return FALSE; +} + +static gboolean configure_event_cb(GtkWidget* widget, + GdkEventConfigure* event) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return FALSE; + } + + return window->OnConfigureEvent(widget, event); +} + +static void container_unrealize_cb(GtkWidget* widget) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + + window->OnContainerUnrealize(); +} + +static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + + window->OnSizeAllocate(allocation); +} + +static void toplevel_window_size_allocate_cb(GtkWidget* widget, + GtkAllocation* allocation) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + + window->UpdateTopLevelOpaqueRegion(); +} + +static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return FALSE; + } + + window->OnDeleteEvent(); + + return TRUE; +} + +static gboolean enter_notify_event_cb(GtkWidget* widget, + GdkEventCrossing* event) { + RefPtr<nsWindow> window = get_window_for_gdk_window(event->window); + if (!window) { + return TRUE; + } + + window->OnEnterNotifyEvent(event); + + return TRUE; +} + +static gboolean leave_notify_event_cb(GtkWidget* widget, + GdkEventCrossing* event) { + if (is_parent_grab_leave(event)) { + return TRUE; + } + + // bug 369599: Suppress LeaveNotify events caused by pointer grabs to + // avoid generating spurious mouse exit events. + auto x = gint(event->x_root); + auto y = gint(event->y_root); + GdkDisplay* display = gtk_widget_get_display(widget); + GdkWindow* winAtPt = gdk_display_get_window_at_pointer(display, &x, &y); + if (winAtPt == event->window) { + return TRUE; + } + + RefPtr<nsWindow> window = get_window_for_gdk_window(event->window); + if (!window) return TRUE; + + window->OnLeaveNotifyEvent(event); + + return TRUE; +} + +static nsWindow* GetFirstNSWindowForGDKWindow(GdkWindow* aGdkWindow) { + nsWindow* window; + while (!(window = get_window_for_gdk_window(aGdkWindow))) { + // The event has bubbled to the moz_container widget as passed into each + // caller's *widget parameter, but its corresponding nsWindow is an ancestor + // of the window that we need. Instead, look at event->window and find the + // first ancestor nsWindow of it because event->window may be in a plugin. + aGdkWindow = gdk_window_get_parent(aGdkWindow); + if (!aGdkWindow) { + window = nullptr; + break; + } + } + return window; +} + +static gboolean motion_notify_event_cb(GtkWidget* widget, + GdkEventMotion* event) { + UpdateLastInputEventTime(event); + + nsWindow* window = GetFirstNSWindowForGDKWindow(event->window); + if (!window) return FALSE; + + window->OnMotionNotifyEvent(event); + + return TRUE; +} + +static gboolean button_press_event_cb(GtkWidget* widget, + GdkEventButton* event) { + UpdateLastInputEventTime(event); + + nsWindow* window = GetFirstNSWindowForGDKWindow(event->window); + if (!window) return FALSE; + + window->OnButtonPressEvent(event); + + return TRUE; +} + +static gboolean button_release_event_cb(GtkWidget* widget, + GdkEventButton* event) { + UpdateLastInputEventTime(event); + + nsWindow* window = GetFirstNSWindowForGDKWindow(event->window); + if (!window) return FALSE; + + window->OnButtonReleaseEvent(event); + + return TRUE; +} + +static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) return FALSE; + + window->OnContainerFocusInEvent(event); + + return FALSE; +} + +static gboolean focus_out_event_cb(GtkWidget* widget, GdkEventFocus* event) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) return FALSE; + + window->OnContainerFocusOutEvent(event); + + return FALSE; +} + +#ifdef MOZ_X11 +// For long-lived popup windows that don't really take focus themselves but +// may have elements that accept keyboard input when the parent window is +// active, focus is handled specially. These windows include noautohide +// panels. (This special handling is not necessary for temporary popups where +// the keyboard is grabbed.) +// +// Mousing over or clicking on these windows should not cause them to steal +// focus from their parent windows, so, the input field of WM_HINTS is set to +// False to request that the window manager not set the input focus to this +// window. http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.7 +// +// However, these windows can still receive WM_TAKE_FOCUS messages from the +// window manager, so they can still detect when the user has indicated that +// they wish to direct keyboard input at these windows. When the window +// manager offers focus to these windows (after a mouse over or click, for +// example), a request to make the parent window active is issued. When the +// parent window becomes active, keyboard events will be received. + +static GdkFilterReturn popup_take_focus_filter(GdkXEvent* gdk_xevent, + GdkEvent* event, gpointer data) { + auto* xevent = static_cast<XEvent*>(gdk_xevent); + if (xevent->type != ClientMessage) return GDK_FILTER_CONTINUE; + + XClientMessageEvent& xclient = xevent->xclient; + if (xclient.message_type != gdk_x11_get_xatom_by_name("WM_PROTOCOLS")) + return GDK_FILTER_CONTINUE; + + Atom atom = xclient.data.l[0]; + if (atom != gdk_x11_get_xatom_by_name("WM_TAKE_FOCUS")) + return GDK_FILTER_CONTINUE; + + guint32 timestamp = xclient.data.l[1]; + + GtkWidget* widget = get_gtk_widget_for_gdk_window(event->any.window); + if (!widget) return GDK_FILTER_CONTINUE; + + GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(widget)); + if (!parent) return GDK_FILTER_CONTINUE; + + if (gtk_window_is_active(parent)) + return GDK_FILTER_REMOVE; // leave input focus on the parent + + GdkWindow* parent_window = gtk_widget_get_window(GTK_WIDGET(parent)); + if (!parent_window) return GDK_FILTER_CONTINUE; + + // In case the parent has not been deconified. + gdk_window_show_unraised(parent_window); + + // Request focus on the parent window. + // Use gdk_window_focus rather than gtk_window_present to avoid + // raising the parent window. + gdk_window_focus(parent_window, timestamp); + return GDK_FILTER_REMOVE; +} +#endif /* MOZ_X11 */ + +static gboolean key_press_event_cb(GtkWidget* widget, GdkEventKey* event) { + LOG(("key_press_event_cb\n")); + + UpdateLastInputEventTime(event); + + // find the window with focus and dispatch this event to that widget + nsWindow* window = get_window_for_gtk_widget(widget); + if (!window) return FALSE; + + RefPtr<nsWindow> focusWindow = gFocusWindow ? gFocusWindow : window; + +#ifdef MOZ_X11 + // Keyboard repeat can cause key press events to queue up when there are + // slow event handlers (bug 301029). Throttle these events by removing + // consecutive pending duplicate KeyPress events to the same window. + // We use the event time of the last one. + // Note: GDK calls XkbSetDetectableAutorepeat so that KeyRelease events + // are generated only when the key is physically released. +# define NS_GDKEVENT_MATCH_MASK 0x1FFF // GDK_SHIFT_MASK .. GDK_BUTTON5_MASK + // Our headers undefine X11 KeyPress - let's redefine it here. +# ifndef KeyPress +# define KeyPress 2 +# endif + GdkDisplay* gdkDisplay = gtk_widget_get_display(widget); + if (GDK_IS_X11_DISPLAY(gdkDisplay)) { + Display* dpy = GDK_DISPLAY_XDISPLAY(gdkDisplay); + while (XPending(dpy)) { + XEvent next_event; + XPeekEvent(dpy, &next_event); + GdkWindow* nextGdkWindow = + gdk_x11_window_lookup_for_display(gdkDisplay, next_event.xany.window); + if (nextGdkWindow != event->window || next_event.type != KeyPress || + next_event.xkey.keycode != event->hardware_keycode || + next_event.xkey.state != (event->state & NS_GDKEVENT_MATCH_MASK)) { + break; + } + XNextEvent(dpy, &next_event); + event->time = next_event.xkey.time; + } + } +#endif + + return focusWindow->OnKeyPressEvent(event); +} + +static gboolean key_release_event_cb(GtkWidget* widget, GdkEventKey* event) { + LOG(("key_release_event_cb\n")); + + UpdateLastInputEventTime(event); + + // find the window with focus and dispatch this event to that widget + nsWindow* window = get_window_for_gtk_widget(widget); + if (!window) return FALSE; + + RefPtr<nsWindow> focusWindow = gFocusWindow ? gFocusWindow : window; + + return focusWindow->OnKeyReleaseEvent(event); +} + +static gboolean property_notify_event_cb(GtkWidget* aWidget, + GdkEventProperty* aEvent) { + RefPtr<nsWindow> window = get_window_for_gdk_window(aEvent->window); + if (!window) return FALSE; + + return window->OnPropertyNotifyEvent(aWidget, aEvent); +} + +static gboolean scroll_event_cb(GtkWidget* widget, GdkEventScroll* event) { + nsWindow* window = GetFirstNSWindowForGDKWindow(event->window); + if (!window) return FALSE; + + window->OnScrollEvent(event); + + return TRUE; +} + +static void hierarchy_changed_cb(GtkWidget* widget, + GtkWidget* previous_toplevel) { + GtkWidget* toplevel = gtk_widget_get_toplevel(widget); + GdkWindowState old_window_state = GDK_WINDOW_STATE_WITHDRAWN; + GdkEventWindowState event; + + event.new_window_state = GDK_WINDOW_STATE_WITHDRAWN; + + if (GTK_IS_WINDOW(previous_toplevel)) { + g_signal_handlers_disconnect_by_func( + previous_toplevel, FuncToGpointer(window_state_event_cb), widget); + GdkWindow* win = gtk_widget_get_window(previous_toplevel); + if (win) { + old_window_state = gdk_window_get_state(win); + } + } + + if (GTK_IS_WINDOW(toplevel)) { + g_signal_connect_swapped(toplevel, "window-state-event", + G_CALLBACK(window_state_event_cb), widget); + GdkWindow* win = gtk_widget_get_window(toplevel); + if (win) { + event.new_window_state = gdk_window_get_state(win); + } + } + + event.changed_mask = + static_cast<GdkWindowState>(old_window_state ^ event.new_window_state); + + if (event.changed_mask) { + event.type = GDK_WINDOW_STATE; + event.window = nullptr; + event.send_event = TRUE; + window_state_event_cb(widget, &event); + } +} + +static gboolean window_state_event_cb(GtkWidget* widget, + GdkEventWindowState* event) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) return FALSE; + + window->OnWindowStateEvent(widget, event); + + return FALSE; +} + +static void settings_changed_cb(GtkSettings* settings, GParamSpec* pspec, + nsWindow* data) { + if (sIgnoreChangedSettings) { + return; + } + RefPtr<nsWindow> window = data; + window->ThemeChanged(); +} + +static void settings_xft_dpi_changed_cb(GtkSettings* gtk_settings, + GParamSpec* pspec, nsWindow* data) { + RefPtr<nsWindow> window = data; + window->OnDPIChanged(); + // Even though the window size in screen pixels has not changed, + // nsViewManager stores the dimensions in app units. + // DispatchResized() updates those. + window->DispatchResized(); +} + +static void check_resize_cb(GtkContainer* container, gpointer user_data) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(GTK_WIDGET(container)); + if (!window) { + return; + } + window->OnCheckResize(); +} + +static void screen_composited_changed_cb(GdkScreen* screen, + gpointer user_data) { + // This callback can run before gfxPlatform::Init() in rare + // cases involving the profile manager. When this happens, + // we have no reason to reset any compositors as graphics + // hasn't been initialized yet. + if (GPUProcessManager::Get()) { + GPUProcessManager::Get()->ResetCompositors(); + } +} + +static void widget_composited_changed_cb(GtkWidget* widget, + gpointer user_data) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + window->OnCompositedChanged(); +} + +static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec, + gpointer aPointer) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + window->OnScaleChanged(&allocation); +} + +static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent) { + UpdateLastInputEventTime(aEvent); + + nsWindow* window = GetFirstNSWindowForGDKWindow(aEvent->window); + if (!window) { + return FALSE; + } + + return window->OnTouchEvent(aEvent); +} + +// This function called generic because there is no signal specific to touchpad +// pinch events. +static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent) { + if (aEvent->type != GDK_TOUCHPAD_PINCH) { + return FALSE; + } + // Using reinterpret_cast because the touchpad_pinch field of GdkEvent is not + // available in GTK+ versions lower than v3.18 + GdkEventTouchpadPinch* event = + reinterpret_cast<GdkEventTouchpadPinch*>(aEvent); + + nsWindow* window = GetFirstNSWindowForGDKWindow(event->window); + + if (!window) { + return FALSE; + } + return window->OnTouchpadPinchEvent(event); +} + +////////////////////////////////////////////////////////////////////// +// These are all of our drag and drop operations + +void nsWindow::InitDragEvent(WidgetDragEvent& aEvent) { + // set the keyboard modifiers + guint modifierState = KeymapWrapper::GetCurrentModifierState(); + KeymapWrapper::InitInputEvent(aEvent, modifierState); +} + +gboolean WindowDragMotionHandler(GtkWidget* aWidget, + GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + gint aX, gint aY, guint aTime) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget); + if (!window) return FALSE; + + // figure out which internal widget this drag motion actually happened on + nscoord retx = 0; + nscoord rety = 0; + + GdkWindow* innerWindow = get_inner_gdk_window(gtk_widget_get_window(aWidget), + aX, aY, &retx, &rety); + RefPtr<nsWindow> innerMostWindow = get_window_for_gdk_window(innerWindow); + + if (!innerMostWindow) { + innerMostWindow = window; + } + + LOGDRAG(("nsWindow drag-motion signal for %p\n", (void*)innerMostWindow)); + + LayoutDeviceIntPoint point = window->GdkPointToDevicePixels({retx, rety}); + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + return dragService->ScheduleMotionEvent(innerMostWindow, aDragContext, + aWaylandDragContext, point, aTime); +} + +static gboolean drag_motion_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, guint aTime, gpointer aData) { + return WindowDragMotionHandler(aWidget, aDragContext, nullptr, aX, aY, aTime); +} + +void WindowDragLeaveHandler(GtkWidget* aWidget) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget); + if (!window) return; + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + + nsWindow* mostRecentDragWindow = dragService->GetMostRecentDestWindow(); + if (!mostRecentDragWindow) { + // This can happen when the target will not accept a drop. A GTK drag + // source sends the leave message to the destination before the + // drag-failed signal on the source widget, but the leave message goes + // via the X server, and so doesn't get processed at least until the + // event loop runs again. + return; + } + + GtkWidget* mozContainer = mostRecentDragWindow->GetMozContainerWidget(); + if (aWidget != mozContainer) { + // When the drag moves between widgets, GTK can send leave signal for + // the old widget after the motion or drop signal for the new widget. + // We'll send the leave event when the motion or drop event is run. + return; + } + + LOGDRAG(("nsWindow drag-leave signal for %p\n", (void*)mostRecentDragWindow)); + + dragService->ScheduleLeaveEvent(); +} + +static void drag_leave_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, guint aTime, + gpointer aData) { + WindowDragLeaveHandler(aWidget); +} + +gboolean WindowDragDropHandler(GtkWidget* aWidget, GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + gint aX, gint aY, guint aTime) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget); + if (!window) return FALSE; + + // figure out which internal widget this drag motion actually happened on + nscoord retx = 0; + nscoord rety = 0; + + GdkWindow* innerWindow = get_inner_gdk_window(gtk_widget_get_window(aWidget), + aX, aY, &retx, &rety); + RefPtr<nsWindow> innerMostWindow = get_window_for_gdk_window(innerWindow); + + if (!innerMostWindow) { + innerMostWindow = window; + } + + LOGDRAG(("nsWindow drag-drop signal for %p\n", (void*)innerMostWindow)); + + LayoutDeviceIntPoint point = window->GdkPointToDevicePixels({retx, rety}); + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + return dragService->ScheduleDropEvent(innerMostWindow, aDragContext, + aWaylandDragContext, point, aTime); +} + +static gboolean drag_drop_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, guint aTime, gpointer aData) { + return WindowDragDropHandler(aWidget, aDragContext, nullptr, aX, aY, aTime); +} + +static void drag_data_received_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, + GtkSelectionData* aSelectionData, + guint aInfo, guint aTime, + gpointer aData) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget); + if (!window) return; + + window->OnDragDataReceivedEvent(aWidget, aDragContext, aX, aY, aSelectionData, + aInfo, aTime, aData); +} + +static nsresult initialize_prefs(void) { + gRaiseWindows = + Preferences::GetBool("mozilla.widget.raise-on-setfocus", true); + gUseWaylandVsync = + Preferences::GetBool("widget.wayland_vsync.enabled", false); + gUseWaylandUseOpaqueRegion = + Preferences::GetBool("widget.wayland.use-opaque-region", false); + + if (Preferences::HasUserValue("widget.use-aspect-ratio")) { + gUseAspectRatio = Preferences::GetBool("widget.use-aspect-ratio", true); + } else { + static const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + gUseAspectRatio = + currentDesktop ? (strstr(currentDesktop, "GNOME") != nullptr) : false; + } + + return NS_OK; +} + +static GdkWindow* get_inner_gdk_window(GdkWindow* aWindow, gint x, gint y, + gint* retx, gint* rety) { + gint cx, cy, cw, ch; + GList* children = gdk_window_peek_children(aWindow); + for (GList* child = g_list_last(children); child; + child = g_list_previous(child)) { + auto* childWindow = (GdkWindow*)child->data; + if (get_window_for_gdk_window(childWindow)) { + gdk_window_get_geometry(childWindow, &cx, &cy, &cw, &ch); + if ((cx < x) && (x < (cx + cw)) && (cy < y) && (y < (cy + ch)) && + gdk_window_is_visible(childWindow)) { + return get_inner_gdk_window(childWindow, x - cx, y - cy, retx, rety); + } + } + } + *retx = x; + *rety = y; + return aWindow; +} + +static int is_parent_ungrab_enter(GdkEventCrossing* aEvent) { + return (GDK_CROSSING_UNGRAB == aEvent->mode) && + ((GDK_NOTIFY_ANCESTOR == aEvent->detail) || + (GDK_NOTIFY_VIRTUAL == aEvent->detail)); +} + +static int is_parent_grab_leave(GdkEventCrossing* aEvent) { + return (GDK_CROSSING_GRAB == aEvent->mode) && + ((GDK_NOTIFY_ANCESTOR == aEvent->detail) || + (GDK_NOTIFY_VIRTUAL == aEvent->detail)); +} + +#ifdef ACCESSIBILITY +void nsWindow::CreateRootAccessible() { + if (mIsTopLevel && !mRootAccessible) { + LOG(("nsWindow:: Create Toplevel Accessibility\n")); + mRootAccessible = GetRootAccessible(); + } +} + +void nsWindow::DispatchEventToRootAccessible(uint32_t aEventType) { + if (!a11y::ShouldA11yBeEnabled()) { + return; + } + + nsAccessibilityService* accService = GetOrCreateAccService(); + if (!accService) { + return; + } + + // Get the root document accessible and fire event to it. + a11y::Accessible* acc = GetRootAccessible(); + if (acc) { + accService->FireAccessibleEvent(aEventType, acc); + } +} + +void nsWindow::DispatchActivateEventAccessible(void) { + DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE); +} + +void nsWindow::DispatchDeactivateEventAccessible(void) { + DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE); +} + +void nsWindow::DispatchMaximizeEventAccessible(void) { + DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE); +} + +void nsWindow::DispatchMinimizeEventAccessible(void) { + DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE); +} + +void nsWindow::DispatchRestoreEventAccessible(void) { + DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_RESTORE); +} + +#endif /* #ifdef ACCESSIBILITY */ + +void nsWindow::SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) { + if (!mIMContext) { + return; + } + mIMContext->SetInputContext(this, &aContext, &aAction); +} + +InputContext nsWindow::GetInputContext() { + InputContext context; + if (!mIMContext) { + context.mIMEState.mEnabled = IMEEnabled::Disabled; + context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED; + } else { + context = mIMContext->GetInputContext(); + } + return context; +} + +TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() { + if (NS_WARN_IF(!mIMContext)) { + return nullptr; + } + return mIMContext; +} + +void nsWindow::GetEditCommandsRemapped(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + nsTArray<CommandInt>& aCommands, + uint32_t aGeckoKeyCode, + uint32_t aNativeKeyCode) { + // If aEvent.mNativeKeyEvent is nullptr, the event was created by chrome + // script. In such case, we shouldn't expose the OS settings to it. + // So, just ignore such events here. + if (!aEvent.mNativeKeyEvent) { + return; + } + WidgetKeyboardEvent modifiedEvent(aEvent); + modifiedEvent.mKeyCode = aGeckoKeyCode; + static_cast<GdkEventKey*>(modifiedEvent.mNativeKeyEvent)->keyval = + aNativeKeyCode; + + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + keyBindings->GetEditCommands(modifiedEvent, aCommands); +} + +bool nsWindow::GetEditCommands(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + nsTArray<CommandInt>& aCommands) { + // Validate the arguments. + if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) { + return false; + } + + if (aEvent.mKeyCode >= NS_VK_LEFT && aEvent.mKeyCode <= NS_VK_DOWN) { + // Check if we're targeting content with vertical writing mode, + // and if so remap the arrow keys. + // XXX This may be expensive. + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + this); + nsEventStatus status; + DispatchEvent(&querySelectedTextEvent, status); + + if (querySelectedTextEvent.FoundSelection() && + querySelectedTextEvent.mReply->mWritingMode.IsVertical()) { + uint32_t geckoCode = 0; + uint32_t gdkCode = 0; + switch (aEvent.mKeyCode) { + case NS_VK_LEFT: + if (querySelectedTextEvent.mReply->mWritingMode.IsVerticalLR()) { + geckoCode = NS_VK_UP; + gdkCode = GDK_Up; + } else { + geckoCode = NS_VK_DOWN; + gdkCode = GDK_Down; + } + break; + + case NS_VK_RIGHT: + if (querySelectedTextEvent.mReply->mWritingMode.IsVerticalLR()) { + geckoCode = NS_VK_DOWN; + gdkCode = GDK_Down; + } else { + geckoCode = NS_VK_UP; + gdkCode = GDK_Up; + } + break; + + case NS_VK_UP: + geckoCode = NS_VK_LEFT; + gdkCode = GDK_Left; + break; + + case NS_VK_DOWN: + geckoCode = NS_VK_RIGHT; + gdkCode = GDK_Right; + break; + } + + GetEditCommandsRemapped(aType, aEvent, aCommands, geckoCode, gdkCode); + return true; + } + } + + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + keyBindings->GetEditCommands(aEvent, aCommands); + return true; +} + +already_AddRefed<DrawTarget> nsWindow::StartRemoteDrawingInRegion( + LayoutDeviceIntRegion& aInvalidRegion, BufferMode* aBufferMode) { + return mSurfaceProvider.StartRemoteDrawingInRegion(aInvalidRegion, + aBufferMode); +} + +void nsWindow::EndRemoteDrawingInRegion( + DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + mSurfaceProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion); +} + +// Code shared begin BeginMoveDrag and BeginResizeDrag +bool nsWindow::GetDragInfo(WidgetMouseEvent* aMouseEvent, GdkWindow** aWindow, + gint* aButton, gint* aRootX, gint* aRootY) { + if (aMouseEvent->mButton != MouseButton::ePrimary) { + // we can only begin a move drag with the left mouse button + return false; + } + *aButton = 1; + + // get the gdk window for this widget + GdkWindow* gdk_window = mGdkWindow; + if (!gdk_window) { + return false; + } +#ifdef DEBUG + // GDK_IS_WINDOW(...) expands to a statement-expression, and + // statement-expressions are not allowed in template-argument lists. So we + // have to make the MOZ_ASSERT condition indirect. + if (!GDK_IS_WINDOW(gdk_window)) { + MOZ_ASSERT(false, "must really be window"); + } +#endif + + // find the top-level window + gdk_window = gdk_window_get_toplevel(gdk_window); + MOZ_ASSERT(gdk_window, "gdk_window_get_toplevel should not return null"); + *aWindow = gdk_window; + + if (!aMouseEvent->mWidget) { + return false; + } + + if (mIsX11Display) { + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=789054 + // To avoid crashes disable double-click on WM without _NET_WM_MOVERESIZE. + // See _should_perform_ewmh_drag() at gdkwindow-x11.c + GdkScreen* screen = gdk_window_get_screen(gdk_window); + GdkAtom atom = gdk_atom_intern("_NET_WM_MOVERESIZE", FALSE); + if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) { + static unsigned int lastTimeStamp = 0; + if (lastTimeStamp != aMouseEvent->mTime) { + lastTimeStamp = aMouseEvent->mTime; + } else { + return false; + } + } + } + + // FIXME: It would be nice to have the widget position at the time + // of the event, but it's relatively unlikely that the widget has + // moved since the mousedown. (On the other hand, it's quite likely + // that the mouse has moved, which is why we use the mouse position + // from the event.) + LayoutDeviceIntPoint offset = aMouseEvent->mWidget->WidgetToScreenOffset(); + *aRootX = aMouseEvent->mRefPoint.x + offset.x; + *aRootY = aMouseEvent->mRefPoint.y + offset.y; + + return true; +} + +nsresult nsWindow::BeginResizeDrag(WidgetGUIEvent* aEvent, int32_t aHorizontal, + int32_t aVertical) { + NS_ENSURE_ARG_POINTER(aEvent); + + if (aEvent->mClass != eMouseEventClass) { + // you can only begin a resize drag with a mouse event + return NS_ERROR_INVALID_ARG; + } + + GdkWindow* gdk_window; + gint button, screenX, screenY; + if (!GetDragInfo(aEvent->AsMouseEvent(), &gdk_window, &button, &screenX, + &screenY)) { + return NS_ERROR_FAILURE; + } + + // work out what GdkWindowEdge we're talking about + GdkWindowEdge window_edge; + if (aVertical < 0) { + if (aHorizontal < 0) { + window_edge = GDK_WINDOW_EDGE_NORTH_WEST; + } else if (aHorizontal == 0) { + window_edge = GDK_WINDOW_EDGE_NORTH; + } else { + window_edge = GDK_WINDOW_EDGE_NORTH_EAST; + } + } else if (aVertical == 0) { + if (aHorizontal < 0) { + window_edge = GDK_WINDOW_EDGE_WEST; + } else if (aHorizontal == 0) { + return NS_ERROR_INVALID_ARG; + } else { + window_edge = GDK_WINDOW_EDGE_EAST; + } + } else { + if (aHorizontal < 0) { + window_edge = GDK_WINDOW_EDGE_SOUTH_WEST; + } else if (aHorizontal == 0) { + window_edge = GDK_WINDOW_EDGE_SOUTH; + } else { + window_edge = GDK_WINDOW_EDGE_SOUTH_EAST; + } + } + + // tell the window manager to start the resize + gdk_window_begin_resize_drag(gdk_window, window_edge, button, screenX, + screenY, aEvent->mTime); + + return NS_OK; +} + +nsIWidget::LayerManager* nsWindow::GetLayerManager( + PLayerTransactionChild* aShadowManager, LayersBackend aBackendHint, + LayerManagerPersistence aPersistence) { + if (mIsDestroyed) { + // Prevent external code from triggering the re-creation of the + // LayerManager/Compositor during shutdown. Just return what we currently + // have, which is most likely null. + return mLayerManager; + } + + return nsBaseWidget::GetLayerManager(aShadowManager, aBackendHint, + aPersistence); +} + +void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) { + if (delegate) { + mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate(); + MOZ_ASSERT(mCompositorWidgetDelegate, + "nsWindow::SetCompositorWidgetDelegate called with a " + "non-PlatformCompositorWidgetDelegate"); +#ifdef MOZ_WAYLAND + MaybeResumeCompositor(); +#endif + } else { + mCompositorWidgetDelegate = nullptr; + } +} + +void nsWindow::ClearCachedResources() { + if (mLayerManager && mLayerManager->GetBackendType() == + mozilla::layers::LayersBackend::LAYERS_BASIC) { + mLayerManager->ClearCachedResources(); + } + + GList* children = gdk_window_peek_children(mGdkWindow); + for (GList* list = children; list; list = list->next) { + nsWindow* window = get_window_for_gdk_window(GDK_WINDOW(list->data)); + if (window) { + window->ClearCachedResources(); + } + } +} + +/* nsWindow::UpdateClientOffsetFromCSDWindow() is designed to be called from + * nsWindow::OnConfigureEvent() when mContainer window is already positioned. + * + * It works only for CSD decorated GtkWindow. + */ +void nsWindow::UpdateClientOffsetFromCSDWindow() { + int x, y; + gdk_window_get_position(mGdkWindow, &x, &y); + + x = GdkCoordToDevicePixels(x); + y = GdkCoordToDevicePixels(y); + + if (mClientOffset.x != x || mClientOffset.y != y) { + mClientOffset = nsIntPoint(x, y); + + LOG(("nsWindow::UpdateClientOffsetFromCSDWindow [%p] %d, %d\n", (void*)this, + mClientOffset.x, mClientOffset.y)); + + // Send a WindowMoved notification. This ensures that BrowserParent + // picks up the new client offset and sends it to the child process + // if appropriate. + NotifyWindowMoved(mBounds.x, mBounds.y); + } +} + +nsresult nsWindow::SetNonClientMargins(LayoutDeviceIntMargin& aMargins) { + SetDrawsInTitlebar(aMargins.top == 0); + return NS_OK; +} + +void nsWindow::SetDrawsInTitlebar(bool aState) { + LOG(("nsWindow::SetDrawsInTitlebar() [%p] State %d mCSDSupportLevel %d\n", + (void*)this, aState, (int)mCSDSupportLevel)); + + if (!mShell || mCSDSupportLevel == CSD_SUPPORT_NONE || + aState == mDrawInTitlebar) { + return; + } + + if (mCSDSupportLevel == CSD_SUPPORT_SYSTEM) { + SetWindowDecoration(aState ? eBorderStyle_border : mBorderStyle); + } else if (mCSDSupportLevel == CSD_SUPPORT_CLIENT) { + LOG((" Using CSD mode\n")); + + /* Window manager does not support GDK_DECOR_BORDER, + * emulate it by CSD. + * + * gtk_window_set_titlebar() works on unrealized widgets only, + * we need to handle mShell carefully here. + * When CSD is enabled mGdkWindow is owned by mContainer which is good + * as we can't delete our mGdkWindow. To make mShell unrealized while + * mContainer is preserved we temporary reparent mContainer to an + * invisible GtkWindow. + */ + NativeShow(false); + + // Using GTK_WINDOW_POPUP rather than + // GTK_WINDOW_TOPLEVEL in the hope that POPUP results in less + // initialization and window manager interaction. + GtkWidget* tmpWindow = gtk_window_new(GTK_WINDOW_POPUP); + gtk_widget_realize(tmpWindow); + + gtk_widget_reparent(GTK_WIDGET(mContainer), tmpWindow); + gtk_widget_unrealize(GTK_WIDGET(mShell)); + + if (aState) { + // Add a hidden titlebar widget to trigger CSD, but disable the default + // titlebar. GtkFixed is a somewhat random choice for a simple unused + // widget. gtk_window_set_titlebar() takes ownership of the titlebar + // widget. + gtk_window_set_titlebar(GTK_WINDOW(mShell), gtk_fixed_new()); + } else { + gtk_window_set_titlebar(GTK_WINDOW(mShell), nullptr); + } + + /* A workaround for https://bugzilla.gnome.org/show_bug.cgi?id=791081 + * gtk_widget_realize() throws: + * "In pixman_region32_init_rect: Invalid rectangle passed" + * when mShell has default 1x1 size. + */ + GtkAllocation allocation = {0, 0, 0, 0}; + gtk_widget_get_preferred_width(GTK_WIDGET(mShell), nullptr, + &allocation.width); + gtk_widget_get_preferred_height(GTK_WIDGET(mShell), nullptr, + &allocation.height); + gtk_widget_size_allocate(GTK_WIDGET(mShell), &allocation); + + gtk_widget_realize(GTK_WIDGET(mShell)); + gtk_widget_reparent(GTK_WIDGET(mContainer), GTK_WIDGET(mShell)); + mNeedsShow = true; + NativeResize(); + + // Label mShell toplevel window so property_notify_event_cb callback + // can find its way home. + g_object_set_data(G_OBJECT(gtk_widget_get_window(mShell)), "nsWindow", + this); +#ifdef MOZ_X11 + SetCompositorHint(GTK_WIDGET_COMPOSIDED_ENABLED); +#endif + RefreshWindowClass(); + + gtk_widget_destroy(tmpWindow); + } + + mDrawInTitlebar = aState; + + if (mTransparencyBitmapForTitlebar) { + if (mDrawInTitlebar && mSizeState == nsSizeMode_Normal && !mIsTiled) { + UpdateTitlebarTransparencyBitmap(); + } else { + ClearTransparencyBitmap(); + } + } +} + +GtkWindow* nsWindow::GetCurrentTopmostWindow() { + GtkWindow* parentWindow = GTK_WINDOW(GetGtkWidget()); + GtkWindow* topmostParentWindow = nullptr; + while (parentWindow) { + topmostParentWindow = parentWindow; + parentWindow = gtk_window_get_transient_for(parentWindow); + } + return topmostParentWindow; +} + +gint nsWindow::GdkScaleFactor() { + // We depend on notify::scale-factor callback which is reliable for toplevel + // windows only, so don't use scale cache for popup windows. + if (mWindowType == eWindowType_toplevel && !mWindowScaleFactorChanged) { + return mWindowScaleFactor; + } + + GdkWindow* scaledGdkWindow = mGdkWindow; + if (!mIsX11Display) { + // For popup windows/dialogs with parent window we need to get scale factor + // of the topmost window. Otherwise the scale factor of the popup is + // not updated during it's hidden. + if (mWindowType == eWindowType_popup || mWindowType == eWindowType_dialog) { + // Get toplevel window for scale factor: + GtkWindow* topmostParentWindow = GetCurrentTopmostWindow(); + if (topmostParentWindow) { + scaledGdkWindow = + gtk_widget_get_window(GTK_WIDGET(topmostParentWindow)); + } else { + NS_WARNING("Popup/Dialog has no parent."); + } + // Fallback for windows which parent has been unrealized. + if (!scaledGdkWindow) { + scaledGdkWindow = mGdkWindow; + } + } + } + + // Available as of GTK 3.10+ + static auto sGdkWindowGetScaleFactorPtr = + (gint(*)(GdkWindow*))dlsym(RTLD_DEFAULT, "gdk_window_get_scale_factor"); + if (sGdkWindowGetScaleFactorPtr && scaledGdkWindow) { + mWindowScaleFactor = (*sGdkWindowGetScaleFactorPtr)(scaledGdkWindow); + mWindowScaleFactorChanged = false; + } else { + mWindowScaleFactor = ScreenHelperGTK::GetGTKMonitorScaleFactor(); + } + + return mWindowScaleFactor; +} + +gint nsWindow::DevicePixelsToGdkCoordRoundUp(int pixels) { + gint scale = GdkScaleFactor(); + return (pixels + scale - 1) / scale; +} + +gint nsWindow::DevicePixelsToGdkCoordRoundDown(int pixels) { + gint scale = GdkScaleFactor(); + return pixels / scale; +} + +GdkPoint nsWindow::DevicePixelsToGdkPointRoundDown(LayoutDeviceIntPoint point) { + gint scale = GdkScaleFactor(); + return {point.x / scale, point.y / scale}; +} + +GdkRectangle nsWindow::DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect rect) { + gint scale = GdkScaleFactor(); + int x = rect.x / scale; + int y = rect.y / scale; + int right = (rect.x + rect.width + scale - 1) / scale; + int bottom = (rect.y + rect.height + scale - 1) / scale; + return {x, y, right - x, bottom - y}; +} + +GdkRectangle nsWindow::DevicePixelsToGdkSizeRoundUp( + LayoutDeviceIntSize pixelSize) { + gint scale = GdkScaleFactor(); + gint width = (pixelSize.width + scale - 1) / scale; + gint height = (pixelSize.height + scale - 1) / scale; + return {0, 0, width, height}; +} + +int nsWindow::GdkCoordToDevicePixels(gint coord) { + return coord * GdkScaleFactor(); +} + +LayoutDeviceIntPoint nsWindow::GdkEventCoordsToDevicePixels(gdouble x, + gdouble y) { + gint scale = GdkScaleFactor(); + return LayoutDeviceIntPoint::Round(x * scale, y * scale); +} + +LayoutDeviceIntPoint nsWindow::GdkPointToDevicePixels(GdkPoint point) { + gint scale = GdkScaleFactor(); + return LayoutDeviceIntPoint(point.x * scale, point.y * scale); +} + +LayoutDeviceIntRect nsWindow::GdkRectToDevicePixels(GdkRectangle rect) { + gint scale = GdkScaleFactor(); + return LayoutDeviceIntRect(rect.x * scale, rect.y * scale, rect.width * scale, + rect.height * scale); +} + +nsresult nsWindow::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "mouseevent"); + + if (!mGdkWindow) { + return NS_OK; + } + + GdkDisplay* display = gdk_window_get_display(mGdkWindow); + + // When a button-press/release event is requested, create it here and put it + // in the event queue. This will not emit a motion event - this needs to be + // done explicitly *before* requesting a button-press/release. You will also + // need to wait for the motion event to be dispatched before requesting a + // button-press/release event to maintain the desired event order. + if (aNativeMessage == GDK_BUTTON_PRESS || + aNativeMessage == GDK_BUTTON_RELEASE) { + GdkEvent event; + memset(&event, 0, sizeof(GdkEvent)); + event.type = (GdkEventType)aNativeMessage; + event.button.button = 1; + event.button.window = mGdkWindow; + event.button.time = GDK_CURRENT_TIME; + + // Get device for event source + GdkDeviceManager* device_manager = gdk_display_get_device_manager(display); + event.button.device = gdk_device_manager_get_client_pointer(device_manager); + + event.button.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x); + event.button.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y); + + LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset(); + event.button.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x); + event.button.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y); + + gdk_event_put(&event); + } else { + // We don't support specific events other than button-press/release. In all + // other cases we'll synthesize a motion event that will be emitted by + // gdk_display_warp_pointer(). + GdkScreen* screen = gdk_window_get_screen(mGdkWindow); + GdkPoint point = DevicePixelsToGdkPointRoundDown(aPoint); + gdk_display_warp_pointer(display, screen, point.x, point.y); + } + + return NS_OK; +} + +nsresult nsWindow::SynthesizeNativeMouseScrollEvent( + mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, + double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags, + uint32_t aAdditionalFlags, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "mousescrollevent"); + + if (!mGdkWindow) { + return NS_OK; + } + + GdkEvent event; + memset(&event, 0, sizeof(GdkEvent)); + event.type = GDK_SCROLL; + event.scroll.window = mGdkWindow; + event.scroll.time = GDK_CURRENT_TIME; + // Get device for event source + GdkDisplay* display = gdk_window_get_display(mGdkWindow); + GdkDeviceManager* device_manager = gdk_display_get_device_manager(display); + event.scroll.device = gdk_device_manager_get_client_pointer(device_manager); + event.scroll.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x); + event.scroll.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y); + + LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset(); + event.scroll.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x); + event.scroll.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y); + + // The delta values are backwards on Linux compared to Windows and Cocoa, + // hence the negation. + event.scroll.direction = GDK_SCROLL_SMOOTH; + event.scroll.delta_x = -aDeltaX; + event.scroll.delta_y = -aDeltaY; + + gdk_event_put(&event); + + return NS_OK; +} + +nsresult nsWindow::SynthesizeNativeTouchPoint(uint32_t aPointerId, + TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, + double aPointerPressure, + uint32_t aPointerOrientation, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "touchpoint"); + + if (!mGdkWindow) { + return NS_OK; + } + + GdkEvent event; + memset(&event, 0, sizeof(GdkEvent)); + + static std::map<uint32_t, GdkEventSequence*> sKnownPointers; + + auto result = sKnownPointers.find(aPointerId); + switch (aPointerState) { + case TOUCH_CONTACT: + if (result == sKnownPointers.end()) { + // GdkEventSequence isn't a thing we can instantiate, and never gets + // dereferenced in the gtk code. It's an opaque pointer, the only + // requirement is that it be distinct from other instances of + // GdkEventSequence*. + event.touch.sequence = (GdkEventSequence*)((uintptr_t)aPointerId); + sKnownPointers[aPointerId] = event.touch.sequence; + event.type = GDK_TOUCH_BEGIN; + } else { + event.touch.sequence = result->second; + event.type = GDK_TOUCH_UPDATE; + } + break; + case TOUCH_REMOVE: + event.type = GDK_TOUCH_END; + if (result == sKnownPointers.end()) { + NS_WARNING("Tried to synthesize touch-end for unknown pointer!"); + return NS_ERROR_UNEXPECTED; + } + event.touch.sequence = result->second; + sKnownPointers.erase(result); + break; + case TOUCH_CANCEL: + event.type = GDK_TOUCH_CANCEL; + if (result == sKnownPointers.end()) { + NS_WARNING("Tried to synthesize touch-cancel for unknown pointer!"); + return NS_ERROR_UNEXPECTED; + } + event.touch.sequence = result->second; + sKnownPointers.erase(result); + break; + case TOUCH_HOVER: + default: + return NS_ERROR_NOT_IMPLEMENTED; + } + + event.touch.window = mGdkWindow; + event.touch.time = GDK_CURRENT_TIME; + + GdkDisplay* display = gdk_window_get_display(mGdkWindow); + GdkDeviceManager* device_manager = gdk_display_get_device_manager(display); + event.touch.device = gdk_device_manager_get_client_pointer(device_manager); + + event.touch.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x); + event.touch.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y); + + LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset(); + event.touch.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x); + event.touch.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y); + + gdk_event_put(&event); + + return NS_OK; +} + +nsWindow::CSDSupportLevel nsWindow::GetSystemCSDSupportLevel(bool aIsPopup) { + if (sCSDSupportLevel != CSD_SUPPORT_UNKNOWN) { + return sCSDSupportLevel; + } + + // Allow MOZ_GTK_TITLEBAR_DECORATION to override our heuristics + const char* decorationOverride = getenv("MOZ_GTK_TITLEBAR_DECORATION"); + if (decorationOverride) { + if (strcmp(decorationOverride, "none") == 0) { + sCSDSupportLevel = CSD_SUPPORT_NONE; + } else if (strcmp(decorationOverride, "client") == 0) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else if (strcmp(decorationOverride, "system") == 0) { + sCSDSupportLevel = CSD_SUPPORT_SYSTEM; + } + return sCSDSupportLevel; + } + + // nsWindow::GetSystemCSDSupportLevel can be called from various threads + // so we can't use gfxPlatformGtk here. + if (!GDK_IS_X11_DISPLAY(gdk_display_get_default())) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + return sCSDSupportLevel; + } + + const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (currentDesktop) { + // GNOME Flashback (fallback) + if (strstr(currentDesktop, "GNOME-Flashback:GNOME") != nullptr) { + sCSDSupportLevel = aIsPopup ? CSD_SUPPORT_CLIENT : CSD_SUPPORT_SYSTEM; + // Pop Linux Bug 1629198 + } else if (strstr(currentDesktop, "pop:GNOME") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + // gnome-shell + } else if (strstr(currentDesktop, "GNOME") != nullptr) { + sCSDSupportLevel = aIsPopup ? CSD_SUPPORT_CLIENT : CSD_SUPPORT_SYSTEM; + } else if (strstr(currentDesktop, "XFCE") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else if (strstr(currentDesktop, "X-Cinnamon") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_SYSTEM; + // KDE Plasma + } else if (strstr(currentDesktop, "KDE") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else if (strstr(currentDesktop, "Enlightenment") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else if (strstr(currentDesktop, "LXDE") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else if (strstr(currentDesktop, "openbox") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else if (strstr(currentDesktop, "i3") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_NONE; + } else if (strstr(currentDesktop, "MATE") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + // Ubuntu Unity + } else if (strstr(currentDesktop, "Unity") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_SYSTEM; + // Elementary OS + } else if (strstr(currentDesktop, "Pantheon") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_SYSTEM; + } else if (strstr(currentDesktop, "LXQt") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_SYSTEM; + } else if (strstr(currentDesktop, "Deepin") != nullptr) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } else { +// Release or beta builds are not supposed to be broken +// so disable titlebar rendering on untested/unknown systems. +#if defined(RELEASE_OR_BETA) + sCSDSupportLevel = CSD_SUPPORT_NONE; +#else + sCSDSupportLevel = CSD_SUPPORT_CLIENT; +#endif + } + } else { + sCSDSupportLevel = CSD_SUPPORT_NONE; + } + + // GTK_CSD forces CSD mode - use also CSD because window manager + // decorations does not work with CSD. + // We check GTK_CSD as well as gtk_window_should_use_csd() does. + if (sCSDSupportLevel == CSD_SUPPORT_SYSTEM) { + const char* csdOverride = getenv("GTK_CSD"); + if (csdOverride && g_strcmp0(csdOverride, "1") == 0) { + sCSDSupportLevel = CSD_SUPPORT_CLIENT; + } + } + + return sCSDSupportLevel; +} + +bool nsWindow::TitlebarUseShapeMask() { + static int useShapeMask = []() { + // Don't use titlebar shape mask on Wayland + if (!gfxPlatformGtk::GetPlatform()->IsX11Display()) { + return false; + } + + // We can'y use shape masks on Mutter/X.org as we can't resize Firefox + // window there (Bug 1530252). + const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (currentDesktop) { + if (strstr(currentDesktop, "GNOME") != nullptr) { + const char* sessionType = getenv("XDG_SESSION_TYPE"); + if (sessionType && strstr(sessionType, "x11") != nullptr) { + return false; + } + } + } + + return Preferences::GetBool("widget.titlebar-x11-use-shape-mask", false); + }(); + return useShapeMask; +} + +bool nsWindow::HideTitlebarByDefault() { + static int hideTitlebar = []() { + // When user defined widget.default-hidden-titlebar don't do any + // heuristics and just follow it. + if (Preferences::HasUserValue("widget.default-hidden-titlebar")) { + return Preferences::GetBool("widget.default-hidden-titlebar", false); + } + + // Don't hide titlebar when it's disabled on current desktop. + const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (!currentDesktop || GetSystemCSDSupportLevel() == CSD_SUPPORT_NONE) { + return false; + } + + // We hide system titlebar on Gnome/ElementaryOS without any restriction. + return ((strstr(currentDesktop, "GNOME-Flashback:GNOME") != nullptr || + strstr(currentDesktop, "GNOME") != nullptr || + strstr(currentDesktop, "Pantheon") != nullptr)); + }(); + return hideTitlebar; +} + +int32_t nsWindow::RoundsWidgetCoordinatesTo() { return GdkScaleFactor(); } + +void nsWindow::GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) { + // Make sure the window XID is propagated to X server, we can fail otherwise + // in GPU process (Bug 1401634). + if (mXDisplay && mXWindow != X11None) { + XFlush(mXDisplay); + } + + bool isShaped = + mIsTransparent && !mHasAlphaVisual && !mTransparencyBitmapForTitlebar; + *aInitData = mozilla::widget::GtkCompositorWidgetInitData( + (mXWindow != X11None) ? mXWindow : (uintptr_t) nullptr, + mXDisplay ? nsCString(XDisplayString(mXDisplay)) : nsCString(), isShaped, + mIsX11Display, GetClientSize()); +} + +#ifdef MOZ_WAYLAND +bool nsWindow::WaylandSurfaceNeedsClear() { + if (mContainer) { + return moz_container_wayland_surface_needs_clear(MOZ_CONTAINER(mContainer)); + } + return false; +} +#endif + +#ifdef MOZ_X11 +/* XApp progress support currently works by setting a property + * on a window with this Atom name. A supporting window manager + * will notice this and pass it along to whatever handling has + * been implemented on that end (e.g. passing it on to a taskbar + * widget.) There is no issue if WM support is lacking, this is + * simply ignored in that case. + * + * See https://github.com/linuxmint/xapps/blob/master/libxapp/xapp-gtk-window.c + * for further details. + */ + +# define PROGRESS_HINT "_NET_WM_XAPP_PROGRESS" + +static void set_window_hint_cardinal(Window xid, const gchar* atom_name, + gulong cardinal) { + GdkDisplay* display; + + display = gdk_display_get_default(); + + if (cardinal > 0) { + XChangeProperty(GDK_DISPLAY_XDISPLAY(display), xid, + gdk_x11_get_xatom_by_name_for_display(display, atom_name), + XA_CARDINAL, 32, PropModeReplace, (guchar*)&cardinal, 1); + } else { + XDeleteProperty(GDK_DISPLAY_XDISPLAY(display), xid, + gdk_x11_get_xatom_by_name_for_display(display, atom_name)); + } +} +#endif // MOZ_X11 + +void nsWindow::SetProgress(unsigned long progressPercent) { +#ifdef MOZ_X11 + + if (!mIsX11Display) { + return; + } + + if (!mShell) { + return; + } + + progressPercent = MIN(progressPercent, 100); + + set_window_hint_cardinal(GDK_WINDOW_XID(gtk_widget_get_window(mShell)), + PROGRESS_HINT, progressPercent); +#endif // MOZ_X11 +} + +#ifdef MOZ_X11 +void nsWindow::SetCompositorHint(WindowComposeRequest aState) { + if (!mIsX11Display) { + return; + } + + gulong value = aState; + GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL); + gdk_property_change(gtk_widget_get_window(mShell), + gdk_atom_intern("_NET_WM_BYPASS_COMPOSITOR", FALSE), + cardinal_atom, + 32, // format + GDK_PROP_MODE_REPLACE, (guchar*)&value, 1); +} +#endif + +nsresult nsWindow::SetSystemFont(const nsCString& aFontName) { + GtkSettings* settings = gtk_settings_get_default(); + g_object_set(settings, "gtk-font-name", aFontName.get(), nullptr); + return NS_OK; +} + +nsresult nsWindow::GetSystemFont(nsCString& aFontName) { + GtkSettings* settings = gtk_settings_get_default(); + gchar* fontName = nullptr; + g_object_get(settings, "gtk-font-name", &fontName, nullptr); + if (fontName) { + aFontName.Assign(fontName); + g_free(fontName); + } + return NS_OK; +} + +already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() { + nsCOMPtr<nsIWidget> window = new nsWindow(); + return window.forget(); +} + +already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() { + nsCOMPtr<nsIWidget> window = new nsWindow(); + return window.forget(); +} + +#ifdef MOZ_WAYLAND +nsresult nsWindow::GetScreenRect(LayoutDeviceIntRect* aRect) { + typedef struct _GdkMonitor GdkMonitor; + static auto s_gdk_display_get_monitor_at_window = + (GdkMonitor * (*)(GdkDisplay*, GdkWindow*)) + dlsym(RTLD_DEFAULT, "gdk_display_get_monitor_at_window"); + + static auto s_gdk_monitor_get_workarea = + (void (*)(GdkMonitor*, GdkRectangle*))dlsym(RTLD_DEFAULT, + "gdk_monitor_get_workarea"); + + if (!s_gdk_display_get_monitor_at_window || !s_gdk_monitor_get_workarea) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + GtkWindow* topmostParentWindow = GetCurrentTopmostWindow(); + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(topmostParentWindow)); + + GdkMonitor* monitor = + s_gdk_display_get_monitor_at_window(gdk_display_get_default(), gdkWindow); + if (monitor) { + GdkRectangle workArea; + s_gdk_monitor_get_workarea(monitor, &workArea); + // The monitor offset won't help us in Wayland, because we can't get the + // absolute position of our window. + aRect->x = aRect->y = 0; + aRect->width = workArea.width; + aRect->height = workArea.height; + LOG((" workarea for [%p], monitor %p: x%d y%d w%d h%d\n", this, monitor, + workArea.x, workArea.y, workArea.width, workArea.height)); + return NS_OK; + } + return NS_ERROR_NOT_IMPLEMENTED; +} +#endif + +bool nsWindow::GetTopLevelWindowActiveState(nsIFrame* aFrame) { + // Used by window frame and button box rendering. We can end up in here in + // the content process when rendering one of these moz styles freely in a + // page. Fail in this case, there is no applicable window focus state. + if (!XRE_IsParentProcess()) { + return false; + } + // All headless windows are considered active so they are painted. + if (gfxPlatform::IsHeadless()) { + return true; + } + // Get the widget. nsIFrame's GetNearestWidget walks up the view chain + // until it finds a real window. + nsWindow* window = static_cast<nsWindow*>(aFrame->GetNearestWidget()); + if (!window) { + return false; + } + + // Get our toplevel nsWindow. + if (!window->mIsTopLevel) { + GtkWidget* widget = window->GetMozContainerWidget(); + if (!widget) { + return false; + } + + GtkWidget* toplevelWidget = gtk_widget_get_toplevel(widget); + window = get_window_for_gtk_widget(toplevelWidget); + if (!window) { + return false; + } + } + + return !window->mTitlebarBackdropState; +} + +static nsIFrame* FindTitlebarFrame(nsIFrame* aFrame) { + for (nsIFrame* childFrame : aFrame->PrincipalChildList()) { + StyleAppearance appearance = + childFrame->StyleDisplay()->EffectiveAppearance(); + if (appearance == StyleAppearance::MozWindowTitlebar || + appearance == StyleAppearance::MozWindowTitlebarMaximized) { + return childFrame; + } + + if (nsIFrame* foundFrame = FindTitlebarFrame(childFrame)) { + return foundFrame; + } + } + return nullptr; +} + +nsIFrame* nsWindow::GetFrame(void) { + nsView* view = nsView::GetViewFor(this); + if (!view) { + return nullptr; + } + return view->GetFrame(); +} + +void nsWindow::UpdateMozWindowActive() { + // Update activation state for the :-moz-window-inactive pseudoclass. + // Normally, this follows focus; we override it here to follow + // GDK_WINDOW_STATE_FOCUSED. + if (mozilla::dom::Document* document = GetDocument()) { + if (nsPIDOMWindowOuter* window = document->GetWindow()) { + if (RefPtr<mozilla::dom::BrowsingContext> bc = + window->GetBrowsingContext()) { + bc->SetIsActiveBrowserWindow(!mTitlebarBackdropState); + } + } + } +} + +void nsWindow::ForceTitlebarRedraw(void) { + MOZ_ASSERT(mDrawInTitlebar, "We should not redraw invisible titlebar."); + + if (!mWidgetListener || !mWidgetListener->GetPresShell()) { + return; + } + + nsIFrame* frame = GetFrame(); + if (!frame) { + return; + } + + frame = FindTitlebarFrame(frame); + if (frame) { + nsIContent* content = frame->GetContent(); + if (content) { + nsLayoutUtils::PostRestyleEvent(content->AsElement(), RestyleHint{0}, + nsChangeHint_RepaintFrame); + } + } +} + +GtkTextDirection nsWindow::GetTextDirection() { + nsIFrame* frame = GetFrame(); + if (!frame) { + return GTK_TEXT_DIR_LTR; + } + + WritingMode wm = frame->GetWritingMode(); + return wm.IsPhysicalLTR() ? GTK_TEXT_DIR_LTR : GTK_TEXT_DIR_RTL; +} + +void nsWindow::LockAspectRatio(bool aShouldLock) { + if (!gUseAspectRatio) { + return; + } + + if (aShouldLock) { + int decWidth = 0, decHeight = 0; + AddCSDDecorationSize(&decWidth, &decHeight); + + float width = + (float)DevicePixelsToGdkCoordRoundDown(mBounds.width) + decWidth; + float height = + (float)DevicePixelsToGdkCoordRoundDown(mBounds.height) + decHeight; + + mAspectRatio = width / height; + LOG(("nsWindow::LockAspectRatio() [%p] width %f height %f aspect %f\n", + (void*)this, width, height, mAspectRatio)); + } else { + mAspectRatio = 0.0; + LOG(("nsWindow::LockAspectRatio() [%p] removed aspect ratio\n", + (void*)this)); + } + + ApplySizeConstraints(); +} + +#ifdef MOZ_WAYLAND +void nsWindow::SetEGLNativeWindowSize( + const LayoutDeviceIntSize& aEGLWindowSize) { + if (mContainer && !mIsX11Display) { + moz_container_wayland_egl_window_set_size(mContainer, aEGLWindowSize.width, + aEGLWindowSize.height); + } +} + +nsWindow* nsWindow::GetFocusedWindow() { return gFocusWindow; } +#endif + +LayoutDeviceIntRect nsWindow::GetMozContainerSize() { + LayoutDeviceIntRect size(0, 0, 0, 0); + if (mContainer) { + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(mContainer), &allocation); + int scale = GdkScaleFactor(); + size.width = allocation.width * scale; + size.height = allocation.height * scale; + } + return size; +} diff --git a/widget/gtk/nsWindow.h b/widget/gtk/nsWindow.h new file mode 100644 index 0000000000..5bfdcd9291 --- /dev/null +++ b/widget/gtk/nsWindow.h @@ -0,0 +1,719 @@ +/* -*- 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 __nsWindow_h__ +#define __nsWindow_h__ + +#include <gdk/gdk.h> +#include <gtk/gtk.h> +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +# include "X11UndefineNone.h" +#endif /* MOZ_X11 */ +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +# include "base/thread.h" +# include "WaylandVsyncSource.h" +#endif +#include "MozContainer.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsIDragService.h" +#include "nsGkAtoms.h" +#include "nsRefPtrHashtable.h" +#include "nsBaseWidget.h" +#include "CompositorWidget.h" +#include "mozilla/widget/WindowSurface.h" +#include "mozilla/widget/WindowSurfaceProvider.h" +#include "mozilla/Maybe.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/Accessible.h" +#endif +#include "mozilla/EventForwards.h" +#include "mozilla/TouchEvents.h" + +#include "IMContextWrapper.h" + +#undef LOG +#ifdef MOZ_LOGGING + +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" + +extern mozilla::LazyLogModule gWidgetLog; +extern mozilla::LazyLogModule gWidgetFocusLog; +extern mozilla::LazyLogModule gWidgetDragLog; +extern mozilla::LazyLogModule gWidgetDrawLog; + +# define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args) +# define LOGFOCUS(args) \ + MOZ_LOG(gWidgetFocusLog, mozilla::LogLevel::Debug, args) +# define LOGDRAG(args) MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, args) +# define LOGDRAW(args) MOZ_LOG(gWidgetDrawLog, mozilla::LogLevel::Debug, args) + +#else + +# define LOG(args) +# define LOGFOCUS(args) +# define LOGDRAG(args) +# define LOGDRAW(args) + +#endif /* MOZ_LOGGING */ + +#ifdef MOZ_WAYLAND +class nsWaylandDragContext; + +gboolean WindowDragMotionHandler(GtkWidget* aWidget, + GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + gint aX, gint aY, guint aTime); +gboolean WindowDragDropHandler(GtkWidget* aWidget, GdkDragContext* aDragContext, + nsWaylandDragContext* aWaylandDragContext, + gint aX, gint aY, guint aTime); +void WindowDragLeaveHandler(GtkWidget* aWidget); +#endif + +class gfxPattern; +class nsIFrame; +#if !GTK_CHECK_VERSION(3, 18, 0) +struct _GdkEventTouchpadPinch; +typedef struct _GdkEventTouchpadPinch GdkEventTouchpadPinch; + +#endif + +namespace mozilla { +class TimeStamp; +class CurrentX11TimeGetter; + +} // namespace mozilla + +class nsWindow final : public nsBaseWidget { + public: + typedef mozilla::gfx::DrawTarget DrawTarget; + typedef mozilla::WidgetEventTime WidgetEventTime; + typedef mozilla::WidgetKeyboardEvent WidgetKeyboardEvent; + typedef mozilla::widget::PlatformCompositorWidgetDelegate + PlatformCompositorWidgetDelegate; + + nsWindow(); + + static void ReleaseGlobals(); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(nsWindow, nsBaseWidget) + + void CommonCreate(nsIWidget* aParent, bool aListenForResizes); + + virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override; + + // called when we are destroyed + virtual void OnDestroy(void) override; + + // called to check and see if a widget's dimensions are sane + bool AreBoundsSane(void); + + // nsIWidget + using nsBaseWidget::Create; // for Create signature not overridden here + [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData) override; + virtual void Destroy() override; + virtual nsIWidget* GetParent() override; + virtual float GetDPI() override; + virtual double GetDefaultScaleInternal() override; + mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() override; + mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScaleByScreen() + override; + virtual void SetParent(nsIWidget* aNewParent) override; + virtual void SetModal(bool aModal) override; + virtual bool IsVisible() const override; + virtual void ConstrainPosition(bool aAllowSlop, int32_t* aX, + int32_t* aY) override; + virtual void SetSizeConstraints(const SizeConstraints& aConstraints) override; + virtual void LockAspectRatio(bool aShouldLock) override; + virtual void Move(double aX, double aY) override; + virtual void Show(bool aState) override; + virtual void Resize(double aWidth, double aHeight, bool aRepaint) override; + virtual void Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) override; + virtual bool IsEnabled() const override; + + void SetZIndex(int32_t aZIndex) override; + virtual void SetSizeMode(nsSizeMode aMode) override; + virtual void GetWorkspaceID(nsAString& workspaceID) override; + virtual void MoveToWorkspace(const nsAString& workspaceID) override; + virtual void Enable(bool aState) override; + virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override; + virtual LayoutDeviceIntRect GetScreenBounds() override; + virtual LayoutDeviceIntRect GetClientBounds() override; + virtual LayoutDeviceIntSize GetClientSize() override; + virtual LayoutDeviceIntPoint GetClientOffset() override; + virtual void SetCursor(nsCursor aDefaultCursor, imgIContainer* aCursor, + uint32_t aHotspotX, uint32_t aHotspotY) override; + virtual void Invalidate(const LayoutDeviceIntRect& aRect) override; + virtual void* GetNativeData(uint32_t aDataType) override; + virtual nsresult SetTitle(const nsAString& aTitle) override; + virtual void SetIcon(const nsAString& aIconSpec) override; + virtual void SetWindowClass(const nsAString& xulWinType) override; + virtual LayoutDeviceIntPoint WidgetToScreenOffset() override; + virtual void CaptureMouse(bool aCapture) override; + virtual void CaptureRollupEvents(nsIRollupListener* aListener, + bool aDoCapture) override; + [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) override; + virtual nsresult SetWindowClipRegion( + const nsTArray<LayoutDeviceIntRect>& aRects, + bool aIntersectWithExisting) override; + virtual bool HasPendingInputEvent() override; + + virtual bool PrepareForFullscreenTransition(nsISupports** aData) override; + virtual void PerformFullscreenTransition(FullscreenTransitionStage aStage, + uint16_t aDuration, + nsISupports* aData, + nsIRunnable* aCallback) override; + virtual already_AddRefed<nsIScreen> GetWidgetScreen() override; + virtual nsresult MakeFullScreen(bool aFullScreen, + nsIScreen* aTargetScreen = nullptr) override; + virtual void HideWindowChrome(bool aShouldHide) override; + + /** + * GetLastUserInputTime returns a timestamp for the most recent user input + * event. This is intended for pointer grab requests (including drags). + */ + static guint32 GetLastUserInputTime(); + + // utility method, -1 if no change should be made, otherwise returns a + // value that can be passed to gdk_window_set_decorations + gint ConvertBorderStyles(nsBorderStyle aStyle); + + GdkRectangle DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect aRect); + + mozilla::widget::IMContextWrapper* GetIMContext() const { return mIMContext; } + + bool DispatchCommandEvent(nsAtom* aCommand); + bool DispatchContentCommandEvent(mozilla::EventMessage aMsg); + + // event callbacks + gboolean OnExposeEvent(cairo_t* cr); + gboolean OnConfigureEvent(GtkWidget* aWidget, GdkEventConfigure* aEvent); + void OnContainerUnrealize(); + void OnSizeAllocate(GtkAllocation* aAllocation); + void OnDeleteEvent(); + void OnEnterNotifyEvent(GdkEventCrossing* aEvent); + void OnLeaveNotifyEvent(GdkEventCrossing* aEvent); + void OnMotionNotifyEvent(GdkEventMotion* aEvent); + void OnButtonPressEvent(GdkEventButton* aEvent); + void OnButtonReleaseEvent(GdkEventButton* aEvent); + void OnContainerFocusInEvent(GdkEventFocus* aEvent); + void OnContainerFocusOutEvent(GdkEventFocus* aEvent); + gboolean OnKeyPressEvent(GdkEventKey* aEvent); + gboolean OnKeyReleaseEvent(GdkEventKey* aEvent); + + void OnScrollEvent(GdkEventScroll* aEvent); + + void OnWindowStateEvent(GtkWidget* aWidget, GdkEventWindowState* aEvent); + void OnDragDataReceivedEvent(GtkWidget* aWidget, GdkDragContext* aDragContext, + gint aX, gint aY, + GtkSelectionData* aSelectionData, guint aInfo, + guint aTime, gpointer aData); + gboolean OnPropertyNotifyEvent(GtkWidget* aWidget, GdkEventProperty* aEvent); + gboolean OnTouchEvent(GdkEventTouch* aEvent); + gboolean OnTouchpadPinchEvent(GdkEventTouchpadPinch* aEvent); + + void UpdateTopLevelOpaqueRegion(); + + virtual already_AddRefed<mozilla::gfx::DrawTarget> StartRemoteDrawingInRegion( + LayoutDeviceIntRegion& aInvalidRegion, + mozilla::layers::BufferMode* aBufferMode) override; + virtual void EndRemoteDrawingInRegion( + mozilla::gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion) override; + + void SetProgress(unsigned long progressPercent); + +#ifdef MOZ_WAYLAND + void SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize); + static nsWindow* GetFocusedWindow(); +#endif + + RefPtr<mozilla::gfx::VsyncSource> GetVsyncSource() override; + + static void WithSettingsChangesIgnored(const std::function<void()>& aFn); + + private: + void UpdateAlpha(mozilla::gfx::SourceSurface* aSourceSurface, + nsIntRect aBoundsRect); + + void NativeMove(); + void NativeResize(); + void NativeMoveResize(); + + void NativeShow(bool aAction); + void SetHasMappedToplevel(bool aState); + LayoutDeviceIntSize GetSafeWindowSize(LayoutDeviceIntSize aSize); + + void EnsureGrabs(void); + void GrabPointer(guint32 aTime); + void ReleaseGrabs(void); + + void UpdateClientOffsetFromFrameExtents(); + void UpdateClientOffsetFromCSDWindow(); + + void DispatchContextMenuEventFromMouseEvent(uint16_t domButton, + GdkEventButton* aEvent); +#ifdef MOZ_WAYLAND + void MaybeResumeCompositor(); +#endif + + void WaylandStartVsync(); + void WaylandStopVsync(); + + public: + void ThemeChanged(void); + void OnDPIChanged(void); + void OnCheckResize(void); + void OnCompositedChanged(void); + void OnScaleChanged(GtkAllocation* aAllocation); + void DispatchResized(); + +#ifdef MOZ_X11 + Window mOldFocusWindow; +#endif /* MOZ_X11 */ + + static guint32 sLastButtonPressTime; + + [[nodiscard]] virtual nsresult BeginResizeDrag( + mozilla::WidgetGUIEvent* aEvent, int32_t aHorizontal, + int32_t aVertical) override; + + MozContainer* GetMozContainer() { return mContainer; } + LayoutDeviceIntRect GetMozContainerSize(); + // GetMozContainerWidget returns the MozContainer even for undestroyed + // descendant windows + GtkWidget* GetMozContainerWidget(); + GdkWindow* GetGdkWindow() { return mGdkWindow; } + GtkWidget* GetGtkWidget() { return mShell; } + nsIFrame* GetFrame(); + bool IsDestroyed() { return mIsDestroyed; } + bool IsPopup(); + bool IsWaylandPopup(); + bool IsPIPWindow() { return mIsPIPWindow; }; + + void DispatchDragEvent(mozilla::EventMessage aMsg, + const LayoutDeviceIntPoint& aRefPoint, guint aTime); + static void UpdateDragStatus(GdkDragContext* aDragContext, + nsIDragService* aDragService); + + WidgetEventTime GetWidgetEventTime(guint32 aEventTime); + mozilla::TimeStamp GetEventTimeStamp(guint32 aEventTime); + mozilla::CurrentX11TimeGetter* GetCurrentTimeGetter(); + + virtual void SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) override; + virtual InputContext GetInputContext() override; + virtual TextEventDispatcherListener* GetNativeTextEventDispatcherListener() + override; + void GetEditCommandsRemapped(NativeKeyBindingsType aType, + const mozilla::WidgetKeyboardEvent& aEvent, + nsTArray<mozilla::CommandInt>& aCommands, + uint32_t aGeckoKeyCode, uint32_t aNativeKeyCode); + virtual bool GetEditCommands( + NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent, + nsTArray<mozilla::CommandInt>& aCommands) override; + + // These methods are for toplevel windows only. + void ResizeTransparencyBitmap(); + void ApplyTransparencyBitmap(); + void ClearTransparencyBitmap(); + + virtual void SetTransparencyMode(nsTransparencyMode aMode) override; + virtual nsTransparencyMode GetTransparencyMode() override; + virtual void SetWindowMouseTransparent(bool aIsTransparent) override; + virtual nsresult ConfigureChildren( + const nsTArray<Configuration>& aConfigurations) override; + nsresult UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect, + uint8_t* aAlphas, + int32_t aStride); + void UpdateTitlebarTransparencyBitmap(); + + virtual void ReparentNativeWidget(nsIWidget* aNewParent) override; + + virtual nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) override; + + virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint, + nsIObserver* aObserver) override { + return SynthesizeNativeMouseEvent(aPoint, GDK_MOTION_NOTIFY, 0, aObserver); + } + + virtual nsresult SynthesizeNativeMouseScrollEvent( + LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX, + double aDeltaY, double aDeltaZ, uint32_t aModifierFlags, + uint32_t aAdditionalFlags, nsIObserver* aObserver) override; + + virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId, + TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, + double aPointerPressure, + uint32_t aPointerOrientation, + nsIObserver* aObserver) override; + +#ifdef MOZ_X11 + Display* XDisplay() { return mXDisplay; } +#endif +#ifdef MOZ_WAYLAND + wl_display* GetWaylandDisplay(); + bool WaylandSurfaceNeedsClear(); + virtual void CreateCompositorVsyncDispatcher() override; +#endif + virtual void GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) override; + + virtual nsresult SetNonClientMargins( + LayoutDeviceIntMargin& aMargins) override; + void SetDrawsInTitlebar(bool aState) override; + bool GetTitlebarRect(mozilla::gfx::Rect& aRect); + virtual void UpdateWindowDraggingRegion( + const LayoutDeviceIntRegion& aRegion) override; + + // HiDPI scale conversion + gint GdkScaleFactor(); + + // To GDK + gint DevicePixelsToGdkCoordRoundUp(int pixels); + gint DevicePixelsToGdkCoordRoundDown(int pixels); + GdkPoint DevicePixelsToGdkPointRoundDown(LayoutDeviceIntPoint point); + GdkRectangle DevicePixelsToGdkSizeRoundUp(LayoutDeviceIntSize pixelSize); + + // From GDK + int GdkCoordToDevicePixels(gint coord); + LayoutDeviceIntPoint GdkPointToDevicePixels(GdkPoint point); + LayoutDeviceIntPoint GdkEventCoordsToDevicePixels(gdouble x, gdouble y); + LayoutDeviceIntRect GdkRectToDevicePixels(GdkRectangle rect); + + virtual bool WidgetTypeSupportsAcceleration() override; + + nsresult SetSystemFont(const nsCString& aFontName) override; + nsresult GetSystemFont(nsCString& aFontName) override; + + typedef enum { + CSD_SUPPORT_SYSTEM, // CSD including shadows + CSD_SUPPORT_CLIENT, // CSD without shadows + CSD_SUPPORT_NONE, // WM does not support CSD at all + CSD_SUPPORT_UNKNOWN + } CSDSupportLevel; + /** + * Get the support of Client Side Decoration by checking + * the XDG_CURRENT_DESKTOP environment variable. + */ + static CSDSupportLevel GetSystemCSDSupportLevel(bool aIsPopup = false); + + static bool HideTitlebarByDefault(); + static bool GetTopLevelWindowActiveState(nsIFrame* aFrame); + static bool TitlebarUseShapeMask(); +#ifdef MOZ_WAYLAND + virtual nsresult GetScreenRect(LayoutDeviceIntRect* aRect) override; + virtual nsRect GetPreferredPopupRect() override { + return mPreferredPopupRect; + }; + virtual void FlushPreferredPopupRect() override { + mPreferredPopupRect = nsRect(0, 0, 0, 0); + mPreferredPopupRectFlushed = true; + }; +#endif + bool IsRemoteContent() { return HasRemoteContent(); } + static void HideWaylandOpenedPopups(); + void NativeMoveResizeWaylandPopupCB(const GdkRectangle* aFinalSize, + bool aFlippedX, bool aFlippedY); + static bool IsToplevelWindowTransparent(); + + protected: + virtual ~nsWindow(); + + // event handling code + void DispatchActivateEvent(void); + void DispatchDeactivateEvent(void); + void MaybeDispatchResized(); + + virtual void RegisterTouchWindow() override; + virtual bool CompositorInitiallyPaused() override { +#ifdef MOZ_WAYLAND + return mCompositorInitiallyPaused; +#else + return false; +#endif + } + nsCOMPtr<nsIWidget> mParent; + // Is this a toplevel window? + bool mIsTopLevel; + // Has this widget been destroyed yet? + bool mIsDestroyed; + + // Should we send resize events on all resizes? + bool mListenForResizes; + // Does WindowResized need to be called on listeners? + bool mNeedsDispatchResized; + // This flag tracks if we're hidden or shown. + bool mIsShown; + bool mNeedsShow; + // is this widget enabled? + bool mEnabled; + // has the native window for this been created yet? + bool mCreated; + // whether we handle touch event + bool mHandleTouchEvent; + // true if this is a drag and drop feedback popup + bool mIsDragPopup; + // Can we access X? + bool mIsX11Display; +#ifdef MOZ_WAYLAND + bool mNeedsCompositorResume; + bool mCompositorInitiallyPaused; +#endif + bool mWindowScaleFactorChanged; + int mWindowScaleFactor; + bool mCompositedScreen; + + private: + void DestroyChildWindows(); + GtkWidget* GetToplevelWidget(); + nsWindow* GetContainerWindow(); + void SetUrgencyHint(GtkWidget* top_window, bool state); + void SetDefaultIcon(void); + void SetWindowDecoration(nsBorderStyle aStyle); + void InitButtonEvent(mozilla::WidgetMouseEvent& aEvent, + GdkEventButton* aGdkEvent); + bool CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel, + bool aAlwaysRollup); + void CheckForRollupDuringGrab() { CheckForRollup(0, 0, false, true); } + + bool GetDragInfo(mozilla::WidgetMouseEvent* aMouseEvent, GdkWindow** aWindow, + gint* aButton, gint* aRootX, gint* aRootY); + void ClearCachedResources(); + nsIWidgetListener* GetListener(); + + nsWindow* GetTransientForWindowIfPopup(); + bool IsHandlingTouchSequence(GdkEventSequence* aSequence); + + void ResizeInt(int aX, int aY, int aWidth, int aHeight, bool aMove, + bool aRepaint); + void NativeMoveResizeWaylandPopup(GdkPoint* aPosition, GdkRectangle* aSize); + + GtkTextDirection GetTextDirection(); + + void AddCSDDecorationSize(int* aWidth, int* aHeight); + +#ifdef MOZ_X11 + typedef enum {GTK_WIDGET_COMPOSIDED_DEFAULT = 0, + GTK_WIDGET_COMPOSIDED_DISABLED = 1, + GTK_WIDGET_COMPOSIDED_ENABLED = 2} WindowComposeRequest; + + void SetCompositorHint(WindowComposeRequest aState); +#endif + nsCString mGtkWindowAppName; + nsCString mGtkWindowRoleName; + void RefreshWindowClass(); + + GtkWidget* mShell; + MozContainer* mContainer; + GdkWindow* mGdkWindow; + bool mWindowShouldStartDragging = false; + PlatformCompositorWidgetDelegate* mCompositorWidgetDelegate; + + uint32_t mHasMappedToplevel : 1, mRetryPointerGrab : 1; + nsSizeMode mSizeState; + float mAspectRatio; + float mAspectRatioSaved; + nsIntPoint mClientOffset; + + // This field omits duplicate scroll events caused by GNOME bug 726878. + guint32 mLastScrollEventTime; + mozilla::ScreenCoord mLastPinchEventSpan; + bool mPanInProgress = false; + + // for touch event handling + nsRefPtrHashtable<nsPtrHashKey<GdkEventSequence>, mozilla::dom::Touch> + mTouches; + +#ifdef MOZ_X11 + Display* mXDisplay; + Window mXWindow; + Visual* mXVisual; + int mXDepth; + mozilla::widget::WindowSurfaceProvider mSurfaceProvider; + + bool ConfigureX11GLVisual(bool aUseAlpha); +#endif +#ifdef MOZ_WAYLAND + RefPtr<mozilla::gfx::VsyncSource> mWaylandVsyncSource; +#endif + + // Upper bound on pending ConfigureNotify events to be dispatched to the + // window. See bug 1225044. + unsigned int mPendingConfigures; + + // Window titlebar rendering mode, CSD_SUPPORT_NONE if it's disabled + // for this window. + CSDSupportLevel mCSDSupportLevel; + // Use dedicated GdkWindow for mContainer + bool mDrawToContainer; + // If true, draw our own window titlebar. + bool mDrawInTitlebar; + // Draw titlebar with :backdrop css state (inactive/unfocused). + bool mTitlebarBackdropState; + // Draggable titlebar region maintained by UpdateWindowDraggingRegion + LayoutDeviceIntRegion mDraggableRegion; + // It's PictureInPicture window. + bool mIsPIPWindow; + bool mAlwaysOnTop; + +#ifdef ACCESSIBILITY + RefPtr<mozilla::a11y::Accessible> mRootAccessible; + + /** + * Request to create the accessible for this window if it is top level. + */ + void CreateRootAccessible(); + + /** + * Dispatch accessible event for the top level window accessible. + * + * @param aEventType [in] the accessible event type to dispatch + */ + void DispatchEventToRootAccessible(uint32_t aEventType); + + /** + * Dispatch accessible window activate event for the top level window + * accessible. + */ + void DispatchActivateEventAccessible(); + + /** + * Dispatch accessible window deactivate event for the top level window + * accessible. + */ + void DispatchDeactivateEventAccessible(); + + /** + * Dispatch accessible window maximize event for the top level window + * accessible. + */ + void DispatchMaximizeEventAccessible(); + + /** + * Dispatch accessible window minize event for the top level window + * accessible. + */ + void DispatchMinimizeEventAccessible(); + + /** + * Dispatch accessible window restore event for the top level window + * accessible. + */ + void DispatchRestoreEventAccessible(); +#endif + + // The cursor cache + static GdkCursor* gsGtkCursorCache[eCursorCount]; + + // Transparency + bool mIsTransparent; + // This bitmap tracks which pixels are transparent. We don't support + // full translucency at this time; each pixel is either fully opaque + // or fully transparent. + gchar* mTransparencyBitmap; + int32_t mTransparencyBitmapWidth; + int32_t mTransparencyBitmapHeight; + // The transparency bitmap is used instead of ARGB visual for toplevel + // window to draw titlebar. + bool mTransparencyBitmapForTitlebar; + + // True when we're on compositing window manager and this + // window is using visual with alpha channel. + bool mHasAlphaVisual; + + // all of our DND stuff + void InitDragEvent(mozilla::WidgetDragEvent& aEvent); + + float mLastMotionPressure; + + // Remember the last sizemode so that we can restore it when + // leaving fullscreen + nsSizeMode mLastSizeMode; + // We can't detect size state changes correctly so set this flag + // to force update mBounds after a size state change from a configure + // event. + bool mBoundsAreValid; + + static bool DragInProgress(void); + + void DispatchMissedButtonReleases(GdkEventCrossing* aGdkEvent); + + // nsBaseWidget + virtual LayerManager* GetLayerManager( + PLayerTransactionChild* aShadowManager = nullptr, + LayersBackend aBackendHint = mozilla::layers::LayersBackend::LAYERS_NONE, + LayerManagerPersistence aPersistence = LAYER_MANAGER_CURRENT) override; + + void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override; + + void CleanLayerManagerRecursive(); + + virtual int32_t RoundsWidgetCoordinatesTo() override; + + void UpdateMozWindowActive(); + + void ForceTitlebarRedraw(); + bool DoDrawTilebarCorners(); + bool IsChromeWindowTitlebar(); + + void SetPopupWindowDecoration(bool aShowOnTaskbar); + + void ApplySizeConstraints(void); + + bool IsMainMenuWindow(); + GtkWidget* ConfigureWaylandPopupWindows(); + void PauseRemoteRenderer(); + void HideWaylandWindow(); + void HideWaylandTooltips(); + void HideWaylandPopupAndAllChildren(); + void CleanupWaylandPopups(); + GtkWindow* GetCurrentTopmostWindow(); + GtkWindow* GetCurrentWindow(); + GtkWindow* GetTopmostWindow(); + bool IsWidgetOverflowWindow(); + nsRect mPreferredPopupRect; + bool mPreferredPopupRectFlushed; + bool mWaitingForMoveToRectCB; + LayoutDeviceIntRect mPendingSizeRect; + + /** + * |mIMContext| takes all IME related stuff. + * + * This is owned by the top-level nsWindow or the topmost child + * nsWindow embedded in a non-Gecko widget. + * + * The instance is created when the top level widget is created. And when + * the widget is destroyed, it's released. All child windows refer its + * ancestor widget's instance. So, one set of IM contexts is created for + * all windows in a hierarchy. If the children are released after the top + * level window is released, the children still have a valid pointer, + * however, IME doesn't work at that time. + */ + RefPtr<mozilla::widget::IMContextWrapper> mIMContext; + + mozilla::UniquePtr<mozilla::CurrentX11TimeGetter> mCurrentTimeGetter; + static CSDSupportLevel sCSDSupportLevel; + + static bool sTransparentMainWindow; +}; + +#endif /* __nsWindow_h__ */ diff --git a/widget/gtk/wayland/gbm.h b/widget/gtk/wayland/gbm.h new file mode 100644 index 0000000000..bd94fa8967 --- /dev/null +++ b/widget/gtk/wayland/gbm.h @@ -0,0 +1,480 @@ +/* + * Copyright © 2011 Intel Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Authors: + * Benjamin Franzke <benjaminfranzke@googlemail.com> + */ + +#ifndef _GBM_H_ +#define _GBM_H_ + +#define __GBM__ 1 + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file gbm.h + * \brief Generic Buffer Manager + */ + +struct gbm_device; +struct gbm_bo; +struct gbm_surface; + +/** + * \mainpage The Generic Buffer Manager + * + * This module provides an abstraction that the caller can use to request a + * buffer from the underlying memory management system for the platform. + * + * This allows the creation of portable code whilst still allowing access to + * the underlying memory manager. + */ + +/** + * Abstraction representing the handle to a buffer allocated by the + * manager + */ +union gbm_bo_handle { + void* ptr; + int32_t s32; + uint32_t u32; + int64_t s64; + uint64_t u64; +}; + +/** Format of the allocated buffer */ +enum gbm_bo_format { + /** RGB with 8 bits per channel in a 32 bit value */ + GBM_BO_FORMAT_XRGB8888, + /** ARGB with 8 bits per channel in a 32 bit value */ + GBM_BO_FORMAT_ARGB8888 +}; + +/** + * The FourCC format codes are taken from the drm_fourcc.h definition, and + * re-namespaced. New GBM formats must not be added, unless they are + * identical ports from drm_fourcc. + */ +#define __gbm_fourcc_code(a, b, c, d) \ + ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | \ + ((uint32_t)(d) << 24)) + +#define GBM_FORMAT_BIG_ENDIAN \ + (1 << 31) /* format is big endian instead of little endian */ + +/* color index */ +#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */ + +/* 8 bpp Red */ +#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */ + +/* 16 bpp RG */ +#define GBM_FORMAT_GR88 \ + __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */ + +/* 8 bpp RGB */ +#define GBM_FORMAT_RGB332 \ + __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */ +#define GBM_FORMAT_BGR233 \ + __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */ + +/* 16 bpp RGB */ +#define GBM_FORMAT_XRGB4444 \ + __gbm_fourcc_code('X', 'R', '1', \ + '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */ +#define GBM_FORMAT_XBGR4444 \ + __gbm_fourcc_code('X', 'B', '1', \ + '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */ +#define GBM_FORMAT_RGBX4444 \ + __gbm_fourcc_code('R', 'X', '1', \ + '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */ +#define GBM_FORMAT_BGRX4444 \ + __gbm_fourcc_code('B', 'X', '1', \ + '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */ + +#define GBM_FORMAT_ARGB4444 \ + __gbm_fourcc_code('A', 'R', '1', \ + '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */ +#define GBM_FORMAT_ABGR4444 \ + __gbm_fourcc_code('A', 'B', '1', \ + '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */ +#define GBM_FORMAT_RGBA4444 \ + __gbm_fourcc_code('R', 'A', '1', \ + '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */ +#define GBM_FORMAT_BGRA4444 \ + __gbm_fourcc_code('B', 'A', '1', \ + '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */ + +#define GBM_FORMAT_XRGB1555 \ + __gbm_fourcc_code('X', 'R', '1', \ + '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */ +#define GBM_FORMAT_XBGR1555 \ + __gbm_fourcc_code('X', 'B', '1', \ + '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */ +#define GBM_FORMAT_RGBX5551 \ + __gbm_fourcc_code('R', 'X', '1', \ + '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */ +#define GBM_FORMAT_BGRX5551 \ + __gbm_fourcc_code('B', 'X', '1', \ + '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */ + +#define GBM_FORMAT_ARGB1555 \ + __gbm_fourcc_code('A', 'R', '1', \ + '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */ +#define GBM_FORMAT_ABGR1555 \ + __gbm_fourcc_code('A', 'B', '1', \ + '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */ +#define GBM_FORMAT_RGBA5551 \ + __gbm_fourcc_code('R', 'A', '1', \ + '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */ +#define GBM_FORMAT_BGRA5551 \ + __gbm_fourcc_code('B', 'A', '1', \ + '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */ + +#define GBM_FORMAT_RGB565 \ + __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */ +#define GBM_FORMAT_BGR565 \ + __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */ + +/* 24 bpp RGB */ +#define GBM_FORMAT_RGB888 \ + __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */ +#define GBM_FORMAT_BGR888 \ + __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */ + +/* 32 bpp RGB */ +#define GBM_FORMAT_XRGB8888 \ + __gbm_fourcc_code('X', 'R', '2', \ + '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */ +#define GBM_FORMAT_XBGR8888 \ + __gbm_fourcc_code('X', 'B', '2', \ + '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */ +#define GBM_FORMAT_RGBX8888 \ + __gbm_fourcc_code('R', 'X', '2', \ + '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */ +#define GBM_FORMAT_BGRX8888 \ + __gbm_fourcc_code('B', 'X', '2', \ + '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */ + +#define GBM_FORMAT_ARGB8888 \ + __gbm_fourcc_code('A', 'R', '2', \ + '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */ +#define GBM_FORMAT_ABGR8888 \ + __gbm_fourcc_code('A', 'B', '2', \ + '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */ +#define GBM_FORMAT_RGBA8888 \ + __gbm_fourcc_code('R', 'A', '2', \ + '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */ +#define GBM_FORMAT_BGRA8888 \ + __gbm_fourcc_code('B', 'A', '2', \ + '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */ + +#define GBM_FORMAT_XRGB2101010 \ + __gbm_fourcc_code('X', 'R', '3', \ + '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */ +#define GBM_FORMAT_XBGR2101010 \ + __gbm_fourcc_code('X', 'B', '3', \ + '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */ +#define GBM_FORMAT_RGBX1010102 \ + __gbm_fourcc_code('R', 'X', '3', \ + '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */ +#define GBM_FORMAT_BGRX1010102 \ + __gbm_fourcc_code('B', 'X', '3', \ + '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */ + +#define GBM_FORMAT_ARGB2101010 \ + __gbm_fourcc_code('A', 'R', '3', \ + '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */ +#define GBM_FORMAT_ABGR2101010 \ + __gbm_fourcc_code('A', 'B', '3', \ + '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */ +#define GBM_FORMAT_RGBA1010102 \ + __gbm_fourcc_code('R', 'A', '3', \ + '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */ +#define GBM_FORMAT_BGRA1010102 \ + __gbm_fourcc_code('B', 'A', '3', \ + '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */ + +/* packed YCbCr */ +#define GBM_FORMAT_YUYV \ + __gbm_fourcc_code('Y', 'U', 'Y', \ + 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */ +#define GBM_FORMAT_YVYU \ + __gbm_fourcc_code('Y', 'V', 'Y', \ + 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */ +#define GBM_FORMAT_UYVY \ + __gbm_fourcc_code('U', 'Y', 'V', \ + 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */ +#define GBM_FORMAT_VYUY \ + __gbm_fourcc_code('V', 'Y', 'U', \ + 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */ + +#define GBM_FORMAT_AYUV \ + __gbm_fourcc_code('A', 'Y', 'U', \ + 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */ + +/* + * 2 plane YCbCr + * index 0 = Y plane, [7:0] Y + * index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian + * or + * index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian + */ +#define GBM_FORMAT_NV12 \ + __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */ +#define GBM_FORMAT_NV21 \ + __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */ +#define GBM_FORMAT_NV16 \ + __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */ +#define GBM_FORMAT_NV61 \ + __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */ + +/* + * 3 plane YCbCr + * index 0: Y plane, [7:0] Y + * index 1: Cb plane, [7:0] Cb + * index 2: Cr plane, [7:0] Cr + * or + * index 1: Cr plane, [7:0] Cr + * index 2: Cb plane, [7:0] Cb + */ +#define GBM_FORMAT_YUV410 \ + __gbm_fourcc_code('Y', 'U', 'V', \ + '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */ +#define GBM_FORMAT_YVU410 \ + __gbm_fourcc_code('Y', 'V', 'U', \ + '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */ +#define GBM_FORMAT_YUV411 \ + __gbm_fourcc_code('Y', 'U', '1', \ + '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */ +#define GBM_FORMAT_YVU411 \ + __gbm_fourcc_code('Y', 'V', '1', \ + '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */ +#define GBM_FORMAT_YUV420 \ + __gbm_fourcc_code('Y', 'U', '1', \ + '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */ +#define GBM_FORMAT_YVU420 \ + __gbm_fourcc_code('Y', 'V', '1', \ + '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */ +#define GBM_FORMAT_YUV422 \ + __gbm_fourcc_code('Y', 'U', '1', \ + '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */ +#define GBM_FORMAT_YVU422 \ + __gbm_fourcc_code('Y', 'V', '1', \ + '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */ +#define GBM_FORMAT_YUV444 \ + __gbm_fourcc_code('Y', 'U', '2', \ + '4') /* non-subsampled Cb (1) and Cr (2) planes */ +#define GBM_FORMAT_YVU444 \ + __gbm_fourcc_code('Y', 'V', '2', \ + '4') /* non-subsampled Cr (1) and Cb (2) planes */ + +struct gbm_format_name_desc { + char name[5]; +}; + +/** + * Flags to indicate the intended use for the buffer - these are passed into + * gbm_bo_create(). The caller must set the union of all the flags that are + * appropriate + * + * \sa Use gbm_device_is_format_supported() to check if the combination of + * format and use flags are supported + */ +enum gbm_bo_flags { + /** + * Buffer is going to be presented to the screen using an API such as KMS + */ + GBM_BO_USE_SCANOUT = (1 << 0), + /** + * Buffer is going to be used as cursor + */ + GBM_BO_USE_CURSOR = (1 << 1), + /** + * Deprecated + */ + GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR, + /** + * Buffer is to be used for rendering - for example it is going to be used + * as the storage for a color buffer + */ + GBM_BO_USE_RENDERING = (1 << 2), + /** + * Buffer can be used for gbm_bo_write. This is guaranteed to work + * with GBM_BO_USE_CURSOR, but may not work for other combinations. + */ + GBM_BO_USE_WRITE = (1 << 3), + /** + * Buffer is linear, i.e. not tiled. + */ + GBM_BO_USE_LINEAR = (1 << 4), +}; + +int gbm_device_get_fd(struct gbm_device* gbm); + +const char* gbm_device_get_backend_name(struct gbm_device* gbm); + +int gbm_device_is_format_supported(struct gbm_device* gbm, uint32_t format, + uint32_t usage); + +int gbm_device_get_format_modifier_plane_count(struct gbm_device* gbm, + uint32_t format, + uint64_t modifier); + +void gbm_device_destroy(struct gbm_device* gbm); + +struct gbm_device* gbm_create_device(int fd); + +struct gbm_bo* gbm_bo_create(struct gbm_device* gbm, uint32_t width, + uint32_t height, uint32_t format, uint32_t flags); + +struct gbm_bo* gbm_bo_create_with_modifiers(struct gbm_device* gbm, + uint32_t width, uint32_t height, + uint32_t format, + const uint64_t* modifiers, + const unsigned int count); +#define GBM_BO_IMPORT_WL_BUFFER 0x5501 +#define GBM_BO_IMPORT_EGL_IMAGE 0x5502 +#define GBM_BO_IMPORT_FD 0x5503 +#define GBM_BO_IMPORT_FD_MODIFIER 0x5504 + +struct gbm_import_fd_data { + int fd; + uint32_t width; + uint32_t height; + uint32_t stride; + uint32_t format; +}; + +struct gbm_import_fd_modifier_data { + uint32_t width; + uint32_t height; + uint32_t format; + uint32_t num_fds; + int fds[4]; + int strides[4]; + int offsets[4]; + uint64_t modifier; +}; + +struct gbm_bo* gbm_bo_import(struct gbm_device* gbm, uint32_t type, + void* buffer, uint32_t usage); + +/** + * Flags to indicate the type of mapping for the buffer - these are + * passed into gbm_bo_map(). The caller must set the union of all the + * flags that are appropriate. + * + * These flags are independent of the GBM_BO_USE_* creation flags. However, + * mapping the buffer may require copying to/from a staging buffer. + * + * See also: pipe_transfer_usage + */ +enum gbm_bo_transfer_flags { + /** + * Buffer contents read back (or accessed directly) at transfer + * create time. + */ + GBM_BO_TRANSFER_READ = (1 << 0), + /** + * Buffer contents will be written back at unmap time + * (or modified as a result of being accessed directly). + */ + GBM_BO_TRANSFER_WRITE = (1 << 1), + /** + * Read/modify/write + */ + GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE), +}; + +void* gbm_bo_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); + +void gbm_bo_unmap(struct gbm_bo* bo, void* map_data); + +uint32_t gbm_bo_get_width(struct gbm_bo* bo); + +uint32_t gbm_bo_get_height(struct gbm_bo* bo); + +uint32_t gbm_bo_get_stride(struct gbm_bo* bo); + +uint32_t gbm_bo_get_stride_for_plane(struct gbm_bo* bo, int plane); + +uint32_t gbm_bo_get_format(struct gbm_bo* bo); + +uint32_t gbm_bo_get_bpp(struct gbm_bo* bo); + +uint32_t gbm_bo_get_offset(struct gbm_bo* bo, int plane); + +struct gbm_device* gbm_bo_get_device(struct gbm_bo* bo); + +union gbm_bo_handle gbm_bo_get_handle(struct gbm_bo* bo); + +int gbm_bo_get_fd(struct gbm_bo* bo); + +uint64_t gbm_bo_get_modifier(struct gbm_bo* bo); + +int gbm_bo_get_plane_count(struct gbm_bo* bo); + +union gbm_bo_handle gbm_bo_get_handle_for_plane(struct gbm_bo* bo, int plane); + +int gbm_bo_write(struct gbm_bo* bo, const void* buf, size_t count); + +void gbm_bo_set_user_data(struct gbm_bo* bo, void* data, + void (*destroy_user_data)(struct gbm_bo*, void*)); + +void* gbm_bo_get_user_data(struct gbm_bo* bo); + +void gbm_bo_destroy(struct gbm_bo* bo); + +struct gbm_surface* gbm_surface_create(struct gbm_device* gbm, uint32_t width, + uint32_t height, uint32_t format, + uint32_t flags); + +struct gbm_surface* gbm_surface_create_with_modifiers( + struct gbm_device* gbm, uint32_t width, uint32_t height, uint32_t format, + const uint64_t* modifiers, const unsigned int count); + +struct gbm_bo* gbm_surface_lock_front_buffer(struct gbm_surface* surface); + +void gbm_surface_release_buffer(struct gbm_surface* surface, struct gbm_bo* bo); + +int gbm_surface_has_free_buffers(struct gbm_surface* surface); + +void gbm_surface_destroy(struct gbm_surface* surface); + +char* gbm_format_get_name(uint32_t gbm_format, + struct gbm_format_name_desc* desc); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/gtk-primary-selection-client-protocol.h b/widget/gtk/wayland/gtk-primary-selection-client-protocol.h new file mode 100644 index 0000000000..770c35e03f --- /dev/null +++ b/widget/gtk/wayland/gtk-primary-selection-client-protocol.h @@ -0,0 +1,580 @@ +/* Generated by wayland-scanner 1.18.0 */ + +#ifndef GTK_PRIMARY_SELECTION_CLIENT_PROTOCOL_H +#define GTK_PRIMARY_SELECTION_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_gtk_primary_selection The gtk_primary_selection protocol + * Primary selection protocol + * + * @section page_desc_gtk_primary_selection Description + * + * This protocol provides the ability to have a primary selection device to + * match that of the X server. This primary selection is a shortcut to the + * common clipboard selection, where text just needs to be selected in order + * to allow copying it elsewhere. The de facto way to perform this action + * is the middle mouse button, although it is not limited to this one. + * + * Clients wishing to honor primary selection should create a primary + * selection source and set it as the selection through + * wp_primary_selection_device.set_selection whenever the text selection + * changes. In order to minimize calls in pointer-driven text selection, + * it should happen only once after the operation finished. Similarly, + * a NULL source should be set when text is unselected. + * + * wp_primary_selection_offer objects are first announced through the + * wp_primary_selection_device.data_offer event. Immediately after this event, + * the primary data offer will emit wp_primary_selection_offer.offer events + * to let know of the mime types being offered. + * + * When the primary selection changes, the client with the keyboard focus + * will receive wp_primary_selection_device.selection events. Only the client + * with the keyboard focus will receive such events with a non-NULL + * wp_primary_selection_offer. Across keyboard focus changes, previously + * focused clients will receive wp_primary_selection_device.events with a + * NULL wp_primary_selection_offer. + * + * In order to request the primary selection data, the client must pass + * a recent serial pertaining to the press event that is triggering the + * operation, if the compositor deems the serial valid and recent, the + * wp_primary_selection_source.send event will happen in the other end + * to let the transfer begin. The client owning the primary selection + * should write the requested data, and close the file descriptor + * immediately. + * + * If the primary selection owner client disappeared during the transfer, + * the client reading the data will receive a + * wp_primary_selection_device.selection event with a NULL + * wp_primary_selection_offer, the client should take this as a hint + * to finish the reads related to the no longer existing offer. + * + * The primary selection owner should be checking for errors during + * writes, merely cancelling the ongoing transfer if any happened. + * + * @section page_ifaces_gtk_primary_selection Interfaces + * - @subpage page_iface_gtk_primary_selection_device_manager - X primary selection emulation + * - @subpage page_iface_gtk_primary_selection_device - + * - @subpage page_iface_gtk_primary_selection_offer - offer to transfer primary selection contents + * - @subpage page_iface_gtk_primary_selection_source - offer to replace the contents of the primary selection + * @section page_copyright_gtk_primary_selection Copyright + * <pre> + * + * Copyright © 2015, 2016 Red Hat + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * </pre> + */ +struct gtk_primary_selection_device; +struct gtk_primary_selection_device_manager; +struct gtk_primary_selection_offer; +struct gtk_primary_selection_source; +struct wl_seat; + +/** + * @page page_iface_gtk_primary_selection_device_manager gtk_primary_selection_device_manager + * @section page_iface_gtk_primary_selection_device_manager_desc Description + * + * The primary selection device manager is a singleton global object that + * provides access to the primary selection. It allows to create + * wp_primary_selection_source objects, as well as retrieving the per-seat + * wp_primary_selection_device objects. + * @section page_iface_gtk_primary_selection_device_manager_api API + * See @ref iface_gtk_primary_selection_device_manager. + */ +/** + * @defgroup iface_gtk_primary_selection_device_manager The gtk_primary_selection_device_manager interface + * + * The primary selection device manager is a singleton global object that + * provides access to the primary selection. It allows to create + * wp_primary_selection_source objects, as well as retrieving the per-seat + * wp_primary_selection_device objects. + */ +extern const struct wl_interface gtk_primary_selection_device_manager_interface; +/** + * @page page_iface_gtk_primary_selection_device gtk_primary_selection_device + * @section page_iface_gtk_primary_selection_device_api API + * See @ref iface_gtk_primary_selection_device. + */ +/** + * @defgroup iface_gtk_primary_selection_device The gtk_primary_selection_device interface + */ +extern const struct wl_interface gtk_primary_selection_device_interface; +/** + * @page page_iface_gtk_primary_selection_offer gtk_primary_selection_offer + * @section page_iface_gtk_primary_selection_offer_desc Description + * + * A wp_primary_selection_offer represents an offer to transfer the contents + * of the primary selection clipboard to the client. Similar to + * wl_data_offer, the offer also describes the mime types that the source + * will transferthat the + * data can be converted to and provides the mechanisms for transferring the + * data directly to the client. + * @section page_iface_gtk_primary_selection_offer_api API + * See @ref iface_gtk_primary_selection_offer. + */ +/** + * @defgroup iface_gtk_primary_selection_offer The gtk_primary_selection_offer interface + * + * A wp_primary_selection_offer represents an offer to transfer the contents + * of the primary selection clipboard to the client. Similar to + * wl_data_offer, the offer also describes the mime types that the source + * will transferthat the + * data can be converted to and provides the mechanisms for transferring the + * data directly to the client. + */ +extern const struct wl_interface gtk_primary_selection_offer_interface; +/** + * @page page_iface_gtk_primary_selection_source gtk_primary_selection_source + * @section page_iface_gtk_primary_selection_source_desc Description + * + * The source side of a wp_primary_selection_offer, it provides a way to + * describe the offered data and respond to requests to transfer the + * requested contents of the primary selection clipboard. + * @section page_iface_gtk_primary_selection_source_api API + * See @ref iface_gtk_primary_selection_source. + */ +/** + * @defgroup iface_gtk_primary_selection_source The gtk_primary_selection_source interface + * + * The source side of a wp_primary_selection_offer, it provides a way to + * describe the offered data and respond to requests to transfer the + * requested contents of the primary selection clipboard. + */ +extern const struct wl_interface gtk_primary_selection_source_interface; + +#define GTK_PRIMARY_SELECTION_DEVICE_MANAGER_CREATE_SOURCE 0 +#define GTK_PRIMARY_SELECTION_DEVICE_MANAGER_GET_DEVICE 1 +#define GTK_PRIMARY_SELECTION_DEVICE_MANAGER_DESTROY 2 + + +/** + * @ingroup iface_gtk_primary_selection_device_manager + */ +#define GTK_PRIMARY_SELECTION_DEVICE_MANAGER_CREATE_SOURCE_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_device_manager + */ +#define GTK_PRIMARY_SELECTION_DEVICE_MANAGER_GET_DEVICE_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_device_manager + */ +#define GTK_PRIMARY_SELECTION_DEVICE_MANAGER_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_gtk_primary_selection_device_manager */ +static inline void +gtk_primary_selection_device_manager_set_user_data(struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) gtk_primary_selection_device_manager, user_data); +} + +/** @ingroup iface_gtk_primary_selection_device_manager */ +static inline void * +gtk_primary_selection_device_manager_get_user_data(struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager) +{ + return wl_proxy_get_user_data((struct wl_proxy *) gtk_primary_selection_device_manager); +} + +static inline uint32_t +gtk_primary_selection_device_manager_get_version(struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager) +{ + return wl_proxy_get_version((struct wl_proxy *) gtk_primary_selection_device_manager); +} + +/** + * @ingroup iface_gtk_primary_selection_device_manager + * + * Create a new primary selection source. + */ +static inline struct gtk_primary_selection_source * +gtk_primary_selection_device_manager_create_source(struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) gtk_primary_selection_device_manager, + GTK_PRIMARY_SELECTION_DEVICE_MANAGER_CREATE_SOURCE, >k_primary_selection_source_interface, NULL); + + return (struct gtk_primary_selection_source *) id; +} + +/** + * @ingroup iface_gtk_primary_selection_device_manager + * + * Create a new data device for a given seat. + */ +static inline struct gtk_primary_selection_device * +gtk_primary_selection_device_manager_get_device(struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) gtk_primary_selection_device_manager, + GTK_PRIMARY_SELECTION_DEVICE_MANAGER_GET_DEVICE, >k_primary_selection_device_interface, NULL, seat); + + return (struct gtk_primary_selection_device *) id; +} + +/** + * @ingroup iface_gtk_primary_selection_device_manager + * + * Destroy the primary selection device manager. + */ +static inline void +gtk_primary_selection_device_manager_destroy(struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_device_manager, + GTK_PRIMARY_SELECTION_DEVICE_MANAGER_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) gtk_primary_selection_device_manager); +} + +/** + * @ingroup iface_gtk_primary_selection_device + * @struct gtk_primary_selection_device_listener + */ +struct gtk_primary_selection_device_listener { + /** + * introduce a new wp_primary_selection_offer + * + * Introduces a new wp_primary_selection_offer object that may be + * used to receive the current primary selection. Immediately + * following this event, the new wp_primary_selection_offer object + * will send wp_primary_selection_offer.offer events to describe + * the offered mime types. + */ + void (*data_offer)(void *data, + struct gtk_primary_selection_device *gtk_primary_selection_device, + struct gtk_primary_selection_offer *offer); + /** + * advertise a new primary selection + * + * The wp_primary_selection_device.selection event is sent to + * notify the client of a new primary selection. This event is sent + * after the wp_primary_selection.data_offer event introducing this + * object, and after the offer has announced its mimetypes through + * wp_primary_selection_offer.offer. + * + * The data_offer is valid until a new offer or NULL is received or + * until the client loses keyboard focus. The client must destroy + * the previous selection data_offer, if any, upon receiving this + * event. + */ + void (*selection)(void *data, + struct gtk_primary_selection_device *gtk_primary_selection_device, + struct gtk_primary_selection_offer *id); +}; + +/** + * @ingroup iface_gtk_primary_selection_device + */ +static inline int +gtk_primary_selection_device_add_listener(struct gtk_primary_selection_device *gtk_primary_selection_device, + const struct gtk_primary_selection_device_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) gtk_primary_selection_device, + (void (**)(void)) listener, data); +} + +#define GTK_PRIMARY_SELECTION_DEVICE_SET_SELECTION 0 +#define GTK_PRIMARY_SELECTION_DEVICE_DESTROY 1 + +/** + * @ingroup iface_gtk_primary_selection_device + */ +#define GTK_PRIMARY_SELECTION_DEVICE_DATA_OFFER_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_device + */ +#define GTK_PRIMARY_SELECTION_DEVICE_SELECTION_SINCE_VERSION 1 + +/** + * @ingroup iface_gtk_primary_selection_device + */ +#define GTK_PRIMARY_SELECTION_DEVICE_SET_SELECTION_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_device + */ +#define GTK_PRIMARY_SELECTION_DEVICE_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_gtk_primary_selection_device */ +static inline void +gtk_primary_selection_device_set_user_data(struct gtk_primary_selection_device *gtk_primary_selection_device, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) gtk_primary_selection_device, user_data); +} + +/** @ingroup iface_gtk_primary_selection_device */ +static inline void * +gtk_primary_selection_device_get_user_data(struct gtk_primary_selection_device *gtk_primary_selection_device) +{ + return wl_proxy_get_user_data((struct wl_proxy *) gtk_primary_selection_device); +} + +static inline uint32_t +gtk_primary_selection_device_get_version(struct gtk_primary_selection_device *gtk_primary_selection_device) +{ + return wl_proxy_get_version((struct wl_proxy *) gtk_primary_selection_device); +} + +/** + * @ingroup iface_gtk_primary_selection_device + * + * Replaces the current selection. The previous owner of the primary selection + * will receive a wp_primary_selection_source.cancelled event. + * + * To unset the selection, set the source to NULL. + */ +static inline void +gtk_primary_selection_device_set_selection(struct gtk_primary_selection_device *gtk_primary_selection_device, struct gtk_primary_selection_source *source, uint32_t serial) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_device, + GTK_PRIMARY_SELECTION_DEVICE_SET_SELECTION, source, serial); +} + +/** + * @ingroup iface_gtk_primary_selection_device + * + * Destroy the primary selection device. + */ +static inline void +gtk_primary_selection_device_destroy(struct gtk_primary_selection_device *gtk_primary_selection_device) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_device, + GTK_PRIMARY_SELECTION_DEVICE_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) gtk_primary_selection_device); +} + +/** + * @ingroup iface_gtk_primary_selection_offer + * @struct gtk_primary_selection_offer_listener + */ +struct gtk_primary_selection_offer_listener { + /** + * advertise offered mime type + * + * Sent immediately after creating announcing the + * wp_primary_selection_offer through + * wp_primary_selection_device.data_offer. One event is sent per + * offered mime type. + */ + void (*offer)(void *data, + struct gtk_primary_selection_offer *gtk_primary_selection_offer, + const char *mime_type); +}; + +/** + * @ingroup iface_gtk_primary_selection_offer + */ +static inline int +gtk_primary_selection_offer_add_listener(struct gtk_primary_selection_offer *gtk_primary_selection_offer, + const struct gtk_primary_selection_offer_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) gtk_primary_selection_offer, + (void (**)(void)) listener, data); +} + +#define GTK_PRIMARY_SELECTION_OFFER_RECEIVE 0 +#define GTK_PRIMARY_SELECTION_OFFER_DESTROY 1 + +/** + * @ingroup iface_gtk_primary_selection_offer + */ +#define GTK_PRIMARY_SELECTION_OFFER_OFFER_SINCE_VERSION 1 + +/** + * @ingroup iface_gtk_primary_selection_offer + */ +#define GTK_PRIMARY_SELECTION_OFFER_RECEIVE_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_offer + */ +#define GTK_PRIMARY_SELECTION_OFFER_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_gtk_primary_selection_offer */ +static inline void +gtk_primary_selection_offer_set_user_data(struct gtk_primary_selection_offer *gtk_primary_selection_offer, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) gtk_primary_selection_offer, user_data); +} + +/** @ingroup iface_gtk_primary_selection_offer */ +static inline void * +gtk_primary_selection_offer_get_user_data(struct gtk_primary_selection_offer *gtk_primary_selection_offer) +{ + return wl_proxy_get_user_data((struct wl_proxy *) gtk_primary_selection_offer); +} + +static inline uint32_t +gtk_primary_selection_offer_get_version(struct gtk_primary_selection_offer *gtk_primary_selection_offer) +{ + return wl_proxy_get_version((struct wl_proxy *) gtk_primary_selection_offer); +} + +/** + * @ingroup iface_gtk_primary_selection_offer + * + * To transfer the contents of the primary selection clipboard, the client + * issues this request and indicates the mime type that it wants to + * receive. The transfer happens through the passed file descriptor + * (typically created with the pipe system call). The source client writes + * the data in the mime type representation requested and then closes the + * file descriptor. + * + * The receiving client reads from the read end of the pipe until EOF and + * closes its end, at which point the transfer is complete. + */ +static inline void +gtk_primary_selection_offer_receive(struct gtk_primary_selection_offer *gtk_primary_selection_offer, const char *mime_type, int32_t fd) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_offer, + GTK_PRIMARY_SELECTION_OFFER_RECEIVE, mime_type, fd); +} + +/** + * @ingroup iface_gtk_primary_selection_offer + * + * Destroy the primary selection offer. + */ +static inline void +gtk_primary_selection_offer_destroy(struct gtk_primary_selection_offer *gtk_primary_selection_offer) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_offer, + GTK_PRIMARY_SELECTION_OFFER_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) gtk_primary_selection_offer); +} + +/** + * @ingroup iface_gtk_primary_selection_source + * @struct gtk_primary_selection_source_listener + */ +struct gtk_primary_selection_source_listener { + /** + * send the primary selection contents + * + * Request for the current primary selection contents from the + * client. Send the specified mime type over the passed file + * descriptor, then close it. + */ + void (*send)(void *data, + struct gtk_primary_selection_source *gtk_primary_selection_source, + const char *mime_type, + int32_t fd); + /** + * request for primary selection contents was canceled + * + * This primary selection source is no longer valid. The client + * should clean up and destroy this primary selection source. + */ + void (*cancelled)(void *data, + struct gtk_primary_selection_source *gtk_primary_selection_source); +}; + +/** + * @ingroup iface_gtk_primary_selection_source + */ +static inline int +gtk_primary_selection_source_add_listener(struct gtk_primary_selection_source *gtk_primary_selection_source, + const struct gtk_primary_selection_source_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) gtk_primary_selection_source, + (void (**)(void)) listener, data); +} + +#define GTK_PRIMARY_SELECTION_SOURCE_OFFER 0 +#define GTK_PRIMARY_SELECTION_SOURCE_DESTROY 1 + +/** + * @ingroup iface_gtk_primary_selection_source + */ +#define GTK_PRIMARY_SELECTION_SOURCE_SEND_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_source + */ +#define GTK_PRIMARY_SELECTION_SOURCE_CANCELLED_SINCE_VERSION 1 + +/** + * @ingroup iface_gtk_primary_selection_source + */ +#define GTK_PRIMARY_SELECTION_SOURCE_OFFER_SINCE_VERSION 1 +/** + * @ingroup iface_gtk_primary_selection_source + */ +#define GTK_PRIMARY_SELECTION_SOURCE_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_gtk_primary_selection_source */ +static inline void +gtk_primary_selection_source_set_user_data(struct gtk_primary_selection_source *gtk_primary_selection_source, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) gtk_primary_selection_source, user_data); +} + +/** @ingroup iface_gtk_primary_selection_source */ +static inline void * +gtk_primary_selection_source_get_user_data(struct gtk_primary_selection_source *gtk_primary_selection_source) +{ + return wl_proxy_get_user_data((struct wl_proxy *) gtk_primary_selection_source); +} + +static inline uint32_t +gtk_primary_selection_source_get_version(struct gtk_primary_selection_source *gtk_primary_selection_source) +{ + return wl_proxy_get_version((struct wl_proxy *) gtk_primary_selection_source); +} + +/** + * @ingroup iface_gtk_primary_selection_source + * + * This request adds a mime type to the set of mime types advertised to + * targets. Can be called several times to offer multiple types. + */ +static inline void +gtk_primary_selection_source_offer(struct gtk_primary_selection_source *gtk_primary_selection_source, const char *mime_type) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_source, + GTK_PRIMARY_SELECTION_SOURCE_OFFER, mime_type); +} + +/** + * @ingroup iface_gtk_primary_selection_source + * + * Destroy the primary selection source. + */ +static inline void +gtk_primary_selection_source_destroy(struct gtk_primary_selection_source *gtk_primary_selection_source) +{ + wl_proxy_marshal((struct wl_proxy *) gtk_primary_selection_source, + GTK_PRIMARY_SELECTION_SOURCE_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) gtk_primary_selection_source); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/gtk-primary-selection-protocol.c b/widget/gtk/wayland/gtk-primary-selection-protocol.c new file mode 100644 index 0000000000..8a1166d2a2 --- /dev/null +++ b/widget/gtk/wayland/gtk-primary-selection-protocol.c @@ -0,0 +1,115 @@ +/* Generated by wayland-scanner 1.18.0 */ + +/* + * Copyright © 2015, 2016 Red Hat + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <stdint.h> +#include <gdk/gdkwayland.h> +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface gtk_primary_selection_device_interface; +extern const struct wl_interface gtk_primary_selection_offer_interface; +extern const struct wl_interface gtk_primary_selection_source_interface; +extern const struct wl_interface wl_seat_interface; + +static const struct wl_interface *gtk_primary_selection_types[] = { + NULL, + NULL, + >k_primary_selection_source_interface, + >k_primary_selection_device_interface, + &wl_seat_interface, + >k_primary_selection_source_interface, + NULL, + >k_primary_selection_offer_interface, + >k_primary_selection_offer_interface, +}; + +static const struct wl_message gtk_primary_selection_device_manager_requests[] = { + { "create_source", "n", gtk_primary_selection_types + 2 }, + { "get_device", "no", gtk_primary_selection_types + 3 }, + { "destroy", "", gtk_primary_selection_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface gtk_primary_selection_device_manager_interface = { + "gtk_primary_selection_device_manager", 1, + 3, gtk_primary_selection_device_manager_requests, + 0, NULL, +}; + +static const struct wl_message gtk_primary_selection_device_requests[] = { + { "set_selection", "?ou", gtk_primary_selection_types + 5 }, + { "destroy", "", gtk_primary_selection_types + 0 }, +}; + +static const struct wl_message gtk_primary_selection_device_events[] = { + { "data_offer", "n", gtk_primary_selection_types + 7 }, + { "selection", "?o", gtk_primary_selection_types + 8 }, +}; + +WL_PRIVATE const struct wl_interface gtk_primary_selection_device_interface = { + "gtk_primary_selection_device", 1, + 2, gtk_primary_selection_device_requests, + 2, gtk_primary_selection_device_events, +}; + +static const struct wl_message gtk_primary_selection_offer_requests[] = { + { "receive", "sh", gtk_primary_selection_types + 0 }, + { "destroy", "", gtk_primary_selection_types + 0 }, +}; + +static const struct wl_message gtk_primary_selection_offer_events[] = { + { "offer", "s", gtk_primary_selection_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface gtk_primary_selection_offer_interface = { + "gtk_primary_selection_offer", 1, + 2, gtk_primary_selection_offer_requests, + 1, gtk_primary_selection_offer_events, +}; + +static const struct wl_message gtk_primary_selection_source_requests[] = { + { "offer", "s", gtk_primary_selection_types + 0 }, + { "destroy", "", gtk_primary_selection_types + 0 }, +}; + +static const struct wl_message gtk_primary_selection_source_events[] = { + { "send", "sh", gtk_primary_selection_types + 0 }, + { "cancelled", "", gtk_primary_selection_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface gtk_primary_selection_source_interface = { + "gtk_primary_selection_source", 1, + 2, gtk_primary_selection_source_requests, + 2, gtk_primary_selection_source_events, +}; diff --git a/widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h b/widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h new file mode 100644 index 0000000000..3ae2303b3c --- /dev/null +++ b/widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h @@ -0,0 +1,228 @@ +/* Generated by wayland-scanner 1.16.0 */ + +#ifndef IDLE_INHIBIT_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define IDLE_INHIBIT_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_idle_inhibit_unstable_v1 The idle_inhibit_unstable_v1 protocol + * @section page_ifaces_idle_inhibit_unstable_v1 Interfaces + * - @subpage page_iface_zwp_idle_inhibit_manager_v1 - control behavior when + * display idles + * - @subpage page_iface_zwp_idle_inhibitor_v1 - context object for inhibiting + * idle behavior + * @section page_copyright_idle_inhibit_unstable_v1 Copyright + * <pre> + * + * Copyright © 2015 Samsung Electronics Co., Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * </pre> + */ +struct wl_surface; +struct zwp_idle_inhibit_manager_v1; +struct zwp_idle_inhibitor_v1; + +/** + * @page page_iface_zwp_idle_inhibit_manager_v1 zwp_idle_inhibit_manager_v1 + * @section page_iface_zwp_idle_inhibit_manager_v1_desc Description + * + * This interface permits inhibiting the idle behavior such as screen + * blanking, locking, and screensaving. The client binds the idle manager + * globally, then creates idle-inhibitor objects for each surface. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * @section page_iface_zwp_idle_inhibit_manager_v1_api API + * See @ref iface_zwp_idle_inhibit_manager_v1. + */ +/** + * @defgroup iface_zwp_idle_inhibit_manager_v1 The zwp_idle_inhibit_manager_v1 + * interface + * + * This interface permits inhibiting the idle behavior such as screen + * blanking, locking, and screensaving. The client binds the idle manager + * globally, then creates idle-inhibitor objects for each surface. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + */ +extern const struct wl_interface zwp_idle_inhibit_manager_v1_interface; +/** + * @page page_iface_zwp_idle_inhibitor_v1 zwp_idle_inhibitor_v1 + * @section page_iface_zwp_idle_inhibitor_v1_desc Description + * + * An idle inhibitor prevents the output that the associated surface is + * visible on from being set to a state where it is not visually usable due + * to lack of user interaction (e.g. blanked, dimmed, locked, set to power + * save, etc.) Any screensaver processes are also blocked from displaying. + * + * If the surface is destroyed, unmapped, becomes occluded, loses + * visibility, or otherwise becomes not visually relevant for the user, the + * idle inhibitor will not be honored by the compositor; if the surface + * subsequently regains visibility the inhibitor takes effect once again. + * Likewise, the inhibitor isn't honored if the system was already idled at + * the time the inhibitor was established, although if the system later + * de-idles and re-idles the inhibitor will take effect. + * @section page_iface_zwp_idle_inhibitor_v1_api API + * See @ref iface_zwp_idle_inhibitor_v1. + */ +/** + * @defgroup iface_zwp_idle_inhibitor_v1 The zwp_idle_inhibitor_v1 interface + * + * An idle inhibitor prevents the output that the associated surface is + * visible on from being set to a state where it is not visually usable due + * to lack of user interaction (e.g. blanked, dimmed, locked, set to power + * save, etc.) Any screensaver processes are also blocked from displaying. + * + * If the surface is destroyed, unmapped, becomes occluded, loses + * visibility, or otherwise becomes not visually relevant for the user, the + * idle inhibitor will not be honored by the compositor; if the surface + * subsequently regains visibility the inhibitor takes effect once again. + * Likewise, the inhibitor isn't honored if the system was already idled at + * the time the inhibitor was established, although if the system later + * de-idles and re-idles the inhibitor will take effect. + */ +extern const struct wl_interface zwp_idle_inhibitor_v1_interface; + +#define ZWP_IDLE_INHIBIT_MANAGER_V1_DESTROY 0 +#define ZWP_IDLE_INHIBIT_MANAGER_V1_CREATE_INHIBITOR 1 + +/** + * @ingroup iface_zwp_idle_inhibit_manager_v1 + */ +#define ZWP_IDLE_INHIBIT_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_idle_inhibit_manager_v1 + */ +#define ZWP_IDLE_INHIBIT_MANAGER_V1_CREATE_INHIBITOR_SINCE_VERSION 1 + +/** @ingroup iface_zwp_idle_inhibit_manager_v1 */ +static inline void zwp_idle_inhibit_manager_v1_set_user_data( + struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1, + void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_idle_inhibit_manager_v1, + user_data); +} + +/** @ingroup iface_zwp_idle_inhibit_manager_v1 */ +static inline void* zwp_idle_inhibit_manager_v1_get_user_data( + struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_idle_inhibit_manager_v1); +} + +static inline uint32_t zwp_idle_inhibit_manager_v1_get_version( + struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_idle_inhibit_manager_v1); +} + +/** + * @ingroup iface_zwp_idle_inhibit_manager_v1 + * + * Destroy the inhibit manager. + */ +static inline void zwp_idle_inhibit_manager_v1_destroy( + struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_idle_inhibit_manager_v1, + ZWP_IDLE_INHIBIT_MANAGER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_idle_inhibit_manager_v1); +} + +/** + * @ingroup iface_zwp_idle_inhibit_manager_v1 + * + * Create a new inhibitor object associated with the given surface. + */ +static inline struct zwp_idle_inhibitor_v1* +zwp_idle_inhibit_manager_v1_create_inhibitor( + struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1, + struct wl_surface* surface) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor( + (struct wl_proxy*)zwp_idle_inhibit_manager_v1, + ZWP_IDLE_INHIBIT_MANAGER_V1_CREATE_INHIBITOR, + &zwp_idle_inhibitor_v1_interface, NULL, surface); + + return (struct zwp_idle_inhibitor_v1*)id; +} + +#define ZWP_IDLE_INHIBITOR_V1_DESTROY 0 + +/** + * @ingroup iface_zwp_idle_inhibitor_v1 + */ +#define ZWP_IDLE_INHIBITOR_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwp_idle_inhibitor_v1 */ +static inline void zwp_idle_inhibitor_v1_set_user_data( + struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_idle_inhibitor_v1, user_data); +} + +/** @ingroup iface_zwp_idle_inhibitor_v1 */ +static inline void* zwp_idle_inhibitor_v1_get_user_data( + struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_idle_inhibitor_v1); +} + +static inline uint32_t zwp_idle_inhibitor_v1_get_version( + struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_idle_inhibitor_v1); +} + +/** + * @ingroup iface_zwp_idle_inhibitor_v1 + * + * Remove the inhibitor effect from the associated wl_surface. + */ +static inline void zwp_idle_inhibitor_v1_destroy( + struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_idle_inhibitor_v1, + ZWP_IDLE_INHIBITOR_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_idle_inhibitor_v1); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c b/widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c new file mode 100644 index 0000000000..579095e003 --- /dev/null +++ b/widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c @@ -0,0 +1,60 @@ +/* Generated by wayland-scanner 1.16.0 */ + +/* + * Copyright © 2015 Samsung Electronics Co., Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <stdint.h> +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#pragma GCC visibility push(default) +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwp_idle_inhibitor_v1_interface; +#pragma GCC visibility pop + +static const struct wl_interface* types[] = { + &zwp_idle_inhibitor_v1_interface, + &wl_surface_interface, +}; + +static const struct wl_message zwp_idle_inhibit_manager_v1_requests[] = { + {"destroy", "", types + 0}, + {"create_inhibitor", "no", types + 0}, +}; + +const struct wl_interface zwp_idle_inhibit_manager_v1_interface = { + "zwp_idle_inhibit_manager_v1", 1, 2, + zwp_idle_inhibit_manager_v1_requests, 0, NULL, +}; + +static const struct wl_message zwp_idle_inhibitor_v1_requests[] = { + {"destroy", "", types + 0}, +}; + +const struct wl_interface zwp_idle_inhibitor_v1_interface = { + "zwp_idle_inhibitor_v1", 1, 1, zwp_idle_inhibitor_v1_requests, 0, NULL, +}; diff --git a/widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h b/widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h new file mode 100644 index 0000000000..cff0426a9c --- /dev/null +++ b/widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h @@ -0,0 +1,650 @@ +/* Generated by wayland-scanner 1.17.0 */ + +#ifndef LINUX_DMABUF_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define LINUX_DMABUF_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_linux_dmabuf_unstable_v1 The linux_dmabuf_unstable_v1 protocol + * @section page_ifaces_linux_dmabuf_unstable_v1 Interfaces + * - @subpage page_iface_zwp_linux_dmabuf_v1 - factory for creating dmabuf-based + * wl_buffers + * - @subpage page_iface_zwp_linux_buffer_params_v1 - parameters for creating a + * dmabuf-based wl_buffer + * @section page_copyright_linux_dmabuf_unstable_v1 Copyright + * <pre> + * + * Copyright © 2014, 2015 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * </pre> + */ +struct wl_buffer; +struct zwp_linux_buffer_params_v1; +struct zwp_linux_dmabuf_v1; + +/** + * @page page_iface_zwp_linux_dmabuf_v1 zwp_linux_dmabuf_v1 + * @section page_iface_zwp_linux_dmabuf_v1_desc Description + * + * Following the interfaces from: + * https://www.khronos.org/registry/egl/extensions/EXT/EGL_EXT_image_dma_buf_import.txt + * and the Linux DRM sub-system's AddFb2 ioctl. + * + * This interface offers ways to create generic dmabuf-based + * wl_buffers. Immediately after a client binds to this interface, + * the set of supported formats and format modifiers is sent with + * 'format' and 'modifier' events. + * + * The following are required from clients: + * + * - Clients must ensure that either all data in the dma-buf is + * coherent for all subsequent read access or that coherency is + * correctly handled by the underlying kernel-side dma-buf + * implementation. + * + * - Don't make any more attachments after sending the buffer to the + * compositor. Making more attachments later increases the risk of + * the compositor not being able to use (re-import) an existing + * dmabuf-based wl_buffer. + * + * The underlying graphics stack must ensure the following: + * + * - The dmabuf file descriptors relayed to the server will stay valid + * for the whole lifetime of the wl_buffer. This means the server may + * at any time use those fds to import the dmabuf into any kernel + * sub-system that might accept it. + * + * To create a wl_buffer from one or more dmabufs, a client creates a + * zwp_linux_dmabuf_params_v1 object with a zwp_linux_dmabuf_v1.create_params + * request. All planes required by the intended format are added with + * the 'add' request. Finally, a 'create' or 'create_immed' request is + * issued, which has the following outcome depending on the import success. + * + * The 'create' request, + * - on success, triggers a 'created' event which provides the final + * wl_buffer to the client. + * - on failure, triggers a 'failed' event to convey that the server + * cannot use the dmabufs received from the client. + * + * For the 'create_immed' request, + * - on success, the server immediately imports the added dmabufs to + * create a wl_buffer. No event is sent from the server in this case. + * - on failure, the server can choose to either: + * - terminate the client by raising a fatal error. + * - mark the wl_buffer as failed, and send a 'failed' event to the + * client. If the client uses a failed wl_buffer as an argument to any + * request, the behaviour is compositor implementation-defined. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * @section page_iface_zwp_linux_dmabuf_v1_api API + * See @ref iface_zwp_linux_dmabuf_v1. + */ +/** + * @defgroup iface_zwp_linux_dmabuf_v1 The zwp_linux_dmabuf_v1 interface + * + * Following the interfaces from: + * https://www.khronos.org/registry/egl/extensions/EXT/EGL_EXT_image_dma_buf_import.txt + * and the Linux DRM sub-system's AddFb2 ioctl. + * + * This interface offers ways to create generic dmabuf-based + * wl_buffers. Immediately after a client binds to this interface, + * the set of supported formats and format modifiers is sent with + * 'format' and 'modifier' events. + * + * The following are required from clients: + * + * - Clients must ensure that either all data in the dma-buf is + * coherent for all subsequent read access or that coherency is + * correctly handled by the underlying kernel-side dma-buf + * implementation. + * + * - Don't make any more attachments after sending the buffer to the + * compositor. Making more attachments later increases the risk of + * the compositor not being able to use (re-import) an existing + * dmabuf-based wl_buffer. + * + * The underlying graphics stack must ensure the following: + * + * - The dmabuf file descriptors relayed to the server will stay valid + * for the whole lifetime of the wl_buffer. This means the server may + * at any time use those fds to import the dmabuf into any kernel + * sub-system that might accept it. + * + * To create a wl_buffer from one or more dmabufs, a client creates a + * zwp_linux_dmabuf_params_v1 object with a zwp_linux_dmabuf_v1.create_params + * request. All planes required by the intended format are added with + * the 'add' request. Finally, a 'create' or 'create_immed' request is + * issued, which has the following outcome depending on the import success. + * + * The 'create' request, + * - on success, triggers a 'created' event which provides the final + * wl_buffer to the client. + * - on failure, triggers a 'failed' event to convey that the server + * cannot use the dmabufs received from the client. + * + * For the 'create_immed' request, + * - on success, the server immediately imports the added dmabufs to + * create a wl_buffer. No event is sent from the server in this case. + * - on failure, the server can choose to either: + * - terminate the client by raising a fatal error. + * - mark the wl_buffer as failed, and send a 'failed' event to the + * client. If the client uses a failed wl_buffer as an argument to any + * request, the behaviour is compositor implementation-defined. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + */ +extern const struct wl_interface zwp_linux_dmabuf_v1_interface; +/** + * @page page_iface_zwp_linux_buffer_params_v1 zwp_linux_buffer_params_v1 + * @section page_iface_zwp_linux_buffer_params_v1_desc Description + * + * This temporary object is a collection of dmabufs and other + * parameters that together form a single logical buffer. The temporary + * object may eventually create one wl_buffer unless cancelled by + * destroying it before requesting 'create'. + * + * Single-planar formats only require one dmabuf, however + * multi-planar formats may require more than one dmabuf. For all + * formats, an 'add' request must be called once per plane (even if the + * underlying dmabuf fd is identical). + * + * You must use consecutive plane indices ('plane_idx' argument for 'add') + * from zero to the number of planes used by the drm_fourcc format code. + * All planes required by the format must be given exactly once, but can + * be given in any order. Each plane index can be set only once. + * @section page_iface_zwp_linux_buffer_params_v1_api API + * See @ref iface_zwp_linux_buffer_params_v1. + */ +/** + * @defgroup iface_zwp_linux_buffer_params_v1 The zwp_linux_buffer_params_v1 + * interface + * + * This temporary object is a collection of dmabufs and other + * parameters that together form a single logical buffer. The temporary + * object may eventually create one wl_buffer unless cancelled by + * destroying it before requesting 'create'. + * + * Single-planar formats only require one dmabuf, however + * multi-planar formats may require more than one dmabuf. For all + * formats, an 'add' request must be called once per plane (even if the + * underlying dmabuf fd is identical). + * + * You must use consecutive plane indices ('plane_idx' argument for 'add') + * from zero to the number of planes used by the drm_fourcc format code. + * All planes required by the format must be given exactly once, but can + * be given in any order. Each plane index can be set only once. + */ +extern const struct wl_interface zwp_linux_buffer_params_v1_interface; + +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + * @struct zwp_linux_dmabuf_v1_listener + */ +struct zwp_linux_dmabuf_v1_listener { + /** + * supported buffer format + * + * This event advertises one buffer format that the server + * supports. All the supported formats are advertised once when the + * client binds to this interface. A roundtrip after binding + * guarantees that the client has received all supported formats. + * + * For the definition of the format codes, see the + * zwp_linux_buffer_params_v1::create request. + * + * Warning: the 'format' event is likely to be deprecated and + * replaced with the 'modifier' event introduced in + * zwp_linux_dmabuf_v1 version 3, described below. Please refrain + * from using the information received from this event. + * @param format DRM_FORMAT code + */ + void (*format)(void* data, struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, + uint32_t format); + /** + * supported buffer format modifier + * + * This event advertises the formats that the server supports, + * along with the modifiers supported for each format. All the + * supported modifiers for all the supported formats are advertised + * once when the client binds to this interface. A roundtrip after + * binding guarantees that the client has received all supported + * format-modifier pairs. + * + * For the definition of the format and modifier codes, see the + * zwp_linux_buffer_params_v1::create request. + * @param format DRM_FORMAT code + * @param modifier_hi high 32 bits of layout modifier + * @param modifier_lo low 32 bits of layout modifier + * @since 3 + */ + void (*modifier)(void* data, struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, + uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo); +}; + +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + */ +static inline int zwp_linux_dmabuf_v1_add_listener( + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, + const struct zwp_linux_dmabuf_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)zwp_linux_dmabuf_v1, + (void (**)(void))listener, data); +} + +#define ZWP_LINUX_DMABUF_V1_DESTROY 0 +#define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS 1 + +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + */ +#define ZWP_LINUX_DMABUF_V1_FORMAT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + */ +#define ZWP_LINUX_DMABUF_V1_MODIFIER_SINCE_VERSION 3 + +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + */ +#define ZWP_LINUX_DMABUF_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + */ +#define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS_SINCE_VERSION 1 + +/** @ingroup iface_zwp_linux_dmabuf_v1 */ +static inline void zwp_linux_dmabuf_v1_set_user_data( + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_linux_dmabuf_v1, user_data); +} + +/** @ingroup iface_zwp_linux_dmabuf_v1 */ +static inline void* zwp_linux_dmabuf_v1_get_user_data( + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_linux_dmabuf_v1); +} + +static inline uint32_t zwp_linux_dmabuf_v1_get_version( + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_linux_dmabuf_v1); +} + +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + * + * Objects created through this interface, especially wl_buffers, will + * remain valid. + */ +static inline void zwp_linux_dmabuf_v1_destroy( + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_linux_dmabuf_v1, + ZWP_LINUX_DMABUF_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_linux_dmabuf_v1); +} + +/** + * @ingroup iface_zwp_linux_dmabuf_v1 + * + * This temporary object is used to collect multiple dmabuf handles into + * a single batch to create a wl_buffer. It can only be used once and + * should be destroyed after a 'created' or 'failed' event has been + * received. + */ +static inline struct zwp_linux_buffer_params_v1* +zwp_linux_dmabuf_v1_create_params( + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) { + struct wl_proxy* params_id; + + params_id = wl_proxy_marshal_constructor( + (struct wl_proxy*)zwp_linux_dmabuf_v1, ZWP_LINUX_DMABUF_V1_CREATE_PARAMS, + &zwp_linux_buffer_params_v1_interface, NULL); + + return (struct zwp_linux_buffer_params_v1*)params_id; +} + +#ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM +# define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM +enum zwp_linux_buffer_params_v1_error { + /** + * the dmabuf_batch object has already been used to create a wl_buffer + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ALREADY_USED = 0, + /** + * plane index out of bounds + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX = 1, + /** + * the plane index was already set + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_SET = 2, + /** + * missing or too many planes to create a buffer + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INCOMPLETE = 3, + /** + * format not supported + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_FORMAT = 4, + /** + * invalid width or height + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DIMENSIONS = 5, + /** + * offset + stride * height goes out of dmabuf bounds + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_OUT_OF_BOUNDS = 6, + /** + * invalid wl_buffer resulted from importing dmabufs via the + * create_immed request on given buffer_params + */ + ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER = 7, +}; +#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM */ + +#ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM +# define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM +enum zwp_linux_buffer_params_v1_flags { + /** + * contents are y-inverted + */ + ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT = 1, + /** + * content is interlaced + */ + ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_INTERLACED = 2, + /** + * bottom field first + */ + ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST = 4, +}; +#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM */ + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + * @struct zwp_linux_buffer_params_v1_listener + */ +struct zwp_linux_buffer_params_v1_listener { + /** + * buffer creation succeeded + * + * This event indicates that the attempted buffer creation was + * successful. It provides the new wl_buffer referencing the + * dmabuf(s). + * + * Upon receiving this event, the client should destroy the + * zlinux_dmabuf_params object. + * @param buffer the newly created wl_buffer + */ + void (*created)(void* data, + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, + struct wl_buffer* buffer); + /** + * buffer creation failed + * + * This event indicates that the attempted buffer creation has + * failed. It usually means that one of the dmabuf constraints has + * not been fulfilled. + * + * Upon receiving this event, the client should destroy the + * zlinux_buffer_params object. + */ + void (*failed)(void* data, + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1); +}; + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +static inline int zwp_linux_buffer_params_v1_add_listener( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, + const struct zwp_linux_buffer_params_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)zwp_linux_buffer_params_v1, + (void (**)(void))listener, data); +} + +#define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY 0 +#define ZWP_LINUX_BUFFER_PARAMS_V1_ADD 1 +#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE 2 +#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED 3 + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +#define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +#define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +#define ZWP_LINUX_BUFFER_PARAMS_V1_ADD_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + */ +#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED_SINCE_VERSION 2 + +/** @ingroup iface_zwp_linux_buffer_params_v1 */ +static inline void zwp_linux_buffer_params_v1_set_user_data( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, + void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_linux_buffer_params_v1, + user_data); +} + +/** @ingroup iface_zwp_linux_buffer_params_v1 */ +static inline void* zwp_linux_buffer_params_v1_get_user_data( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_linux_buffer_params_v1); +} + +static inline uint32_t zwp_linux_buffer_params_v1_get_version( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_linux_buffer_params_v1); +} + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + * + * Cleans up the temporary data sent to the server for dmabuf-based + * wl_buffer creation. + */ +static inline void zwp_linux_buffer_params_v1_destroy( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_linux_buffer_params_v1, + ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_linux_buffer_params_v1); +} + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + * + * This request adds one dmabuf to the set in this + * zwp_linux_buffer_params_v1. + * + * The 64-bit unsigned value combined from modifier_hi and modifier_lo + * is the dmabuf layout modifier. DRM AddFB2 ioctl calls this the + * fb modifier, which is defined in drm_mode.h of Linux UAPI. + * This is an opaque token. Drivers use this token to express tiling, + * compression, etc. driver-specific modifications to the base format + * defined by the DRM fourcc code. + * + * This request raises the PLANE_IDX error if plane_idx is too large. + * The error PLANE_SET is raised if attempting to set a plane that + * was already set. + */ +static inline void zwp_linux_buffer_params_v1_add( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, int32_t fd, + uint32_t plane_idx, uint32_t offset, uint32_t stride, uint32_t modifier_hi, + uint32_t modifier_lo) { + wl_proxy_marshal((struct wl_proxy*)zwp_linux_buffer_params_v1, + ZWP_LINUX_BUFFER_PARAMS_V1_ADD, fd, plane_idx, offset, + stride, modifier_hi, modifier_lo); +} + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + * + * This asks for creation of a wl_buffer from the added dmabuf + * buffers. The wl_buffer is not created immediately but returned via + * the 'created' event if the dmabuf sharing succeeds. The sharing + * may fail at runtime for reasons a client cannot predict, in + * which case the 'failed' event is triggered. + * + * The 'format' argument is a DRM_FORMAT code, as defined by the + * libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the + * authoritative source on how the format codes should work. + * + * The 'flags' is a bitfield of the flags defined in enum "flags". + * 'y_invert' means the that the image needs to be y-flipped. + * + * Flag 'interlaced' means that the frame in the buffer is not + * progressive as usual, but interlaced. An interlaced buffer as + * supported here must always contain both top and bottom fields. + * The top field always begins on the first pixel row. The temporal + * ordering between the two fields is top field first, unless + * 'bottom_first' is specified. It is undefined whether 'bottom_first' + * is ignored if 'interlaced' is not set. + * + * This protocol does not convey any information about field rate, + * duration, or timing, other than the relative ordering between the + * two fields in one buffer. A compositor may have to estimate the + * intended field rate from the incoming buffer rate. It is undefined + * whether the time of receiving wl_surface.commit with a new buffer + * attached, applying the wl_surface state, wl_surface.frame callback + * trigger, presentation, or any other point in the compositor cycle + * is used to measure the frame or field times. There is no support + * for detecting missed or late frames/fields/buffers either, and + * there is no support whatsoever for cooperating with interlaced + * compositor output. + * + * The composited image quality resulting from the use of interlaced + * buffers is explicitly undefined. A compositor may use elaborate + * hardware features or software to deinterlace and create progressive + * output frames from a sequence of interlaced input buffers, or it + * may produce substandard image quality. However, compositors that + * cannot guarantee reasonable image quality in all cases are recommended + * to just reject all interlaced buffers. + * + * Any argument errors, including non-positive width or height, + * mismatch between the number of planes and the format, bad + * format, bad offset or stride, may be indicated by fatal protocol + * errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, + * OUT_OF_BOUNDS. + * + * Dmabuf import errors in the server that are not obvious client + * bugs are returned via the 'failed' event as non-fatal. This + * allows attempting dmabuf sharing and falling back in the client + * if it fails. + * + * This request can be sent only once in the object's lifetime, after + * which the only legal request is destroy. This object should be + * destroyed after issuing a 'create' request. Attempting to use this + * object after issuing 'create' raises ALREADY_USED protocol error. + * + * It is not mandatory to issue 'create'. If a client wants to + * cancel the buffer creation, it can just destroy this object. + */ +static inline void zwp_linux_buffer_params_v1_create( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, + int32_t width, int32_t height, uint32_t format, uint32_t flags) { + wl_proxy_marshal((struct wl_proxy*)zwp_linux_buffer_params_v1, + ZWP_LINUX_BUFFER_PARAMS_V1_CREATE, width, height, format, + flags); +} + +/** + * @ingroup iface_zwp_linux_buffer_params_v1 + * + * This asks for immediate creation of a wl_buffer by importing the + * added dmabufs. + * + * In case of import success, no event is sent from the server, and the + * wl_buffer is ready to be used by the client. + * + * Upon import failure, either of the following may happen, as seen fit + * by the implementation: + * - the client is terminated with one of the following fatal protocol + * errors: + * - INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS, + * in case of argument errors such as mismatch between the number + * of planes and the format, bad format, non-positive width or + * height, or bad offset or stride. + * - INVALID_WL_BUFFER, in case the cause for failure is unknown or + * plaform specific. + * - the server creates an invalid wl_buffer, marks it as failed and + * sends a 'failed' event to the client. The result of using this + * invalid wl_buffer as an argument in any request by the client is + * defined by the compositor implementation. + * + * This takes the same arguments as a 'create' request, and obeys the + * same restrictions. + */ +static inline struct wl_buffer* zwp_linux_buffer_params_v1_create_immed( + struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, + int32_t width, int32_t height, uint32_t format, uint32_t flags) { + struct wl_proxy* buffer_id; + + buffer_id = wl_proxy_marshal_constructor( + (struct wl_proxy*)zwp_linux_buffer_params_v1, + ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED, &wl_buffer_interface, NULL, + width, height, format, flags); + + return (struct wl_buffer*)buffer_id; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c b/widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c new file mode 100644 index 0000000000..51c1e8e575 --- /dev/null +++ b/widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c @@ -0,0 +1,81 @@ +/* Generated by wayland-scanner 1.17.0 */ + +/* + * Copyright © 2014, 2015 Collabora, Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <stdint.h> +#include "wayland-util.h" + +#pragma GCC visibility push(default) +extern const struct wl_interface wl_buffer_interface; +extern const struct wl_interface zwp_linux_buffer_params_v1_interface; +#pragma GCC visibility pop + +static const struct wl_interface* types[] = { + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + &zwp_linux_buffer_params_v1_interface, + &wl_buffer_interface, + NULL, + NULL, + NULL, + NULL, + &wl_buffer_interface, +}; + +static const struct wl_message zwp_linux_dmabuf_v1_requests[] = { + {"destroy", "", types + 0}, + {"create_params", "n", types + 6}, +}; + +static const struct wl_message zwp_linux_dmabuf_v1_events[] = { + {"format", "u", types + 0}, + {"modifier", "3uuu", types + 0}, +}; + +const struct wl_interface zwp_linux_dmabuf_v1_interface = { + "zwp_linux_dmabuf_v1", 3, 2, + zwp_linux_dmabuf_v1_requests, 2, zwp_linux_dmabuf_v1_events, +}; + +static const struct wl_message zwp_linux_buffer_params_v1_requests[] = { + {"destroy", "", types + 0}, + {"add", "huuuuu", types + 0}, + {"create", "iiuu", types + 0}, + {"create_immed", "2niiuu", types + 7}, +}; + +static const struct wl_message zwp_linux_buffer_params_v1_events[] = { + {"created", "n", types + 12}, + {"failed", "", types + 0}, +}; + +const struct wl_interface zwp_linux_buffer_params_v1_interface = { + "zwp_linux_buffer_params_v1", 3, 4, + zwp_linux_buffer_params_v1_requests, 2, zwp_linux_buffer_params_v1_events, +}; diff --git a/widget/gtk/wayland/moz.build b/widget/gtk/wayland/moz.build new file mode 100644 index 0000000000..a6d4adbdc3 --- /dev/null +++ b/widget/gtk/wayland/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Widget: Gtk") + +SOURCES += [ + "gtk-primary-selection-protocol.c", + "idle-inhibit-unstable-v1-protocol.c", + "linux-dmabuf-unstable-v1-protocol.c", + "primary-selection-unstable-v1-protocol.c", + "xdg-output-unstable-v1-protocol.c", +] + +EXPORTS.mozilla.widget += [ + "gbm.h", + "gtk-primary-selection-client-protocol.h", + "idle-inhibit-unstable-v1-client-protocol.h", + "linux-dmabuf-unstable-v1-client-protocol.h", + "primary-selection-unstable-v1-client-protocol.h", + "va_drmcommon.h", + "xdg-output-unstable-v1-client-protocol.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +CFLAGS += CONFIG["TK_CFLAGS"] +CXXFLAGS += CONFIG["TK_CFLAGS"] + +CXXFLAGS += ["-Wno-error=shadow"] diff --git a/widget/gtk/wayland/primary-selection-unstable-v1-client-protocol.h b/widget/gtk/wayland/primary-selection-unstable-v1-client-protocol.h new file mode 100644 index 0000000000..998266c3db --- /dev/null +++ b/widget/gtk/wayland/primary-selection-unstable-v1-client-protocol.h @@ -0,0 +1,578 @@ +/* Generated by wayland-scanner 1.18.0 */ + +#ifndef WP_PRIMARY_SELECTION_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define WP_PRIMARY_SELECTION_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_wp_primary_selection_unstable_v1 The wp_primary_selection_unstable_v1 protocol + * Primary selection protocol + * + * @section page_desc_wp_primary_selection_unstable_v1 Description + * + * This protocol provides the ability to have a primary selection device to + * match that of the X server. This primary selection is a shortcut to the + * common clipboard selection, where text just needs to be selected in order + * to allow copying it elsewhere. The de facto way to perform this action + * is the middle mouse button, although it is not limited to this one. + * + * Clients wishing to honor primary selection should create a primary + * selection source and set it as the selection through + * wp_primary_selection_device.set_selection whenever the text selection + * changes. In order to minimize calls in pointer-driven text selection, + * it should happen only once after the operation finished. Similarly, + * a NULL source should be set when text is unselected. + * + * wp_primary_selection_offer objects are first announced through the + * wp_primary_selection_device.data_offer event. Immediately after this event, + * the primary data offer will emit wp_primary_selection_offer.offer events + * to let know of the mime types being offered. + * + * When the primary selection changes, the client with the keyboard focus + * will receive wp_primary_selection_device.selection events. Only the client + * with the keyboard focus will receive such events with a non-NULL + * wp_primary_selection_offer. Across keyboard focus changes, previously + * focused clients will receive wp_primary_selection_device.events with a + * NULL wp_primary_selection_offer. + * + * In order to request the primary selection data, the client must pass + * a recent serial pertaining to the press event that is triggering the + * operation, if the compositor deems the serial valid and recent, the + * wp_primary_selection_source.send event will happen in the other end + * to let the transfer begin. The client owning the primary selection + * should write the requested data, and close the file descriptor + * immediately. + * + * If the primary selection owner client disappeared during the transfer, + * the client reading the data will receive a + * wp_primary_selection_device.selection event with a NULL + * wp_primary_selection_offer, the client should take this as a hint + * to finish the reads related to the no longer existing offer. + * + * The primary selection owner should be checking for errors during + * writes, merely cancelling the ongoing transfer if any happened. + * + * @section page_ifaces_wp_primary_selection_unstable_v1 Interfaces + * - @subpage page_iface_zwp_primary_selection_device_manager_v1 - X primary selection emulation + * - @subpage page_iface_zwp_primary_selection_device_v1 - + * - @subpage page_iface_zwp_primary_selection_offer_v1 - offer to transfer primary selection contents + * - @subpage page_iface_zwp_primary_selection_source_v1 - offer to replace the contents of the primary selection + * @section page_copyright_wp_primary_selection_unstable_v1 Copyright + * <pre> + * + * Copyright © 2015, 2016 Red Hat + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * </pre> + */ +struct wl_seat; +struct zwp_primary_selection_device_manager_v1; +struct zwp_primary_selection_device_v1; +struct zwp_primary_selection_offer_v1; +struct zwp_primary_selection_source_v1; + +/** + * @page page_iface_zwp_primary_selection_device_manager_v1 zwp_primary_selection_device_manager_v1 + * @section page_iface_zwp_primary_selection_device_manager_v1_desc Description + * + * The primary selection device manager is a singleton global object that + * provides access to the primary selection. It allows to create + * wp_primary_selection_source objects, as well as retrieving the per-seat + * wp_primary_selection_device objects. + * @section page_iface_zwp_primary_selection_device_manager_v1_api API + * See @ref iface_zwp_primary_selection_device_manager_v1. + */ +/** + * @defgroup iface_zwp_primary_selection_device_manager_v1 The zwp_primary_selection_device_manager_v1 interface + * + * The primary selection device manager is a singleton global object that + * provides access to the primary selection. It allows to create + * wp_primary_selection_source objects, as well as retrieving the per-seat + * wp_primary_selection_device objects. + */ +extern const struct wl_interface zwp_primary_selection_device_manager_v1_interface; +/** + * @page page_iface_zwp_primary_selection_device_v1 zwp_primary_selection_device_v1 + * @section page_iface_zwp_primary_selection_device_v1_api API + * See @ref iface_zwp_primary_selection_device_v1. + */ +/** + * @defgroup iface_zwp_primary_selection_device_v1 The zwp_primary_selection_device_v1 interface + */ +extern const struct wl_interface zwp_primary_selection_device_v1_interface; +/** + * @page page_iface_zwp_primary_selection_offer_v1 zwp_primary_selection_offer_v1 + * @section page_iface_zwp_primary_selection_offer_v1_desc Description + * + * A wp_primary_selection_offer represents an offer to transfer the contents + * of the primary selection clipboard to the client. Similar to + * wl_data_offer, the offer also describes the mime types that the data can + * be converted to and provides the mechanisms for transferring the data + * directly to the client. + * @section page_iface_zwp_primary_selection_offer_v1_api API + * See @ref iface_zwp_primary_selection_offer_v1. + */ +/** + * @defgroup iface_zwp_primary_selection_offer_v1 The zwp_primary_selection_offer_v1 interface + * + * A wp_primary_selection_offer represents an offer to transfer the contents + * of the primary selection clipboard to the client. Similar to + * wl_data_offer, the offer also describes the mime types that the data can + * be converted to and provides the mechanisms for transferring the data + * directly to the client. + */ +extern const struct wl_interface zwp_primary_selection_offer_v1_interface; +/** + * @page page_iface_zwp_primary_selection_source_v1 zwp_primary_selection_source_v1 + * @section page_iface_zwp_primary_selection_source_v1_desc Description + * + * The source side of a wp_primary_selection_offer, it provides a way to + * describe the offered data and respond to requests to transfer the + * requested contents of the primary selection clipboard. + * @section page_iface_zwp_primary_selection_source_v1_api API + * See @ref iface_zwp_primary_selection_source_v1. + */ +/** + * @defgroup iface_zwp_primary_selection_source_v1 The zwp_primary_selection_source_v1 interface + * + * The source side of a wp_primary_selection_offer, it provides a way to + * describe the offered data and respond to requests to transfer the + * requested contents of the primary selection clipboard. + */ +extern const struct wl_interface zwp_primary_selection_source_v1_interface; + +#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_CREATE_SOURCE 0 +#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_GET_DEVICE 1 +#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_DESTROY 2 + + +/** + * @ingroup iface_zwp_primary_selection_device_manager_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_CREATE_SOURCE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_device_manager_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_GET_DEVICE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_device_manager_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwp_primary_selection_device_manager_v1 */ +static inline void +zwp_primary_selection_device_manager_v1_set_user_data(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_device_manager_v1, user_data); +} + +/** @ingroup iface_zwp_primary_selection_device_manager_v1 */ +static inline void * +zwp_primary_selection_device_manager_v1_get_user_data(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_device_manager_v1); +} + +static inline uint32_t +zwp_primary_selection_device_manager_v1_get_version(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_manager_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_device_manager_v1 + * + * Create a new primary selection source. + */ +static inline struct zwp_primary_selection_source_v1 * +zwp_primary_selection_device_manager_v1_create_source(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) zwp_primary_selection_device_manager_v1, + ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_CREATE_SOURCE, &zwp_primary_selection_source_v1_interface, NULL); + + return (struct zwp_primary_selection_source_v1 *) id; +} + +/** + * @ingroup iface_zwp_primary_selection_device_manager_v1 + * + * Create a new data device for a given seat. + */ +static inline struct zwp_primary_selection_device_v1 * +zwp_primary_selection_device_manager_v1_get_device(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) zwp_primary_selection_device_manager_v1, + ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_GET_DEVICE, &zwp_primary_selection_device_v1_interface, NULL, seat); + + return (struct zwp_primary_selection_device_v1 *) id; +} + +/** + * @ingroup iface_zwp_primary_selection_device_manager_v1 + * + * Destroy the primary selection device manager. + */ +static inline void +zwp_primary_selection_device_manager_v1_destroy(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_device_manager_v1, + ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zwp_primary_selection_device_manager_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_device_v1 + * @struct zwp_primary_selection_device_v1_listener + */ +struct zwp_primary_selection_device_v1_listener { + /** + * introduce a new wp_primary_selection_offer + * + * Introduces a new wp_primary_selection_offer object that may be + * used to receive the current primary selection. Immediately + * following this event, the new wp_primary_selection_offer object + * will send wp_primary_selection_offer.offer events to describe + * the offered mime types. + */ + void (*data_offer)(void *data, + struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, + struct zwp_primary_selection_offer_v1 *offer); + /** + * advertise a new primary selection + * + * The wp_primary_selection_device.selection event is sent to + * notify the client of a new primary selection. This event is sent + * after the wp_primary_selection.data_offer event introducing this + * object, and after the offer has announced its mimetypes through + * wp_primary_selection_offer.offer. + * + * The data_offer is valid until a new offer or NULL is received or + * until the client loses keyboard focus. The client must destroy + * the previous selection data_offer, if any, upon receiving this + * event. + */ + void (*selection)(void *data, + struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, + struct zwp_primary_selection_offer_v1 *id); +}; + +/** + * @ingroup iface_zwp_primary_selection_device_v1 + */ +static inline int +zwp_primary_selection_device_v1_add_listener(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, + const struct zwp_primary_selection_device_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_primary_selection_device_v1, + (void (**)(void)) listener, data); +} + +#define ZWP_PRIMARY_SELECTION_DEVICE_V1_SET_SELECTION 0 +#define ZWP_PRIMARY_SELECTION_DEVICE_V1_DESTROY 1 + +/** + * @ingroup iface_zwp_primary_selection_device_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_V1_DATA_OFFER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_device_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_V1_SELECTION_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_primary_selection_device_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_V1_SET_SELECTION_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_device_v1 + */ +#define ZWP_PRIMARY_SELECTION_DEVICE_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwp_primary_selection_device_v1 */ +static inline void +zwp_primary_selection_device_v1_set_user_data(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_device_v1, user_data); +} + +/** @ingroup iface_zwp_primary_selection_device_v1 */ +static inline void * +zwp_primary_selection_device_v1_get_user_data(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_device_v1); +} + +static inline uint32_t +zwp_primary_selection_device_v1_get_version(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_device_v1 + * + * Replaces the current selection. The previous owner of the primary + * selection will receive a wp_primary_selection_source.cancelled event. + * + * To unset the selection, set the source to NULL. + */ +static inline void +zwp_primary_selection_device_v1_set_selection(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, struct zwp_primary_selection_source_v1 *source, uint32_t serial) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_device_v1, + ZWP_PRIMARY_SELECTION_DEVICE_V1_SET_SELECTION, source, serial); +} + +/** + * @ingroup iface_zwp_primary_selection_device_v1 + * + * Destroy the primary selection device. + */ +static inline void +zwp_primary_selection_device_v1_destroy(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_device_v1, + ZWP_PRIMARY_SELECTION_DEVICE_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zwp_primary_selection_device_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + * @struct zwp_primary_selection_offer_v1_listener + */ +struct zwp_primary_selection_offer_v1_listener { + /** + * advertise offered mime type + * + * Sent immediately after creating announcing the + * wp_primary_selection_offer through + * wp_primary_selection_device.data_offer. One event is sent per + * offered mime type. + */ + void (*offer)(void *data, + struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, + const char *mime_type); +}; + +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + */ +static inline int +zwp_primary_selection_offer_v1_add_listener(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, + const struct zwp_primary_selection_offer_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_primary_selection_offer_v1, + (void (**)(void)) listener, data); +} + +#define ZWP_PRIMARY_SELECTION_OFFER_V1_RECEIVE 0 +#define ZWP_PRIMARY_SELECTION_OFFER_V1_DESTROY 1 + +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + */ +#define ZWP_PRIMARY_SELECTION_OFFER_V1_OFFER_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + */ +#define ZWP_PRIMARY_SELECTION_OFFER_V1_RECEIVE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + */ +#define ZWP_PRIMARY_SELECTION_OFFER_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwp_primary_selection_offer_v1 */ +static inline void +zwp_primary_selection_offer_v1_set_user_data(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_offer_v1, user_data); +} + +/** @ingroup iface_zwp_primary_selection_offer_v1 */ +static inline void * +zwp_primary_selection_offer_v1_get_user_data(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_offer_v1); +} + +static inline uint32_t +zwp_primary_selection_offer_v1_get_version(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_offer_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + * + * To transfer the contents of the primary selection clipboard, the client + * issues this request and indicates the mime type that it wants to + * receive. The transfer happens through the passed file descriptor + * (typically created with the pipe system call). The source client writes + * the data in the mime type representation requested and then closes the + * file descriptor. + * + * The receiving client reads from the read end of the pipe until EOF and + * closes its end, at which point the transfer is complete. + */ +static inline void +zwp_primary_selection_offer_v1_receive(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime_type, int32_t fd) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_offer_v1, + ZWP_PRIMARY_SELECTION_OFFER_V1_RECEIVE, mime_type, fd); +} + +/** + * @ingroup iface_zwp_primary_selection_offer_v1 + * + * Destroy the primary selection offer. + */ +static inline void +zwp_primary_selection_offer_v1_destroy(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_offer_v1, + ZWP_PRIMARY_SELECTION_OFFER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zwp_primary_selection_offer_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_source_v1 + * @struct zwp_primary_selection_source_v1_listener + */ +struct zwp_primary_selection_source_v1_listener { + /** + * send the primary selection contents + * + * Request for the current primary selection contents from the + * client. Send the specified mime type over the passed file + * descriptor, then close it. + */ + void (*send)(void *data, + struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, + const char *mime_type, + int32_t fd); + /** + * request for primary selection contents was canceled + * + * This primary selection source is no longer valid. The client + * should clean up and destroy this primary selection source. + */ + void (*cancelled)(void *data, + struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1); +}; + +/** + * @ingroup iface_zwp_primary_selection_source_v1 + */ +static inline int +zwp_primary_selection_source_v1_add_listener(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, + const struct zwp_primary_selection_source_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_primary_selection_source_v1, + (void (**)(void)) listener, data); +} + +#define ZWP_PRIMARY_SELECTION_SOURCE_V1_OFFER 0 +#define ZWP_PRIMARY_SELECTION_SOURCE_V1_DESTROY 1 + +/** + * @ingroup iface_zwp_primary_selection_source_v1 + */ +#define ZWP_PRIMARY_SELECTION_SOURCE_V1_SEND_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_source_v1 + */ +#define ZWP_PRIMARY_SELECTION_SOURCE_V1_CANCELLED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_primary_selection_source_v1 + */ +#define ZWP_PRIMARY_SELECTION_SOURCE_V1_OFFER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_primary_selection_source_v1 + */ +#define ZWP_PRIMARY_SELECTION_SOURCE_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwp_primary_selection_source_v1 */ +static inline void +zwp_primary_selection_source_v1_set_user_data(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_source_v1, user_data); +} + +/** @ingroup iface_zwp_primary_selection_source_v1 */ +static inline void * +zwp_primary_selection_source_v1_get_user_data(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_source_v1); +} + +static inline uint32_t +zwp_primary_selection_source_v1_get_version(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_source_v1); +} + +/** + * @ingroup iface_zwp_primary_selection_source_v1 + * + * This request adds a mime type to the set of mime types advertised to + * targets. Can be called several times to offer multiple types. + */ +static inline void +zwp_primary_selection_source_v1_offer(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, const char *mime_type) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_source_v1, + ZWP_PRIMARY_SELECTION_SOURCE_V1_OFFER, mime_type); +} + +/** + * @ingroup iface_zwp_primary_selection_source_v1 + * + * Destroy the primary selection source. + */ +static inline void +zwp_primary_selection_source_v1_destroy(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_primary_selection_source_v1, + ZWP_PRIMARY_SELECTION_SOURCE_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zwp_primary_selection_source_v1); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/primary-selection-unstable-v1-protocol.c b/widget/gtk/wayland/primary-selection-unstable-v1-protocol.c new file mode 100644 index 0000000000..9f43d4d65e --- /dev/null +++ b/widget/gtk/wayland/primary-selection-unstable-v1-protocol.c @@ -0,0 +1,115 @@ +/* Generated by wayland-scanner 1.18.0 */ + +/* + * Copyright © 2015, 2016 Red Hat + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <stdint.h> +#include <gdk/gdkwayland.h> +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface zwp_primary_selection_device_v1_interface; +extern const struct wl_interface zwp_primary_selection_offer_v1_interface; +extern const struct wl_interface zwp_primary_selection_source_v1_interface; + +static const struct wl_interface *wp_primary_selection_unstable_v1_types[] = { + NULL, + NULL, + &zwp_primary_selection_source_v1_interface, + &zwp_primary_selection_device_v1_interface, + &wl_seat_interface, + &zwp_primary_selection_source_v1_interface, + NULL, + &zwp_primary_selection_offer_v1_interface, + &zwp_primary_selection_offer_v1_interface, +}; + +static const struct wl_message zwp_primary_selection_device_manager_v1_requests[] = { + { "create_source", "n", wp_primary_selection_unstable_v1_types + 2 }, + { "get_device", "no", wp_primary_selection_unstable_v1_types + 3 }, + { "destroy", "", wp_primary_selection_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_primary_selection_device_manager_v1_interface = { + "zwp_primary_selection_device_manager_v1", 1, + 3, zwp_primary_selection_device_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message zwp_primary_selection_device_v1_requests[] = { + { "set_selection", "?ou", wp_primary_selection_unstable_v1_types + 5 }, + { "destroy", "", wp_primary_selection_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwp_primary_selection_device_v1_events[] = { + { "data_offer", "n", wp_primary_selection_unstable_v1_types + 7 }, + { "selection", "?o", wp_primary_selection_unstable_v1_types + 8 }, +}; + +WL_PRIVATE const struct wl_interface zwp_primary_selection_device_v1_interface = { + "zwp_primary_selection_device_v1", 1, + 2, zwp_primary_selection_device_v1_requests, + 2, zwp_primary_selection_device_v1_events, +}; + +static const struct wl_message zwp_primary_selection_offer_v1_requests[] = { + { "receive", "sh", wp_primary_selection_unstable_v1_types + 0 }, + { "destroy", "", wp_primary_selection_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwp_primary_selection_offer_v1_events[] = { + { "offer", "s", wp_primary_selection_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_primary_selection_offer_v1_interface = { + "zwp_primary_selection_offer_v1", 1, + 2, zwp_primary_selection_offer_v1_requests, + 1, zwp_primary_selection_offer_v1_events, +}; + +static const struct wl_message zwp_primary_selection_source_v1_requests[] = { + { "offer", "s", wp_primary_selection_unstable_v1_types + 0 }, + { "destroy", "", wp_primary_selection_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwp_primary_selection_source_v1_events[] = { + { "send", "sh", wp_primary_selection_unstable_v1_types + 0 }, + { "cancelled", "", wp_primary_selection_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_primary_selection_source_v1_interface = { + "zwp_primary_selection_source_v1", 1, + 2, zwp_primary_selection_source_v1_requests, + 2, zwp_primary_selection_source_v1_events, +}; diff --git a/widget/gtk/wayland/va_drmcommon.h b/widget/gtk/wayland/va_drmcommon.h new file mode 100644 index 0000000000..e16f244a46 --- /dev/null +++ b/widget/gtk/wayland/va_drmcommon.h @@ -0,0 +1,156 @@ +/* + * va_drmcommon.h - Common utilities for DRM-based drivers + * + * Copyright (c) 2012 Intel Corporation. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sub license, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * IN NO EVENT SHALL INTEL AND/OR ITS SUPPLIERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef VA_DRM_COMMON_H +#define VA_DRM_COMMON_H + +#include <stdint.h> + +/** \brief DRM authentication type. */ +enum { + /** \brief Disconnected. */ + VA_DRM_AUTH_NONE = 0, + /** + * \brief Connected. Authenticated with DRI1 protocol. + * + * @deprecated + * This is a deprecated authentication type. All DRI-based drivers have + * been migrated to use the DRI2 protocol. Newly written drivers shall + * use DRI2 protocol only, or a custom authentication means. e.g. opt + * for authenticating on the VA driver side, instead of libva side. + */ + VA_DRM_AUTH_DRI1 = 1, + /** + * \brief Connected. Authenticated with DRI2 protocol. + * + * This is only useful to VA/X11 drivers. The libva-x11 library provides + * a helper function VA_DRI2Authenticate() for authenticating the + * connection. However, DRI2 conformant drivers don't need to call that + * function since authentication happens on the libva side, implicitly. + */ + VA_DRM_AUTH_DRI2 = 2, + /** + * \brief Connected. Authenticated with some alternate raw protocol. + * + * This authentication mode is mainly used in non-VA/X11 drivers. + * Authentication happens through some alternative method, at the + * discretion of the VA driver implementation. + */ + VA_DRM_AUTH_CUSTOM = 3 +}; + +/** \brief Base DRM state. */ +struct drm_state { + /** \brief DRM connection descriptor. */ + int fd; + /** \brief DRM authentication type. */ + int auth_type; + /** \brief Reserved bytes for future use, must be zero */ + int va_reserved[8]; +}; + +/** \brief Kernel DRM buffer memory type. */ +#define VA_SURFACE_ATTRIB_MEM_TYPE_KERNEL_DRM 0x10000000 +/** \brief DRM PRIME memory type (old version) + * + * This supports only single objects with restricted memory layout. + * Used with VASurfaceAttribExternalBuffers. + */ +#define VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME 0x20000000 +/** \brief DRM PRIME memory type + * + * Used with VADRMPRIMESurfaceDescriptor. + */ +#define VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 0x40000000 + +/** + * \brief External buffer descriptor for a DRM PRIME surface. + * + * For export, call vaExportSurfaceHandle() with mem_type set to + * VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 and pass a pointer to an + * instance of this structure to fill. + * If VA_EXPORT_SURFACE_SEPARATE_LAYERS is specified on export, each + * layer will contain exactly one plane. For example, an NV12 + * surface will be exported as two layers, one of DRM_FORMAT_R8 and + * one of DRM_FORMAT_GR88. + * If VA_EXPORT_SURFACE_COMPOSED_LAYERS is specified on export, + * there will be exactly one layer. + * + * For import, call vaCreateSurfaces() with the MemoryType attribute + * set to VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 and the + * ExternalBufferDescriptor attribute set to point to an array of + * num_surfaces instances of this structure. + * The number of planes which need to be provided for a given layer + * is dependent on both the format and the format modifier used for + * the objects containing it. For example, the format DRM_FORMAT_RGBA + * normally requires one plane, but with the format modifier + * I915_FORMAT_MOD_Y_TILED_CCS it requires two planes - the first + * being the main data plane and the second containing the color + * control surface. + * Note that a given driver may only support a subset of possible + * representations of a particular format. For example, it may only + * support NV12 surfaces when they are contained within a single DRM + * object, and therefore fail to create such surfaces if the two + * planes are in different DRM objects. + */ +typedef struct _VADRMPRIMESurfaceDescriptor { + /** Pixel format fourcc of the whole surface (VA_FOURCC_*). */ + uint32_t fourcc; + /** Width of the surface in pixels. */ + uint32_t width; + /** Height of the surface in pixels. */ + uint32_t height; + /** Number of distinct DRM objects making up the surface. */ + uint32_t num_objects; + /** Description of each object. */ + struct { + /** DRM PRIME file descriptor for this object. */ + int fd; + /** Total size of this object (may include regions which are + * not part of the surface). */ + uint32_t size; + /** Format modifier applied to this object. */ + uint64_t drm_format_modifier; + } objects[4]; + /** Number of layers making up the surface. */ + uint32_t num_layers; + /** Description of each layer in the surface. */ + struct { + /** DRM format fourcc of this layer (DRM_FOURCC_*). */ + uint32_t drm_format; + /** Number of planes in this layer. */ + uint32_t num_planes; + /** Index in the objects array of the object containing each + * plane. */ + uint32_t object_index[4]; + /** Offset within the object of each plane. */ + uint32_t offset[4]; + /** Pitch of each plane. */ + uint32_t pitch[4]; + } layers[4]; +} VADRMPRIMESurfaceDescriptor; + +#endif /* VA_DRM_COMMON_H */ diff --git a/widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h b/widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h new file mode 100644 index 0000000000..432057ccef --- /dev/null +++ b/widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h @@ -0,0 +1,392 @@ +/* Generated by wayland-scanner 1.18.0 */ + +#ifndef XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_output_unstable_v1 The xdg_output_unstable_v1 protocol + * Protocol to describe output regions + * + * @section page_desc_xdg_output_unstable_v1 Description + * + * This protocol aims at describing outputs in a way which is more in line + * with the concept of an output on desktop oriented systems. + * + * Some information are more specific to the concept of an output for + * a desktop oriented system and may not make sense in other applications, + * such as IVI systems for example. + * + * Typically, the global compositor space on a desktop system is made of + * a contiguous or overlapping set of rectangular regions. + * + * Some of the information provided in this protocol might be identical + * to their counterparts already available from wl_output, in which case + * the information provided by this protocol should be preferred to their + * equivalent in wl_output. The goal is to move the desktop specific + * concepts (such as output location within the global compositor space, + * the connector name and types, etc.) out of the core wl_output protocol. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible + * changes may be added together with the corresponding interface + * version bump. + * Backward incompatible changes are done by bumping the version + * number in the protocol and interface names and resetting the + * interface version. Once the protocol is to be declared stable, + * the 'z' prefix and the version number in the protocol and + * interface names are removed and the interface version number is + * reset. + * + * @section page_ifaces_xdg_output_unstable_v1 Interfaces + * - @subpage page_iface_zxdg_output_manager_v1 - manage xdg_output objects + * - @subpage page_iface_zxdg_output_v1 - compositor logical output region + * @section page_copyright_xdg_output_unstable_v1 Copyright + * <pre> + * + * Copyright © 2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * </pre> + */ +struct wl_output; +struct zxdg_output_manager_v1; +struct zxdg_output_v1; + +/** + * @page page_iface_zxdg_output_manager_v1 zxdg_output_manager_v1 + * @section page_iface_zxdg_output_manager_v1_desc Description + * + * A global factory interface for xdg_output objects. + * @section page_iface_zxdg_output_manager_v1_api API + * See @ref iface_zxdg_output_manager_v1. + */ +/** + * @defgroup iface_zxdg_output_manager_v1 The zxdg_output_manager_v1 interface + * + * A global factory interface for xdg_output objects. + */ +extern const struct wl_interface zxdg_output_manager_v1_interface; +/** + * @page page_iface_zxdg_output_v1 zxdg_output_v1 + * @section page_iface_zxdg_output_v1_desc Description + * + * An xdg_output describes part of the compositor geometry. + * + * This typically corresponds to a monitor that displays part of the + * compositor space. + * + * For objects version 3 onwards, after all xdg_output properties have been + * sent (when the object is created and when properties are updated), a + * wl_output.done event is sent. This allows changes to the output + * properties to be seen as atomic, even if they happen via multiple events. + * @section page_iface_zxdg_output_v1_api API + * See @ref iface_zxdg_output_v1. + */ +/** + * @defgroup iface_zxdg_output_v1 The zxdg_output_v1 interface + * + * An xdg_output describes part of the compositor geometry. + * + * This typically corresponds to a monitor that displays part of the + * compositor space. + * + * For objects version 3 onwards, after all xdg_output properties have been + * sent (when the object is created and when properties are updated), a + * wl_output.done event is sent. This allows changes to the output + * properties to be seen as atomic, even if they happen via multiple events. + */ +extern const struct wl_interface zxdg_output_v1_interface; + +#define ZXDG_OUTPUT_MANAGER_V1_DESTROY 0 +#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT 1 + +/** + * @ingroup iface_zxdg_output_manager_v1 + */ +#define ZXDG_OUTPUT_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_manager_v1 + */ +#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_output_manager_v1 */ +static inline void zxdg_output_manager_v1_set_user_data( + struct zxdg_output_manager_v1* zxdg_output_manager_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zxdg_output_manager_v1, user_data); +} + +/** @ingroup iface_zxdg_output_manager_v1 */ +static inline void* zxdg_output_manager_v1_get_user_data( + struct zxdg_output_manager_v1* zxdg_output_manager_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zxdg_output_manager_v1); +} + +static inline uint32_t zxdg_output_manager_v1_get_version( + struct zxdg_output_manager_v1* zxdg_output_manager_v1) { + return wl_proxy_get_version((struct wl_proxy*)zxdg_output_manager_v1); +} + +/** + * @ingroup iface_zxdg_output_manager_v1 + * + * Using this request a client can tell the server that it is not + * going to use the xdg_output_manager object anymore. + * + * Any objects already created through this instance are not affected. + */ +static inline void zxdg_output_manager_v1_destroy( + struct zxdg_output_manager_v1* zxdg_output_manager_v1) { + wl_proxy_marshal((struct wl_proxy*)zxdg_output_manager_v1, + ZXDG_OUTPUT_MANAGER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zxdg_output_manager_v1); +} + +/** + * @ingroup iface_zxdg_output_manager_v1 + * + * This creates a new xdg_output object for the given wl_output. + */ +static inline struct zxdg_output_v1* zxdg_output_manager_v1_get_xdg_output( + struct zxdg_output_manager_v1* zxdg_output_manager_v1, + struct wl_output* output) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor((struct wl_proxy*)zxdg_output_manager_v1, + ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT, + &zxdg_output_v1_interface, NULL, output); + + return (struct zxdg_output_v1*)id; +} + +/** + * @ingroup iface_zxdg_output_v1 + * @struct zxdg_output_v1_listener + */ +struct zxdg_output_v1_listener { + /** + * position of the output within the global compositor space + * + * The position event describes the location of the wl_output + * within the global compositor space. + * + * The logical_position event is sent after creating an xdg_output + * (see xdg_output_manager.get_xdg_output) and whenever the + * location of the output changes within the global compositor + * space. + * @param x x position within the global compositor space + * @param y y position within the global compositor space + */ + void (*logical_position)(void* data, struct zxdg_output_v1* zxdg_output_v1, + int32_t x, int32_t y); + /** + * size of the output in the global compositor space + * + * The logical_size event describes the size of the output in the + * global compositor space. + * + * For example, a surface without any buffer scale, transformation + * nor rotation set, with the size matching the logical_size will + * have the same size as the corresponding output when displayed. + * + * Most regular Wayland clients should not pay attention to the + * logical size and would rather rely on xdg_shell interfaces. + * + * Some clients such as Xwayland, however, need this to configure + * their surfaces in the global compositor space as the compositor + * may apply a different scale from what is advertised by the + * output scaling property (to achieve fractional scaling, for + * example). + * + * For example, for a wl_output mode 3840×2160 and a scale factor + * 2: + * + * - A compositor not scaling the surface buffers will advertise a + * logical size of 3840×2160, + * + * - A compositor automatically scaling the surface buffers will + * advertise a logical size of 1920×1080, + * + * - A compositor using a fractional scale of 1.5 will advertise a + * logical size to 2560×1620. + * + * For example, for a wl_output mode 1920×1080 and a 90 degree + * rotation, the compositor will advertise a logical size of + * 1080x1920. + * + * The logical_size event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output) and whenever the logical size + * of the output changes, either as a result of a change in the + * applied scale or because of a change in the corresponding output + * mode(see wl_output.mode) or transform (see wl_output.transform). + * @param width width in global compositor space + * @param height height in global compositor space + */ + void (*logical_size)(void* data, struct zxdg_output_v1* zxdg_output_v1, + int32_t width, int32_t height); + /** + * all information about the output have been sent + * + * This event is sent after all other properties of an xdg_output + * have been sent. + * + * This allows changes to the xdg_output properties to be seen as + * atomic, even if they happen via multiple events. + * + * For objects version 3 onwards, this event is deprecated. + * Compositors are not required to send it anymore and must send + * wl_output.done instead. + */ + void (*done)(void* data, struct zxdg_output_v1* zxdg_output_v1); + /** + * name of this output + * + * Many compositors will assign names to their outputs, show them + * to the user, allow them to be configured by name, etc. The + * client may wish to know this name as well to offer the user + * similar behaviors. + * + * The naming convention is compositor defined, but limited to + * alphanumeric characters and dashes (-). Each name is unique + * among all wl_output globals, but if a wl_output global is + * destroyed the same name may be reused later. The names will also + * remain consistent across sessions with the same hardware and + * software configuration. + * + * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. + * However, do not assume that the name is a reflection of an + * underlying DRM connector, X11 connection, etc. + * + * The name event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output). This event is only sent once + * per xdg_output, and the name does not change over the lifetime + * of the wl_output global. + * @param name output name + * @since 2 + */ + void (*name)(void* data, struct zxdg_output_v1* zxdg_output_v1, + const char* name); + /** + * human-readable description of this output + * + * Many compositors can produce human-readable descriptions of + * their outputs. The client may wish to know this description as + * well, to communicate the user for various purposes. + * + * The description is a UTF-8 string with no convention defined for + * its contents. Examples might include 'Foocorp 11" Display' or + * 'Virtual X11 output via :1'. + * + * The description event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output) and whenever the description + * changes. The description is optional, and may not be sent at + * all. + * + * For objects of version 2 and lower, this event is only sent once + * per xdg_output, and the description does not change over the + * lifetime of the wl_output global. + * @param description output description + * @since 2 + */ + void (*description)(void* data, struct zxdg_output_v1* zxdg_output_v1, + const char* description); +}; + +/** + * @ingroup iface_zxdg_output_v1 + */ +static inline int zxdg_output_v1_add_listener( + struct zxdg_output_v1* zxdg_output_v1, + const struct zxdg_output_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)zxdg_output_v1, + (void (**)(void))listener, data); +} + +#define ZXDG_OUTPUT_V1_DESTROY 0 + +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_LOGICAL_POSITION_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_LOGICAL_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_NAME_SINCE_VERSION 2 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DESCRIPTION_SINCE_VERSION 2 + +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_output_v1 */ +static inline void zxdg_output_v1_set_user_data( + struct zxdg_output_v1* zxdg_output_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zxdg_output_v1, user_data); +} + +/** @ingroup iface_zxdg_output_v1 */ +static inline void* zxdg_output_v1_get_user_data( + struct zxdg_output_v1* zxdg_output_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zxdg_output_v1); +} + +static inline uint32_t zxdg_output_v1_get_version( + struct zxdg_output_v1* zxdg_output_v1) { + return wl_proxy_get_version((struct wl_proxy*)zxdg_output_v1); +} + +/** + * @ingroup iface_zxdg_output_v1 + * + * Using this request a client can tell the server that it is not + * going to use the xdg_output object anymore. + */ +static inline void zxdg_output_v1_destroy( + struct zxdg_output_v1* zxdg_output_v1) { + wl_proxy_marshal((struct wl_proxy*)zxdg_output_v1, ZXDG_OUTPUT_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zxdg_output_v1); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/xdg-output-unstable-v1-protocol.c b/widget/gtk/wayland/xdg-output-unstable-v1-protocol.c new file mode 100644 index 0000000000..f80133f357 --- /dev/null +++ b/widget/gtk/wayland/xdg-output-unstable-v1-protocol.c @@ -0,0 +1,74 @@ +/* Generated by wayland-scanner 1.18.0 */ + +/* + * Copyright © 2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <stdint.h> +#include <gdk/gdkwayland.h> +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +# define WL_PRIVATE __attribute__((visibility("hidden"))) +#else +# define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface zxdg_output_v1_interface; + +static const struct wl_interface* xdg_output_unstable_v1_types[] = { + NULL, + NULL, + &zxdg_output_v1_interface, + &wl_output_interface, +}; + +static const struct wl_message zxdg_output_manager_v1_requests[] = { + {"destroy", "", xdg_output_unstable_v1_types + 0}, + {"get_xdg_output", "no", xdg_output_unstable_v1_types + 2}, +}; + +WL_PRIVATE const struct wl_interface zxdg_output_manager_v1_interface = { + "zxdg_output_manager_v1", 3, 2, zxdg_output_manager_v1_requests, 0, NULL, +}; + +static const struct wl_message zxdg_output_v1_requests[] = { + {"destroy", "", xdg_output_unstable_v1_types + 0}, +}; + +static const struct wl_message zxdg_output_v1_events[] = { + {"logical_position", "ii", xdg_output_unstable_v1_types + 0}, + {"logical_size", "ii", xdg_output_unstable_v1_types + 0}, + {"done", "", xdg_output_unstable_v1_types + 0}, + {"name", "2s", xdg_output_unstable_v1_types + 0}, + {"description", "2s", xdg_output_unstable_v1_types + 0}, +}; + +WL_PRIVATE const struct wl_interface zxdg_output_v1_interface = { + "zxdg_output_v1", 3, 1, zxdg_output_v1_requests, 5, zxdg_output_v1_events, +}; |