diff options
Diffstat (limited to 'widget/gtk')
153 files changed, 56036 insertions, 0 deletions
diff --git a/widget/gtk/AsyncDBus.cpp b/widget/gtk/AsyncDBus.cpp new file mode 100644 index 0000000000..cb1905dc17 --- /dev/null +++ b/widget/gtk/AsyncDBus.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AsyncDBus.h" + +#include <dbus/dbus-glib-lowlevel.h> +#include "mozilla/UniquePtrExtensions.h" + +namespace mozilla::widget { + +static void CreateProxyCallback(GObject*, GAsyncResult* aResult, + gpointer aUserData) { + RefPtr<DBusProxyPromise::Private> promise = + dont_AddRef(static_cast<DBusProxyPromise::Private*>(aUserData)); + GUniquePtr<GError> error; + RefPtr<GDBusProxy> proxy = dont_AddRef( + g_dbus_proxy_new_for_bus_finish(aResult, getter_Transfers(error))); + if (proxy) { + promise->Resolve(std::move(proxy), __func__); + } else { + promise->Reject(std::move(error), __func__); + } +} + +RefPtr<DBusProxyPromise> CreateDBusProxyForBus( + GBusType aBusType, GDBusProxyFlags aFlags, + GDBusInterfaceInfo* aInterfaceInfo, const char* aName, + const char* aObjectPath, const char* aInterfaceName, + GCancellable* aCancellable) { + auto promise = MakeRefPtr<DBusProxyPromise::Private>(__func__); + g_dbus_proxy_new_for_bus(aBusType, aFlags, aInterfaceInfo, aName, aObjectPath, + aInterfaceName, aCancellable, CreateProxyCallback, + do_AddRef(promise).take()); + return promise.forget(); +} + +static void ProxyCallCallback(GObject* aSourceObject, GAsyncResult* aResult, + gpointer aUserData) { + RefPtr<DBusCallPromise::Private> promise = + dont_AddRef(static_cast<DBusCallPromise::Private*>(aUserData)); + GUniquePtr<GError> error; + RefPtr<GVariant> result = dont_AddRef(g_dbus_proxy_call_finish( + G_DBUS_PROXY(aSourceObject), aResult, getter_Transfers(error))); + if (result) { + promise->Resolve(std::move(result), __func__); + } else { + promise->Reject(std::move(error), __func__); + } +} + +RefPtr<DBusCallPromise> DBusProxyCall(GDBusProxy* aProxy, const char* aMethod, + GVariant* aArgs, GDBusCallFlags aFlags, + gint aTimeout, + GCancellable* aCancellable) { + auto promise = MakeRefPtr<DBusCallPromise::Private>(__func__); + g_dbus_proxy_call(aProxy, aMethod, aArgs, aFlags, aTimeout, aCancellable, + ProxyCallCallback, do_AddRef(promise).take()); + return promise.forget(); +} + +static void ProxyCallWithUnixFDListCallback(GObject* aSourceObject, + GAsyncResult* aResult, + gpointer aUserData) { + RefPtr<DBusCallPromise::Private> promise = + dont_AddRef(static_cast<DBusCallPromise::Private*>(aUserData)); + GUniquePtr<GError> error; + GUnixFDList** aFDList = nullptr; + RefPtr<GVariant> result = + dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish( + G_DBUS_PROXY(aSourceObject), aFDList, aResult, + getter_Transfers(error))); + if (result) { + promise->Resolve(std::move(result), __func__); + } else { + promise->Reject(std::move(error), __func__); + } +} + +RefPtr<DBusCallPromise> DBusProxyCallWithUnixFDList( + GDBusProxy* aProxy, const char* aMethod, GVariant* aArgs, + GDBusCallFlags aFlags, gint aTimeout, GUnixFDList* aFDList, + GCancellable* aCancellable) { + auto promise = MakeRefPtr<DBusCallPromise::Private>(__func__); + g_dbus_proxy_call_with_unix_fd_list( + aProxy, aMethod, aArgs, aFlags, aTimeout, aFDList, aCancellable, + ProxyCallWithUnixFDListCallback, do_AddRef(promise).take()); + return promise.forget(); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/AsyncDBus.h b/widget/gtk/AsyncDBus.h new file mode 100644 index 0000000000..41558c8d70 --- /dev/null +++ b/widget/gtk/AsyncDBus.h @@ -0,0 +1,40 @@ +/* -*- 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_AsyncDBus_h +#define mozilla_widget_AsyncDBus_h + +#include <dbus/dbus-glib.h> + +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/MozPromise.h" + +namespace mozilla::widget { + +using DBusProxyPromise = MozPromise<RefPtr<GDBusProxy>, GUniquePtr<GError>, + /* IsExclusive = */ true>; + +using DBusCallPromise = MozPromise<RefPtr<GVariant>, GUniquePtr<GError>, + /* IsExclusive = */ true>; + +RefPtr<DBusProxyPromise> CreateDBusProxyForBus( + GBusType aBusType, GDBusProxyFlags aFlags, + GDBusInterfaceInfo* aInterfaceInfo, const char* aName, + const char* aObjectPath, const char* aInterfaceName, + GCancellable* aCancellable = nullptr); + +RefPtr<DBusCallPromise> DBusProxyCall(GDBusProxy*, const char* aMethod, + GVariant* aArgs, GDBusCallFlags, + gint aTimeout = -1, + GCancellable* = nullptr); + +RefPtr<DBusCallPromise> DBusProxyCallWithUnixFDList( + GDBusProxy*, const char* aMethod, GVariant* aArgs, GDBusCallFlags, + gint aTimeout = -1, GUnixFDList* = nullptr, GCancellable* = nullptr); + +} // namespace mozilla::widget + +#endif diff --git a/widget/gtk/AsyncGtkClipboardRequest.cpp b/widget/gtk/AsyncGtkClipboardRequest.cpp new file mode 100644 index 0000000000..75801c698d --- /dev/null +++ b/widget/gtk/AsyncGtkClipboardRequest.cpp @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 2; 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 "AsyncGtkClipboardRequest.h" + +namespace mozilla { + +AsyncGtkClipboardRequest::AsyncGtkClipboardRequest(ClipboardDataType aDataType, + int32_t aWhichClipboard, + const char* aMimeType) { + GtkClipboard* clipboard = + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); + mRequest = MakeUnique<Request>(aDataType); + + switch (aDataType) { + case ClipboardDataType::Data: + LOGCLIP(" getting DATA MIME %s\n", aMimeType); + gtk_clipboard_request_contents(clipboard, + gdk_atom_intern(aMimeType, FALSE), + OnDataReceived, mRequest.get()); + break; + case ClipboardDataType::Text: + LOGCLIP(" getting TEXT\n"); + gtk_clipboard_request_text(clipboard, OnTextReceived, mRequest.get()); + break; + case ClipboardDataType::Targets: + LOGCLIP(" getting TARGETS\n"); + gtk_clipboard_request_contents(clipboard, + gdk_atom_intern("TARGETS", FALSE), + OnDataReceived, mRequest.get()); + break; + } +} + +void AsyncGtkClipboardRequest::OnDataReceived(GtkClipboard* clipboard, + GtkSelectionData* selection_data, + gpointer data) { + int whichClipboard = GetGeckoClipboardType(clipboard); + LOGCLIP("OnDataReceived(%s) callback\n", + whichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + static_cast<Request*>(data)->Complete(selection_data); +} + +void AsyncGtkClipboardRequest::OnTextReceived(GtkClipboard* clipboard, + const gchar* text, + gpointer data) { + int whichClipboard = GetGeckoClipboardType(clipboard); + LOGCLIP("OnTextReceived(%s) callback\n", + whichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + static_cast<Request*>(data)->Complete(text); +} + +void AsyncGtkClipboardRequest::Request::Complete(const void* aData) { + LOGCLIP("Request::Complete(), aData = %p, timedOut = %d\n", aData, mTimedOut); + + if (mTimedOut) { + delete this; + return; + } + + mData.emplace(); + + gint dataLength = 0; + if (mDataType == ClipboardDataType::Targets || + mDataType == ClipboardDataType::Data) { + dataLength = gtk_selection_data_get_length((GtkSelectionData*)aData); + } else { + dataLength = aData ? strlen((const char*)aData) : 0; + } + + // Negative size means no data or data error. + if (dataLength <= 0) { + LOGCLIP(" zero dataLength, quit.\n"); + return; + } + + switch (mDataType) { + case ClipboardDataType::Targets: { + LOGCLIP(" getting %d bytes of clipboard targets.\n", dataLength); + gint n_targets = 0; + GdkAtom* targets = nullptr; + if (!gtk_selection_data_get_targets((GtkSelectionData*)aData, &targets, + &n_targets) || + !n_targets) { + // We failed to get targets + return; + } + mData->SetTargets( + ClipboardTargets{GUniquePtr<GdkAtom>(targets), uint32_t(n_targets)}); + break; + } + case ClipboardDataType::Text: { + LOGCLIP(" getting %d bytes of text.\n", dataLength); + mData->SetText(Span(static_cast<const char*>(aData), dataLength)); + LOGCLIP(" done, mClipboardData = %p\n", mData->AsSpan().data()); + break; + } + case ClipboardDataType::Data: { + LOGCLIP(" getting %d bytes of data.\n", dataLength); + mData->SetData(Span(gtk_selection_data_get_data((GtkSelectionData*)aData), + dataLength)); + LOGCLIP(" done, mClipboardData = %p\n", mData->AsSpan().data()); + break; + } + } +} + +AsyncGtkClipboardRequest::~AsyncGtkClipboardRequest() { + if (mRequest && mRequest->mData.isNothing()) { + mRequest->mTimedOut = true; + Unused << mRequest.release(); + } +} + +} // namespace mozilla diff --git a/widget/gtk/AsyncGtkClipboardRequest.h b/widget/gtk/AsyncGtkClipboardRequest.h new file mode 100644 index 0000000000..1d72691560 --- /dev/null +++ b/widget/gtk/AsyncGtkClipboardRequest.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_AsyncGtkClipboardRequest_h +#define mozilla_AsyncGtkClipboardRequest_h + +#include "nsClipboard.h" + +namespace mozilla { + +// An asynchronous clipboard request that we wait for synchronously by +// spinning the event loop. +class MOZ_STACK_CLASS AsyncGtkClipboardRequest { + // Heap-allocated object that we give GTK as a callback. + struct Request { + explicit Request(ClipboardDataType aDataType) : mDataType(aDataType) {} + + void Complete(const void*); + + const ClipboardDataType mDataType; + Maybe<ClipboardData> mData; + bool mTimedOut = false; + }; + + UniquePtr<Request> mRequest; + + static void OnDataReceived(GtkClipboard*, GtkSelectionData*, gpointer); + static void OnTextReceived(GtkClipboard*, const gchar*, gpointer); + + public: + // Launch a request for a particular GTK clipboard. The current status of the + // request can be observed by calling HasCompleted() and TakeResult(). + AsyncGtkClipboardRequest(ClipboardDataType, int32_t aWhichClipboard, + const char* aMimeType = nullptr); + + // Returns whether the request has been answered already. + bool HasCompleted() const { return mRequest->mData.isSome(); } + + // Takes the result from the current request if completed, or a + // default-constructed data otherwise. The destructor will take care of + // flagging the request as timed out in that case. + ClipboardData TakeResult() { + if (!HasCompleted()) { + return {}; + } + auto request = std::move(mRequest); + return request->mData.extract(); + } + + // If completed, frees the request if needed. Otherwise, marks it as a timed + // out request so that when it completes the Request object is properly + // freed. + ~AsyncGtkClipboardRequest(); +}; + +}; // namespace mozilla + +#endif diff --git a/widget/gtk/CompositorWidgetChild.cpp b/widget/gtk/CompositorWidgetChild.cpp new file mode 100644 index 0000000000..b7908a43d4 --- /dev/null +++ b/widget/gtk/CompositorWidgetChild.cpp @@ -0,0 +1,51 @@ +/* -*- 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); +} + +void CompositorWidgetChild::DisableRendering() { + Unused << SendDisableRendering(); +} + +void CompositorWidgetChild::EnableRendering(const uintptr_t aXWindow, + const bool aShaped) { + Unused << SendEnableRendering(aXWindow, aShaped); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/CompositorWidgetChild.h b/widget/gtk/CompositorWidgetChild.h new file mode 100644 index 0000000000..b1cad75da3 --- /dev/null +++ b/widget/gtk/CompositorWidgetChild.h @@ -0,0 +1,41 @@ +/* -*- 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; + void DisableRendering() override; + void EnableRendering(const uintptr_t aXWindow, const bool aShaped) 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..998614622e --- /dev/null +++ b/widget/gtk/CompositorWidgetParent.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CompositorWidgetParent.h" +#include "mozilla/Unused.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsWindow.h" + +namespace mozilla::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(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvDisableRendering() { + DisableRendering(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvEnableRendering( + const uintptr_t& aXWindow, const bool& aShaped) { + EnableRendering(aXWindow, aShaped); + return IPC_OK(); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/CompositorWidgetParent.h b/widget/gtk/CompositorWidgetParent.h new file mode 100644 index 0000000000..2bbc70af3e --- /dev/null +++ b/widget/gtk/CompositorWidgetParent.h @@ -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/. */ + +#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; + + mozilla::ipc::IPCResult RecvDisableRendering() override; + mozilla::ipc::IPCResult RecvEnableRendering(const uintptr_t& aXWindow, + const bool& aShaped) 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..31cea31adb --- /dev/null +++ b/widget/gtk/DMABufLibWrapper.cpp @@ -0,0 +1,325 @@ +/* -*- 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 "base/message_loop.h" // for MessageLoop +#include "nsWaylandDisplay.h" +#include "DMABufLibWrapper.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/gfx/gfxVars.h" +#include "WidgetUtilsGtk.h" +#include "gfxConfig.h" +#include "nsIGfxInfo.h" +#include "mozilla/Components.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <dlfcn.h> +#include <mutex> + +using namespace mozilla::gfx; + +namespace mozilla { +namespace widget { + +bool sUseWebGLDmabufBackend = true; + +#define GBMLIB_NAME "libgbm.so.1" +#define DRMLIB_NAME "libdrm.so.2" + +// Use static lock to protect dri operation as +// gbm_dri.c is not thread safe. +// https://gitlab.freedesktop.org/mesa/mesa/-/issues/4422 +mozilla::StaticMutex GbmLib::sDRILock MOZ_UNANNOTATED; + +bool GbmLib::sLoaded = false; +void* GbmLib::sGbmLibHandle = nullptr; +void* GbmLib::sXf86DrmLibHandle = nullptr; +CreateDeviceFunc GbmLib::sCreateDevice; +DestroyDeviceFunc GbmLib::sDestroyDevice; +CreateFunc GbmLib::sCreate; +CreateWithModifiersFunc GbmLib::sCreateWithModifiers; +GetModifierFunc GbmLib::sGetModifier; +GetStrideFunc GbmLib::sGetStride; +GetFdFunc GbmLib::sGetFd; +DestroyFunc GbmLib::sDestroy; +MapFunc GbmLib::sMap; +UnmapFunc GbmLib::sUnmap; +GetPlaneCountFunc GbmLib::sGetPlaneCount; +GetHandleForPlaneFunc GbmLib::sGetHandleForPlane; +GetStrideForPlaneFunc GbmLib::sGetStrideForPlane; +GetOffsetFunc GbmLib::sGetOffset; +DeviceIsFormatSupportedFunc GbmLib::sDeviceIsFormatSupported; +DrmPrimeHandleToFDFunc GbmLib::sDrmPrimeHandleToFD; +CreateSurfaceFunc GbmLib::sCreateSurface; +DestroySurfaceFunc GbmLib::sDestroySurface; + +bool GbmLib::IsLoaded() { + return sCreateDevice != nullptr && sDestroyDevice != 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 && sCreateSurface != nullptr && + sDestroySurface != nullptr; +} + +bool GbmLib::Load() { + static bool sTriedToLoad = false; + if (sTriedToLoad) { + return sLoaded; + } + + sTriedToLoad = true; + + MOZ_ASSERT(!sGbmLibHandle); + MOZ_ASSERT(!sLoaded); + + LOGDMABUF(("Loading DMABuf system library %s ...\n", GBMLIB_NAME)); + + 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"); + sDestroyDevice = + (DestroyDeviceFunc)dlsym(sGbmLibHandle, "gbm_device_destroy"); + 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"); + sCreateSurface = + (CreateSurfaceFunc)dlsym(sGbmLibHandle, "gbm_surface_create"); + sDestroySurface = + (DestroySurfaceFunc)dlsym(sGbmLibHandle, "gbm_surface_destroy"); + + 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"); + sLoaded = IsLoaded(); + if (!sLoaded) { + LOGDMABUF(("Failed to load all symbols from %s\n", GBMLIB_NAME)); + } + return sLoaded; +} + +int DMABufDevice::GetDmabufFD(uint32_t aGEMHandle) { + int fd; + return GbmLib::DrmPrimeHandleToFD(mDRMFd, aGEMHandle, 0, &fd) < 0 ? -1 : fd; +} + +gbm_device* DMABufDevice::GetGbmDevice() { + std::call_once(mFlagGbmDevice, [&] { + mGbmDevice = (mDRMFd != -1) ? GbmLib::CreateDevice(mDRMFd) : nullptr; + }); + return mGbmDevice; +} + +int DMABufDevice::OpenDRMFd() { return open(mDrmRenderNode.get(), O_RDWR); } + +bool DMABufDevice::IsEnabled(nsACString& aFailureId) { + if (mDRMFd == -1) { + aFailureId = mFailureId; + } + return mDRMFd != -1; +} + +DMABufDevice::DMABufDevice() + : mXRGBFormat({true, false, GBM_FORMAT_XRGB8888, {}}), + mARGBFormat({true, true, GBM_FORMAT_ARGB8888, {}}) { + Configure(); +} + +DMABufDevice::~DMABufDevice() { + if (mGbmDevice) { + GbmLib::DestroyDevice(mGbmDevice); + mGbmDevice = nullptr; + } + if (mDRMFd != -1) { + close(mDRMFd); + mDRMFd = -1; + } +} + +void DMABufDevice::Configure() { + LOGDMABUF(("DMABufDevice::Configure()")); + + if (!GbmLib::IsAvailable()) { + LOGDMABUF(("GbmLib is not available!")); + mFailureId = "FEATURE_FAILURE_NO_LIBGBM"; + return; + } + + mDrmRenderNode = nsAutoCString(getenv("MOZ_DRM_DEVICE")); + if (mDrmRenderNode.IsEmpty()) { + mDrmRenderNode.Assign(gfx::gfxVars::DrmRenderDevice()); + } + if (mDrmRenderNode.IsEmpty()) { + LOGDMABUF(("We're missing DRM render device!\n")); + mFailureId = "FEATURE_FAILURE_NO_DRM_DEVICE"; + return; + } + + LOGDMABUF(("Using DRM device %s", mDrmRenderNode.get())); + mDRMFd = open(mDrmRenderNode.get(), O_RDWR); + if (mDRMFd < 0) { + LOGDMABUF(("Failed to open drm render node %s error %s\n", + mDrmRenderNode.get(), strerror(errno))); + mFailureId = "FEATURE_FAILURE_NO_DRM_DEVICE"; + return; + } + + LoadFormatModifiers(); + + LOGDMABUF(("DMABuf is enabled")); +} + +#ifdef NIGHTLY_BUILD +bool DMABufDevice::IsDMABufTexturesEnabled() { + return gfx::gfxVars::UseDMABuf() && + StaticPrefs::widget_dmabuf_textures_enabled(); +} +#else +bool DMABufDevice::IsDMABufTexturesEnabled() { return false; } +#endif +bool DMABufDevice::IsDMABufWebGLEnabled() { + LOGDMABUF( + ("DMABufDevice::IsDMABufWebGLEnabled: UseDMABuf %d " + "sUseWebGLDmabufBackend %d " + "widget_dmabuf_webgl_enabled %d\n", + gfx::gfxVars::UseDMABuf(), sUseWebGLDmabufBackend, + StaticPrefs::widget_dmabuf_webgl_enabled())); + return gfx::gfxVars::UseDMABuf() && sUseWebGLDmabufBackend && + StaticPrefs::widget_dmabuf_webgl_enabled(); +} + +void DMABufDevice::SetModifiersToGfxVars() { + gfxVars::SetDMABufModifiersXRGB(mXRGBFormat.mModifiers); + gfxVars::SetDMABufModifiersARGB(mARGBFormat.mModifiers); +} + +void DMABufDevice::GetModifiersFromGfxVars() { + mXRGBFormat.mModifiers = gfxVars::DMABufModifiersXRGB().Clone(); + mARGBFormat.mModifiers = gfxVars::DMABufModifiersARGB().Clone(); +} + +void DMABufDevice::DisableDMABufWebGL() { sUseWebGLDmabufBackend = false; } + +GbmFormat* DMABufDevice::GetGbmFormat(bool aHasAlpha) { + GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat; + return format->mIsSupported ? format : nullptr; +} + +void DMABufDevice::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->mModifiers.AppendElement(((uint64_t)mModifierHi << 32) | mModifierLo); +} + +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) { + // skip modifiers marked as invalid + if (modifier_hi == (DRM_FORMAT_MOD_INVALID >> 32) && + modifier_lo == (DRM_FORMAT_MOD_INVALID & 0xffffffff)) { + return; + } + + auto* device = static_cast<DMABufDevice*>(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) { + 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.")); + 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}; + +void DMABufDevice::LoadFormatModifiers() { + if (!GdkIsWaylandDisplay()) { + return; + } + if (XRE_IsParentProcess()) { + MOZ_ASSERT(NS_IsMainThread()); + wl_display* display = WaylandDisplayGetWLDisplay(); + wl_registry* registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, this); + wl_display_roundtrip(display); + wl_display_roundtrip(display); + wl_registry_destroy(registry); + SetModifiersToGfxVars(); + } else { + GetModifiersFromGfxVars(); + } +} + +DMABufDevice* GetDMABufDevice() { + static DMABufDevice 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..9c1b05f1cd --- /dev/null +++ b/widget/gtk/DMABufLibWrapper.h @@ -0,0 +1,228 @@ +/* -*- 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 __MOZ_DMABUF_LIB_WRAPPER_H__ +#define __MOZ_DMABUF_LIB_WRAPPER_H__ + +#include "mozilla/widget/gbm.h" +#include "mozilla/StaticMutex.h" +#include <mutex> + +#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 */ + +#ifndef DRM_FORMAT_MOD_INVALID +# define DRM_FORMAT_MOD_INVALID ((1ULL << 56) - 1) +#endif + +namespace mozilla { +namespace widget { + +typedef struct gbm_device* (*CreateDeviceFunc)(int); +typedef void (*DestroyDeviceFunc)(struct gbm_device*); +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*); +typedef struct gbm_surface* (*CreateSurfaceFunc)(struct gbm_device*, uint32_t, + uint32_t, uint32_t, uint32_t); +typedef void (*DestroySurfaceFunc)(struct gbm_surface*); + +class GbmLib { + public: + static bool IsAvailable() { return sLoaded || Load(); } + static bool IsModifierAvailable(); + + static struct gbm_device* CreateDevice(int fd) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreateDevice(fd); + }; + static void DestroyDevice(struct gbm_device* gdm) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDestroyDevice(gdm); + }; + static struct gbm_bo* Create(struct gbm_device* gbm, uint32_t width, + uint32_t height, uint32_t format, + uint32_t flags) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreate(gbm, width, height, format, flags); + } + static void Destroy(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + sDestroy(bo); + } + static uint32_t GetStride(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetStride(bo); + } + static int GetFd(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + 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) { + StaticMutexAutoLock lockDRI(sDRILock); + return sMap(bo, x, y, width, height, flags, stride, map_data); + } + static void Unmap(struct gbm_bo* bo, void* map_data) { + StaticMutexAutoLock lockDRI(sDRILock); + 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) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreateWithModifiers(gbm, width, height, format, modifiers, count); + } + static uint64_t GetModifier(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetModifier(bo); + } + static int GetPlaneCount(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetPlaneCount(bo); + } + static union gbm_bo_handle GetHandleForPlane(struct gbm_bo* bo, int plane) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetHandleForPlane(bo, plane); + } + static uint32_t GetStrideForPlane(struct gbm_bo* bo, int plane) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetStrideForPlane(bo, plane); + } + static uint32_t GetOffset(struct gbm_bo* bo, int plane) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetOffset(bo, plane); + } + static int DeviceIsFormatSupported(struct gbm_device* gbm, uint32_t format, + uint32_t usage) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDeviceIsFormatSupported(gbm, format, usage); + } + static int DrmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, + int* prime_fd) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDrmPrimeHandleToFD(fd, handle, flags, prime_fd); + } + static struct gbm_surface* CreateSurface(struct gbm_device* gbm, + uint32_t width, uint32_t height, + uint32_t format, uint32_t flags) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreateSurface(gbm, width, height, format, flags); + } + static void DestroySurface(struct gbm_surface* surface) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDestroySurface(surface); + } + + private: + static bool Load(); + static bool IsLoaded(); + + static CreateDeviceFunc sCreateDevice; + static DestroyDeviceFunc sDestroyDevice; + 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 CreateSurfaceFunc sCreateSurface; + static DestroySurfaceFunc sDestroySurface; + static bool sLoaded; + + static void* sGbmLibHandle; + static void* sXf86DrmLibHandle; + static mozilla::StaticMutex sDRILock MOZ_UNANNOTATED; +}; + +struct GbmFormat { + bool mIsSupported; + bool mHasAlpha; + int mFormat; + nsTArray<uint64_t> mModifiers; +}; + +class DMABufDevice { + public: + DMABufDevice(); + ~DMABufDevice(); + + int OpenDRMFd(); + gbm_device* GetGbmDevice(); + int GetDmabufFD(uint32_t aGEMHandle); + + bool IsEnabled(nsACString& aFailureId); + + // Use dmabuf for WebRender general web content + static bool IsDMABufTexturesEnabled(); + // Use dmabuf for WebGL content + static bool IsDMABufWebGLEnabled(); + static void DisableDMABufWebGL(); + + void AddFormatModifier(bool aHasAlpha, int aFormat, uint32_t mModifierHi, + uint32_t mModifierLo); + GbmFormat* GetGbmFormat(bool aHasAlpha); + + private: + void Configure(); + void LoadFormatModifiers(); + + void SetModifiersToGfxVars(); + void GetModifiersFromGfxVars(); + + private: + GbmFormat mXRGBFormat; + GbmFormat mARGBFormat; + + int mDRMFd = -1; + std::once_flag mFlagGbmDevice; + gbm_device* mGbmDevice = nullptr; + const char* mFailureId = nullptr; + nsAutoCString mDrmRenderNode; +}; + +DMABufDevice* 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..d2c3396469 --- /dev/null +++ b/widget/gtk/DMABufSurface.cpp @@ -0,0 +1,1654 @@ +/* -*- 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 <sys/ioctl.h> + +#include "mozilla/widget/gbm.h" +#include "mozilla/widget/va_drmcommon.h" +#include "YCbCrUtils.h" +#include "mozilla/gfx/2D.h" +#include "GLContextTypes.h" // for GLContext, etc +#include "GLContextEGL.h" +#include "GLContextProvider.h" +#include "ScopedGLHelpers.h" +#include "GLBlitHelper.h" +#include "GLReadTexImageHelper.h" +#include "nsGtkUtils.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; +using namespace mozilla::gfx; + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +static LazyLogModule gDmabufRefLog("DmabufRef"); +# define LOGDMABUFREF(args) \ + MOZ_LOG(gDmabufRefLog, mozilla::LogLevel::Debug, args) +#else +# define LOGDMABUFREF(args) +#endif /* MOZ_LOGGING */ + +#define BUFFER_FLAGS 0 + +static RefPtr<GLContext> sSnapshotContext; +static StaticMutex sSnapshotContextMutex MOZ_UNANNOTATED; +static Atomic<int> gNewSurfaceUID(1); + +RefPtr<GLContext> ClaimSnapshotGLContext() { + if (!sSnapshotContext) { + nsCString discardFailureId; + sSnapshotContext = GLContextProvider::CreateHeadless({}, &discardFailureId); + if (!sSnapshotContext) { + LOGDMABUF( + ("ClaimSnapshotGLContext: Failed to create snapshot GLContext.")); + return nullptr; + } + sSnapshotContext->mOwningThreadId = Nothing(); // No singular owner. + } + if (!sSnapshotContext->MakeCurrent()) { + LOGDMABUF(("ClaimSnapshotGLContext: Failed to make GLContext current.")); + return nullptr; + } + return sSnapshotContext; +} + +void ReturnSnapshotGLContext(RefPtr<GLContext> aGLContext) { + // direct eglMakeCurrent() call breaks current context caching so make sure + // it's not used. + MOZ_ASSERT(!aGLContext->mUseTLSIsCurrent); + if (!aGLContext->IsCurrent()) { + LOGDMABUF(("ReturnSnapshotGLContext() failed, is not current!")); + return; + } + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + egl->fMakeCurrent(EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +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() { + if (!mGlobalRefCountFd) { + return; + } + LOGDMABUFREF(("DMABufSurface::GlobalRefRelease UID %d", mUID)); + uint64_t counter; + if (read(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) { + if (errno == EAGAIN) { + LOGDMABUFREF( + (" GlobalRefRelease failed: already zero reference! UID %d", mUID)); + } + // EAGAIN means the refcount is already zero. It happens when we release + // last reference to the surface. + if (errno != EAGAIN) { + NS_WARNING(nsPrintfCString("Failed to unref dmabuf global ref count: %s", + strerror(errno)) + .get()); + } + } +} + +void DMABufSurface::GlobalRefAdd() { + LOGDMABUFREF(("DMABufSurface::GlobalRefAdd UID %d", mUID)); + MOZ_DIAGNOSTIC_ASSERT(mGlobalRefCountFd); + uint64_t counter = 1; + if (write(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) { + NS_WARNING(nsPrintfCString("Failed to ref dmabuf global ref count: %s", + strerror(errno)) + .get()); + } +} + +void DMABufSurface::GlobalRefCountCreate() { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountCreate UID %d", mUID)); + MOZ_DIAGNOSTIC_ASSERT(!mGlobalRefCountFd); + // Create global ref count initialized to 0, + // i.e. is not referenced after create. + mGlobalRefCountFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE); + if (mGlobalRefCountFd < 0) { + NS_WARNING(nsPrintfCString("Failed to create dmabuf global ref count: %s", + strerror(errno)) + .get()); + mGlobalRefCountFd = 0; + return; + } +} + +void DMABufSurface::GlobalRefCountImport(int aFd) { + mGlobalRefCountFd = aFd; + if (mGlobalRefCountFd) { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountImport UID %d", mUID)); + GlobalRefAdd(); + } +} + +int DMABufSurface::GlobalRefCountExport() { +#ifdef MOZ_LOGGING + if (mGlobalRefCountFd) { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountExport UID %d", mUID)); + } +#endif + return mGlobalRefCountFd; +} + +void DMABufSurface::GlobalRefCountDelete() { + if (mGlobalRefCountFd) { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountDelete UID %d", mUID)); + close(mGlobalRefCountFd); + mGlobalRefCountFd = 0; + } +} + +void DMABufSurface::ReleaseDMABuf() { + LOGDMABUF(("DMABufSurface::ReleaseDMABuf() UID %d", mUID)); + for (int i = 0; i < mBufferPlaneCount; i++) { + Unmap(i); + } + + MutexAutoLock lockFD(mSurfaceLock); + CloseFileDescriptors(lockFD, /* aForceClose */ true); + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mGbmBufferObject[i]) { + GbmLib::Destroy(mGbmBufferObject[i]); + mGbmBufferObject[i] = nullptr; + } + } + mBufferPlaneCount = 0; +} + +DMABufSurface::DMABufSurface(SurfaceType aSurfaceType) + : mSurfaceType(aSurfaceType), + mBufferPlaneCount(0), + mDrmFormats(), + mStrides(), + mOffsets(), + mGbmBufferObject(), + mMappedRegion(), + mMappedRegionStride(), + mSyncFd(-1), + mSync(nullptr), + mGlobalRefCountFd(0), + mUID(gNewSurfaceUID++), + mSurfaceLock("DMABufSurface") { + for (auto& slot : mDmabufFds) { + slot = -1; + } + for (auto& modifier : mBufferModifiers) { + modifier = DRM_FORMAT_MOD_INVALID; + } +} + +DMABufSurface::~DMABufSurface() { + FenceDelete(); + GlobalRefRelease(); + 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 (mSyncFd > 0) { + close(mSyncFd); + mSyncFd = -1; + } + + if (!mGL) { + return; + } + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (mSync) { + egl->fDestroySync(mSync); + mSync = nullptr; + } +} + +void DMABufSurface::FenceSet() { + if (!mGL || !mGL->MakeCurrent()) { + MOZ_DIAGNOSTIC_ASSERT(mGL, + "DMABufSurface::FenceSet(): missing GL context!"); + 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 || mSyncFd < 0) { + MOZ_DIAGNOSTIC_ASSERT(mGL, + "DMABufSurface::FenceWait() missing GL context!"); + return; + } + + 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}; + EGLSync sync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, attribs); + if (!sync) { + MOZ_ASSERT(false, "DMABufSurface::FenceWait(): Failed to create GLFence!"); + // We failed to create GLFence so clear mSyncFd to avoid another try. + close(mSyncFd); + mSyncFd = -1; + return; + } + + // mSyncFd is owned by GLFence so clear local reference to avoid double close + // at DMABufSurface::FenceDelete(). + mSyncFd = -1; + + egl->fClientWaitSync(sync, 0, LOCAL_EGL_FOREVER); + egl->fDestroySync(sync); +} + +bool DMABufSurface::OpenFileDescriptors(const MutexAutoLock& aProofOfLock) { + for (int i = 0; i < mBufferPlaneCount; i++) { + if (!OpenFileDescriptorForPlane(aProofOfLock, i)) { + return false; + } + } + return true; +} + +// We can safely close DMABuf file descriptors only when we have a valid +// GbmBufferObject. When we don't have a valid GbmBufferObject and a DMABuf +// file descriptor is closed, whole surface is released. +void DMABufSurface::CloseFileDescriptors(const MutexAutoLock& aProofOfLock, + bool aForceClose) { + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + CloseFileDescriptorForPlane(aProofOfLock, i, aForceClose); + } +} + +DMABufSurfaceRGBA::DMABufSurfaceRGBA() + : DMABufSurface(SURFACE_RGBA), + mSurfaceFlags(0), + mWidth(0), + mHeight(0), + mGmbFormat(nullptr), + mEGLImage(LOCAL_EGL_NO_IMAGE), + mTexture(0), + mGbmBufferFlags(0), + mWlBuffer(nullptr) {} + +DMABufSurfaceRGBA::~DMABufSurfaceRGBA() { + ReleaseWlBuffer(); + ReleaseSurface(); +} + +bool DMABufSurfaceRGBA::OpenFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane) { + if (mDmabufFds[aPlane] >= 0) { + return true; + } + gbm_bo* bo = mGbmBufferObject[0]; + if (NS_WARN_IF(!bo)) { + LOGDMABUF( + ("DMABufSurfaceRGBA::OpenFileDescriptorForPlane: Missing " + "mGbmBufferObject object!")); + return false; + } + + if (mBufferPlaneCount == 1) { + MOZ_ASSERT(aPlane == 0, "DMABuf: wrong surface plane!"); + mDmabufFds[0] = GbmLib::GetFd(bo); + } else { + mDmabufFds[aPlane] = GetDMABufDevice()->GetDmabufFD( + GbmLib::GetHandleForPlane(bo, aPlane).u32); + } + + if (mDmabufFds[aPlane] < 0) { + CloseFileDescriptors(aProofOfLock); + return false; + } + + return true; +} + +void DMABufSurfaceRGBA::CloseFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane, bool aForceClose = false) { + if ((aForceClose || mGbmBufferObject[0]) && mDmabufFds[aPlane] >= 0) { + close(mDmabufFds[aPlane]); + mDmabufFds[aPlane] = -1; + } +} + +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)); + + if (!GetDMABufDevice()->GetGbmDevice()) { + LOGDMABUF((" Missing GbmDevice!")); + return false; + } + + mGmbFormat = GetDMABufDevice()->GetGbmFormat(mSurfaceFlags & DMABUF_ALPHA); + if (!mGmbFormat) { + // Requested DRM format is not supported. + return false; + } + mDrmFormats[0] = mGmbFormat->mFormat; + + bool useModifiers = (aDMABufSurfaceFlags & DMABUF_USE_MODIFIERS) && + !mGmbFormat->mModifiers.IsEmpty(); + if (useModifiers) { + LOGDMABUF((" Creating with modifiers\n")); + mGbmBufferObject[0] = GbmLib::CreateWithModifiers( + GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, mDrmFormats[0], + mGmbFormat->mModifiers.Elements(), mGmbFormat->mModifiers.Length()); + if (mGbmBufferObject[0]) { + mBufferModifiers[0] = GbmLib::GetModifier(mGbmBufferObject[0]); + } + } + + if (!mGbmBufferObject[0]) { + LOGDMABUF((" Creating without modifiers\n")); + mGbmBufferFlags = GBM_BO_USE_LINEAR; + mGbmBufferObject[0] = + GbmLib::Create(GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, + mDrmFormats[0], mGbmBufferFlags); + mBufferModifiers[0] = DRM_FORMAT_MOD_INVALID; + } + + if (!mGbmBufferObject[0]) { + LOGDMABUF((" Failed to create GbmBufferObject\n")); + return false; + } + + if (mBufferModifiers[0] != DRM_FORMAT_MOD_INVALID) { + mBufferPlaneCount = GbmLib::GetPlaneCount(mGbmBufferObject[0]); + if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) { + LOGDMABUF((" There's too many dmabuf planes!")); + ReleaseSurface(); + return false; + } + + for (int i = 0; i < mBufferPlaneCount; i++) { + mStrides[i] = GbmLib::GetStrideForPlane(mGbmBufferObject[0], i); + mOffsets[i] = GbmLib::GetOffset(mGbmBufferObject[0], i); + } + } else { + mBufferPlaneCount = 1; + mStrides[0] = GbmLib::GetStride(mGbmBufferObject[0]); + } + + LOGDMABUF((" Success\n")); + return true; +} + +bool DMABufSurfaceRGBA::Create(mozilla::gl::GLContext* aGLContext, + const EGLImageKHR aEGLImage, int aWidth, + int aHeight) { + LOGDMABUF(("DMABufSurfaceRGBA::Create() from EGLImage UID = %d\n", mUID)); + if (!aGLContext) { + return false; + } + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + mGL = aGLContext; + mWidth = aWidth; + mHeight = aHeight; + mEGLImage = aEGLImage; + if (!egl->fExportDMABUFImageQuery(mEGLImage, mDrmFormats, &mBufferPlaneCount, + mBufferModifiers)) { + LOGDMABUF((" ExportDMABUFImageQueryMESA failed, quit\n")); + return false; + } + if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) { + LOGDMABUF((" wrong plane count %d, quit\n", mBufferPlaneCount)); + return false; + } + if (!egl->fExportDMABUFImage(mEGLImage, mDmabufFds, mStrides, mOffsets)) { + LOGDMABUF((" ExportDMABUFImageMESA failed, quit\n")); + return false; + } + + // A broken driver can return dmabuf without valid file descriptors + // which leads to fails later so quit now. + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mDmabufFds[i] < 0) { + LOGDMABUF( + (" ExportDMABUFImageMESA failed, mDmabufFds[%d] is invalid, quit", + i)); + return false; + } + } + + LOGDMABUF((" imported size %d x %d format %x planes %d modifiers %" PRIx64, + mWidth, mHeight, mDrmFormats[0], mBufferPlaneCount, + mBufferModifiers[0])); + return true; +} + +bool DMABufSurfaceRGBA::ImportSurfaceDescriptor( + const SurfaceDescriptor& aDesc) { + const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf(); + + mWidth = desc.width()[0]; + mHeight = desc.height()[0]; + mBufferModifiers[0] = desc.modifier()[0]; + mDrmFormats[0] = desc.format()[0]; + mBufferPlaneCount = desc.fds().Length(); + mGbmBufferFlags = desc.flags(); + MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES); + mUID = desc.uid(); + + LOGDMABUF( + ("DMABufSurfaceRGBA::ImportSurfaceDescriptor() UID %d size %d x %d\n", + mUID, mWidth, mHeight)); + + for (int i = 0; i < mBufferPlaneCount; i++) { + mDmabufFds[i] = desc.fds()[i].ClonePlatformHandle().release(); + if (mDmabufFds[i] < 0) { + LOGDMABUF( + (" failed to get DMABuf file descriptor: %s", strerror(errno))); + return false; + } + mStrides[i] = desc.strides()[i]; + mOffsets[i] = desc.offsets()[i]; + } + + if (desc.fence().Length() > 0) { + mSyncFd = desc.fence()[0].ClonePlatformHandle().release(); + if (mSyncFd < 0) { + LOGDMABUF( + (" failed to get GL fence file descriptor: %s", strerror(errno))); + return false; + } + } + + if (desc.refCount().Length() > 0) { + GlobalRefCountImport(desc.refCount()[0].ClonePlatformHandle().release()); + } + + LOGDMABUF((" imported size %d x %d format %x planes %d", mWidth, mHeight, + mDrmFormats[0], mBufferPlaneCount)); + return true; +} + +bool DMABufSurfaceRGBA::Create(const SurfaceDescriptor& aDesc) { + return ImportSurfaceDescriptor(aDesc); +} + +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<uint64_t, DMABUF_BUFFER_PLANES> modifiers; + AutoTArray<ipc::FileDescriptor, 1> fenceFDs; + AutoTArray<ipc::FileDescriptor, 1> refCountFDs; + + LOGDMABUF(("DMABufSurfaceRGBA::Serialize() UID %d\n", mUID)); + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + + width.AppendElement(mWidth); + height.AppendElement(mHeight); + format.AppendElement(mDrmFormats[0]); + modifiers.AppendElement(mBufferModifiers[0]); + for (int i = 0; i < mBufferPlaneCount; i++) { + fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i])); + strides.AppendElement(mStrides[i]); + offsets.AppendElement(mOffsets[i]); + } + + CloseFileDescriptors(lockFD); + + if (mSync) { + fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd)); + } + + if (mGlobalRefCountFd) { + refCountFDs.AppendElement(ipc::FileDescriptor(GlobalRefCountExport())); + } + + aOutDescriptor = SurfaceDescriptorDMABuf( + mSurfaceType, modifiers, mGbmBufferFlags, fds, width, height, width, + height, format, strides, offsets, GetYUVColorSpace(), mColorRange, + fenceFDs, mUID, refCountFDs); + return true; +} + +bool DMABufSurfaceRGBA::CreateTexture(GLContext* aGLContext, int aPlane) { + LOGDMABUF(("DMABufSurfaceRGBA::CreateTexture() UID %d\n", mUID)); + 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); + 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 (mBufferModifiers[0] != DRM_FORMAT_MOD_INVALID) { \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \ + attribs.AppendElement(mBufferModifiers[0] & 0xFFFFFFFF); \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \ + attribs.AppendElement(mBufferModifiers[0] >> 32); \ + } \ + } + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + 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()); + + CloseFileDescriptors(lockFD); + + if (mEGLImage == LOCAL_EGL_NO_IMAGE) { + LOGDMABUF(("EGLImageKHR creation failed")); + return false; + } + + if (!aGLContext->MakeCurrent()) { + LOGDMABUF( + ("DMABufSurfaceRGBA::CreateTexture(): failed to make GL context " + "current")); + return false; + } + 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() { + LOGDMABUF(("DMABufSurfaceRGBA::ReleaseTextures() UID %d\n", mUID)); + FenceDelete(); + + if (!mTexture && !mEGLImage) { + return; + } + + if (!mGL) { +#ifdef NIGHTLY_BUILD + MOZ_DIAGNOSTIC_ASSERT(mGL, "Missing GL context!"); +#else + NS_WARNING( + "DMABufSurfaceRGBA::ReleaseTextures(): Missing GL context! We're " + "leaking textures!"); + return; +#endif + } + + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (mTexture && mGL->MakeCurrent()) { + mGL->fDeleteTextures(1, &mTexture); + mTexture = 0; + } + + if (mEGLImage != LOCAL_EGL_NO_IMAGE) { + egl->fDestroyImage(mEGLImage); + mEGLImage = LOCAL_EGL_NO_IMAGE; + } + mGL = nullptr; +} + +void DMABufSurfaceRGBA::ReleaseSurface() { + MOZ_ASSERT(!IsMapped(), "We can't release mapped buffer!"); + + ReleaseTextures(); + ReleaseDMABuf(); +} + +bool DMABufSurfaceRGBA::CreateWlBuffer() { + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + + nsWaylandDisplay* waylandDisplay = widget::WaylandDisplayGet(); + if (!waylandDisplay->GetDmabuf()) { + CloseFileDescriptors(lockFD); + return false; + } + + struct zwp_linux_buffer_params_v1* params = + zwp_linux_dmabuf_v1_create_params(waylandDisplay->GetDmabuf()); + zwp_linux_buffer_params_v1_add(params, mDmabufFds[0], 0, mOffsets[0], + mStrides[0], mBufferModifiers[0] >> 32, + mBufferModifiers[0] & 0xffffffff); + + mWlBuffer = zwp_linux_buffer_params_v1_create_immed( + params, GetWidth(), GetHeight(), mDrmFormats[0], 0); + + CloseFileDescriptors(lockFD); + + return mWlBuffer != nullptr; +} + +void DMABufSurfaceRGBA::ReleaseWlBuffer() { + MozClearPointer(mWlBuffer, wl_buffer_destroy); +} + +// We should synchronize DMA Buffer object access from CPU to avoid potential +// cache incoherency and data loss. +// See +// https://01.org/linuxgraphics/gfx-docs/drm/driver-api/dma-buf.html#cpu-access-to-dma-buffer-objects +struct dma_buf_sync { + uint64_t flags; +}; +#define DMA_BUF_SYNC_READ (1 << 0) +#define DMA_BUF_SYNC_WRITE (2 << 0) +#define DMA_BUF_SYNC_START (0 << 2) +#define DMA_BUF_SYNC_END (1 << 2) +#define DMA_BUF_BASE 'b' +#define DMA_BUF_IOCTL_SYNC _IOW(DMA_BUF_BASE, 0, struct dma_buf_sync) + +static void SyncDmaBuf(int aFd, uint64_t aFlags) { + struct dma_buf_sync sync = {0}; + + sync.flags = aFlags | DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE; + while (true) { + int ret; + ret = ioctl(aFd, DMA_BUF_IOCTL_SYNC, &sync); + if (ret == -1 && errno == EINTR) { + continue; + } else if (ret == -1) { + LOGDMABUF( + ("Failed to synchronize DMA buffer: %s FD %d", strerror(errno), aFd)); + break; + } else { + break; + } + } +} + +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 plane %d size %d x %d -> %d x " + "%d\n", + mUID, aPlane, aX, aY, aWidth, aHeight)); + + mMappedRegionStride[aPlane] = 0; + mMappedRegionData[aPlane] = nullptr; + mMappedRegion[aPlane] = + GbmLib::Map(mGbmBufferObject[aPlane], aX, aY, aWidth, aHeight, aGbmFlags, + &mMappedRegionStride[aPlane], &mMappedRegionData[aPlane]); + if (!mMappedRegion[aPlane]) { + LOGDMABUF((" Surface mapping failed: %s", strerror(errno))); + return nullptr; + } + if (aStride) { + *aStride = mMappedRegionStride[aPlane]; + } + + MutexAutoLock lockFD(mSurfaceLock); + if (OpenFileDescriptorForPlane(lockFD, aPlane)) { + SyncDmaBuf(mDmabufFds[aPlane], DMA_BUF_SYNC_START); + CloseFileDescriptorForPlane(lockFD, 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]) { + LOGDMABUF(("DMABufSurface::Unmap() UID %d plane %d\n", mUID, aPlane)); + MutexAutoLock lockFD(mSurfaceLock); + if (OpenFileDescriptorForPlane(lockFD, aPlane)) { + SyncDmaBuf(mDmabufFds[aPlane], DMA_BUF_SYNC_END); + CloseFileDescriptorForPlane(lockFD, aPlane); + } + GbmLib::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; +} + +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<DMABufSurface> DMABufSurfaceRGBA::CreateDMABufSurface( + mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, int aWidth, + int aHeight) { + RefPtr<DMABufSurfaceRGBA> surf = new DMABufSurfaceRGBA(); + if (!surf->Create(aGLContext, aEGLImage, aWidth, aHeight)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CreateYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) { + RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV(); + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurface() UID %d from desc\n", + surf->GetUID())); + if (!surf->UpdateYUVData(aDesc, aWidth, aHeight, /* aCopy */ false)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CopyYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) { + RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV(); + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurfaceCopy() UID %d from desc\n", + surf->GetUID())); + if (!surf->UpdateYUVData(aDesc, aWidth, aHeight, /* aCopy */ true)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData, int* aLineSizes) { + RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV(); + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurface() UID %d %d x %d\n", + surf->GetUID(), aWidth, aHeight)); + if (!surf->Create(aWidth, aHeight, aPixelData, aLineSizes)) { + return nullptr; + } + return surf.forget(); +} + +DMABufSurfaceYUV::DMABufSurfaceYUV() + : DMABufSurface(SURFACE_NV12), + mWidth(), + mHeight(), + mWidthAligned(), + mHeightAligned(), + mTexture() { + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + mEGLImage[i] = LOCAL_EGL_NO_IMAGE; + } +} + +DMABufSurfaceYUV::~DMABufSurfaceYUV() { ReleaseSurface(); } + +bool DMABufSurfaceYUV::OpenFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane) { + // The fd is already opened, no need to reopen. + // This can happen when we import dmabuf surface from VA-API decoder, + // mGbmBufferObject is null and we don't close + // file descriptors for surface as they are our only reference to it. + if (mDmabufFds[aPlane] >= 0) { + return true; + } + + if (mGbmBufferObject[aPlane] == nullptr) { + LOGDMABUF( + ("DMABufSurfaceYUV::OpenFileDescriptorForPlane: Missing " + "mGbmBufferObject object!")); + return false; + } + + mDmabufFds[aPlane] = GbmLib::GetFd(mGbmBufferObject[aPlane]); + if (mDmabufFds[aPlane] < 0) { + CloseFileDescriptors(aProofOfLock); + return false; + } + return true; +} + +void DMABufSurfaceYUV::CloseFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane, bool aForceClose = false) { + if ((aForceClose || mGbmBufferObject[aPlane]) && mDmabufFds[aPlane] >= 0) { + close(mDmabufFds[aPlane]); + mDmabufFds[aPlane] = -1; + } +} + +bool DMABufSurfaceYUV::ImportPRIMESurfaceDescriptor( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) { + LOGDMABUF(("DMABufSurfaceYUV::ImportPRIMESurfaceDescriptor() UID %d", mUID)); + // Already exists? + MOZ_DIAGNOSTIC_ASSERT(mDmabufFds[0] < 0); + + if (aDesc.num_layers > DMABUF_BUFFER_PLANES || + aDesc.num_objects > DMABUF_BUFFER_PLANES) { + LOGDMABUF((" Can't import, wrong layers/objects number (%d, %d)", + aDesc.num_layers, aDesc.num_objects)); + return false; + } + if (aDesc.fourcc == VA_FOURCC_NV12) { + mSurfaceType = SURFACE_NV12; + } else if (aDesc.fourcc == VA_FOURCC_P010) { + mSurfaceType = SURFACE_NV12; + } else if (aDesc.fourcc == VA_FOURCC_YV12) { + mSurfaceType = SURFACE_YUV420; + } else { + LOGDMABUF((" Can't import surface data of 0x%x format", aDesc.fourcc)); + return false; + } + + mBufferPlaneCount = aDesc.num_layers; + + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + // All supported formats have 4:2:0 chroma sub-sampling. + unsigned int subsample = i == 0 ? 0 : 1; + + unsigned int object = aDesc.layers[i].object_index[0]; + mBufferModifiers[i] = aDesc.objects[object].drm_format_modifier; + mDrmFormats[i] = aDesc.layers[i].drm_format; + mOffsets[i] = aDesc.layers[i].offset[0]; + mStrides[i] = aDesc.layers[i].pitch[0]; + mWidthAligned[i] = aDesc.width >> subsample; + mHeightAligned[i] = aDesc.height >> subsample; + mWidth[i] = aWidth >> subsample; + mHeight[i] = aHeight >> subsample; + LOGDMABUF((" plane %d size %d x %d format %x", i, mWidth[i], mHeight[i], + mDrmFormats[i])); + } + return true; +} + +bool DMABufSurfaceYUV::MoveYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight) { + if (!ImportPRIMESurfaceDescriptor(aDesc, aWidth, aHeight)) { + return false; + } + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + unsigned int object = aDesc.layers[i].object_index[0]; + // Keep VADRMPRIMESurfaceDescriptor untouched and dup() dmabuf + // file descriptors. + mDmabufFds[i] = dup(aDesc.objects[object].fd); + } + return true; +} + +void DMABufSurfaceYUV::ReleaseVADRMPRIMESurfaceDescriptor( + VADRMPRIMESurfaceDescriptor& aDesc) { + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + unsigned int object = aDesc.layers[i].object_index[0]; + if (aDesc.objects[object].fd != -1) { + close(aDesc.objects[object].fd); + aDesc.objects[object].fd = -1; + } + } +} + +bool DMABufSurfaceYUV::CreateYUVPlane(int aPlane) { + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVPlane() UID %d size %d x %d", mUID, + mWidth[aPlane], mHeight[aPlane])); + + if (!GetDMABufDevice()->GetGbmDevice()) { + LOGDMABUF((" Missing GbmDevice!")); + return false; + } + + MOZ_DIAGNOSTIC_ASSERT(mGbmBufferObject[aPlane] == nullptr); + bool useModifiers = (mBufferModifiers[aPlane] != DRM_FORMAT_MOD_INVALID); + if (useModifiers) { + LOGDMABUF((" Creating with modifiers")); + mGbmBufferObject[aPlane] = GbmLib::CreateWithModifiers( + GetDMABufDevice()->GetGbmDevice(), mWidth[aPlane], mHeight[aPlane], + mDrmFormats[aPlane], mBufferModifiers + aPlane, 1); + } + if (!mGbmBufferObject[aPlane]) { + LOGDMABUF((" Creating without modifiers")); + mGbmBufferObject[aPlane] = GbmLib::Create( + GetDMABufDevice()->GetGbmDevice(), mWidth[aPlane], mHeight[aPlane], + mDrmFormats[aPlane], GBM_BO_USE_RENDERING); + mBufferModifiers[aPlane] = DRM_FORMAT_MOD_INVALID; + } + if (!mGbmBufferObject[aPlane]) { + LOGDMABUF((" Failed to create GbmBufferObject: %s", strerror(errno))); + return false; + } + + mStrides[aPlane] = GbmLib::GetStride(mGbmBufferObject[aPlane]); + mOffsets[aPlane] = GbmLib::GetOffset(mGbmBufferObject[aPlane], 0); + mWidthAligned[aPlane] = mWidth[aPlane]; + mHeightAligned[aPlane] = mHeight[aPlane]; + return true; +} + +bool DMABufSurfaceYUV::CopyYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight) { + RefPtr<DMABufSurfaceYUV> tmpSurf = CreateYUVSurface(aDesc, aWidth, aHeight); + if (!tmpSurf) { + return false; + } + + if (!ImportPRIMESurfaceDescriptor(aDesc, aWidth, aHeight)) { + return false; + } + + StaticMutexAutoLock lock(sSnapshotContextMutex); + RefPtr<GLContext> context = ClaimSnapshotGLContext(); + auto releaseTextures = MakeScopeExit([&] { + tmpSurf->ReleaseTextures(); + ReleaseTextures(); + ReturnSnapshotGLContext(context); + }); + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (!tmpSurf->CreateTexture(context, i)) { + return false; + } + if (!CreateYUVPlane(i) || !CreateTexture(context, i)) { + return false; + } + gfx::IntSize size(GetWidth(i), GetHeight(i)); + context->BlitHelper()->BlitTextureToTexture( + tmpSurf->GetTexture(i), GetTexture(i), size, size, LOCAL_GL_TEXTURE_2D, + LOCAL_GL_TEXTURE_2D); + } + return true; +} + +bool DMABufSurfaceYUV::UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight, bool aCopy) { + LOGDMABUF(("DMABufSurfaceYUV::UpdateYUVData() UID %d copy %d", mUID, aCopy)); + return aCopy ? CopyYUVDataImpl(aDesc, aWidth, aHeight) + : MoveYUVDataImpl(aDesc, aWidth, aHeight); +} + +bool DMABufSurfaceYUV::CreateLinearYUVPlane(int aPlane, int aWidth, int aHeight, + int aDrmFormat) { + LOGDMABUF(("DMABufSurfaceYUV::CreateLinearYUVPlane() UID %d size %d x %d", + mUID, aWidth, aHeight)); + + if (!GetDMABufDevice()->GetGbmDevice()) { + LOGDMABUF((" Missing GbmDevice!")); + return false; + } + + mWidth[aPlane] = aWidth; + mHeight[aPlane] = aHeight; + mDrmFormats[aPlane] = aDrmFormat; + + mGbmBufferObject[aPlane] = + GbmLib::Create(GetDMABufDevice()->GetGbmDevice(), aWidth, aHeight, + aDrmFormat, GBM_BO_USE_LINEAR); + if (!mGbmBufferObject[aPlane]) { + LOGDMABUF((" Failed to create GbmBufferObject: %s", strerror(errno))); + return false; + } + + mStrides[aPlane] = GbmLib::GetStride(mGbmBufferObject[aPlane]); + mDmabufFds[aPlane] = -1; + + return true; +} + +void DMABufSurfaceYUV::UpdateYUVPlane(int aPlane, void* aPixelData, + int aLineSize) { + LOGDMABUF( + ("DMABufSurfaceYUV::UpdateYUVPlane() UID %d plane %d", mUID, aPlane)); + 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) { + LOGDMABUF(("DMABufSurfaceYUV::UpdateYUVData() UID %d", mUID)); + if (mSurfaceType != SURFACE_YUV420) { + LOGDMABUF((" UpdateYUVData can upload YUV420 surface type only!")); + return false; + } + + if (mBufferPlaneCount != 3) { + LOGDMABUF((" 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]) { + LOGDMABUF((" DMABufSurfaceYUV plane can't be mapped!")); + return false; + } + if ((int)mMappedRegionStride[i] < mWidth[i]) { + LOGDMABUF((" 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) { + LOGDMABUF(("DMABufSurfaceYUV::Create() UID %d size %d x %d", mUID, aWidth, + aHeight)); + + mSurfaceType = SURFACE_YUV420; + mBufferPlaneCount = 3; + + if (!CreateLinearYUVPlane(0, aWidth, aHeight, GBM_FORMAT_R8)) { + return false; + } + if (!CreateLinearYUVPlane(1, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) { + return false; + } + if (!CreateLinearYUVPlane(2, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) { + return false; + } + if (!aPixelData || !aLineSizes) { + return true; + } + return UpdateYUVData(aPixelData, aLineSizes); +} + +bool DMABufSurfaceYUV::Create(const SurfaceDescriptor& aDesc) { + return ImportSurfaceDescriptor(aDesc); +} + +bool DMABufSurfaceYUV::ImportSurfaceDescriptor( + const SurfaceDescriptorDMABuf& aDesc) { + mBufferPlaneCount = aDesc.fds().Length(); + mSurfaceType = (mBufferPlaneCount == 2) ? SURFACE_NV12 : SURFACE_YUV420; + mColorSpace = aDesc.yUVColorSpace(); + mColorRange = aDesc.colorRange(); + mUID = aDesc.uid(); + + LOGDMABUF(("DMABufSurfaceYUV::ImportSurfaceDescriptor() UID %d", mUID)); + + MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES); + for (int i = 0; i < mBufferPlaneCount; i++) { + mDmabufFds[i] = aDesc.fds()[i].ClonePlatformHandle().release(); + if (mDmabufFds[i] < 0) { + LOGDMABUF((" failed to get DMABuf plane file descriptor: %s", + strerror(errno))); + return false; + } + mWidth[i] = aDesc.width()[i]; + mHeight[i] = aDesc.height()[i]; + mWidthAligned[i] = aDesc.widthAligned()[i]; + mHeightAligned[i] = aDesc.heightAligned()[i]; + mDrmFormats[i] = aDesc.format()[i]; + mStrides[i] = aDesc.strides()[i]; + mOffsets[i] = aDesc.offsets()[i]; + mBufferModifiers[i] = aDesc.modifier()[i]; + LOGDMABUF((" plane %d fd %d size %d x %d format %x", i, mDmabufFds[i], + mWidth[i], mHeight[i], mDrmFormats[i])); + } + + if (aDesc.fence().Length() > 0) { + mSyncFd = aDesc.fence()[0].ClonePlatformHandle().release(); + if (mSyncFd < 0) { + LOGDMABUF( + (" failed to get GL fence file descriptor: %s", strerror(errno))); + return false; + } + } + + if (aDesc.refCount().Length() > 0) { + GlobalRefCountImport(aDesc.refCount()[0].ClonePlatformHandle().release()); + } + + return true; +} + +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> widthBytes; + AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> heightBytes; + 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<uint64_t, DMABUF_BUFFER_PLANES> modifiers; + AutoTArray<ipc::FileDescriptor, 1> fenceFDs; + AutoTArray<ipc::FileDescriptor, 1> refCountFDs; + + LOGDMABUF(("DMABufSurfaceYUV::Serialize() UID %d", mUID)); + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + + for (int i = 0; i < mBufferPlaneCount; i++) { + width.AppendElement(mWidth[i]); + height.AppendElement(mHeight[i]); + widthBytes.AppendElement(mWidthAligned[i]); + heightBytes.AppendElement(mHeightAligned[i]); + format.AppendElement(mDrmFormats[i]); + fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i])); + strides.AppendElement(mStrides[i]); + offsets.AppendElement(mOffsets[i]); + modifiers.AppendElement(mBufferModifiers[i]); + } + + CloseFileDescriptors(lockFD); + + if (mSync) { + fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd)); + } + + if (mGlobalRefCountFd) { + refCountFDs.AppendElement(ipc::FileDescriptor(GlobalRefCountExport())); + } + + aOutDescriptor = SurfaceDescriptorDMABuf( + mSurfaceType, modifiers, 0, fds, width, height, widthBytes, heightBytes, + format, strides, offsets, GetYUVColorSpace(), mColorRange, fenceFDs, mUID, + refCountFDs); + return true; +} + +bool DMABufSurfaceYUV::CreateEGLImage(GLContext* aGLContext, int aPlane) { + LOGDMABUF( + ("DMABufSurfaceYUV::CreateEGLImage() UID %d plane %d", mUID, aPlane)); + MOZ_ASSERT(mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE, + "EGLImage is already created!"); + MOZ_ASSERT(aGLContext, "Missing GLContext!"); + + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptorForPlane(lockFD, aPlane)) { + LOGDMABUF((" failed to open dmabuf file descriptors")); + return false; + } + + nsTArray<EGLint> attribs; + attribs.AppendElement(LOCAL_EGL_WIDTH); + attribs.AppendElement(mWidthAligned[aPlane]); + attribs.AppendElement(LOCAL_EGL_HEIGHT); + attribs.AppendElement(mHeightAligned[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]); \ + if (mBufferModifiers[aPlane] != DRM_FORMAT_MOD_INVALID) { \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \ + attribs.AppendElement(mBufferModifiers[aPlane] & 0xFFFFFFFF); \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \ + attribs.AppendElement(mBufferModifiers[aPlane] >> 32); \ + } + 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()); + + CloseFileDescriptorForPlane(lockFD, aPlane); + + if (mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE) { + LOGDMABUF((" EGLImageKHR creation failed")); + return false; + } + + LOGDMABUF((" Success.")); + return true; +} + +void DMABufSurfaceYUV::ReleaseEGLImages(GLContext* aGLContext) { + LOGDMABUF(("DMABufSurfaceYUV::ReleaseEGLImages() UID %d", mUID)); + MOZ_ASSERT(aGLContext, "Missing GLContext!"); + + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mEGLImage[i] != LOCAL_EGL_NO_IMAGE) { + egl->fDestroyImage(mEGLImage[i]); + mEGLImage[i] = LOCAL_EGL_NO_IMAGE; + } + } +} + +bool DMABufSurfaceYUV::CreateTexture(GLContext* aGLContext, int aPlane) { + LOGDMABUF( + ("DMABufSurfaceYUV::CreateTexture() UID %d plane %d", mUID, aPlane)); + MOZ_ASSERT(!mTexture[aPlane], "Texture is already created!"); + MOZ_ASSERT(aGLContext, "Missing GLContext!"); + + if (!CreateEGLImage(aGLContext, aPlane)) { + return false; + } + if (!aGLContext->MakeCurrent()) { + LOGDMABUF((" Failed to make GL context current.")); + return false; + } + 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() { + LOGDMABUF(("DMABufSurfaceYUV::ReleaseTextures() UID %d", mUID)); + + FenceDelete(); + + bool textureActive = false; + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mTexture[i] || mEGLImage[i]) { + textureActive = true; + break; + } + } + + if (!textureActive) { + return; + } + + if (!mGL) { +#ifdef NIGHTLY_BUILD + MOZ_DIAGNOSTIC_ASSERT(mGL, "Missing GL context!"); +#else + NS_WARNING( + "DMABufSurfaceYUV::ReleaseTextures(): Missing GL context! We're " + "leaking textures!"); + return; +#endif + } + + if (!mGL->MakeCurrent()) { + NS_WARNING( + "DMABufSurfaceYUV::ReleaseTextures(): MakeCurrent failed. We're " + "leaking textures!"); + return; + } + + mGL->fDeleteTextures(DMABUF_BUFFER_PLANES, mTexture); + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + mTexture[i] = 0; + } + ReleaseEGLImages(mGL); + mGL = nullptr; +} + +bool DMABufSurfaceYUV::VerifyTextureCreation() { + LOGDMABUF(("DMABufSurfaceYUV::VerifyTextureCreation() UID %d", mUID)); + + StaticMutexAutoLock lock(sSnapshotContextMutex); + RefPtr<GLContext> context = ClaimSnapshotGLContext(); + auto release = MakeScopeExit([&] { + ReleaseEGLImages(context); + ReturnSnapshotGLContext(context); + }); + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (!CreateEGLImage(context, i)) { + LOGDMABUF((" failed to create EGL image!")); + return false; + } + } + + LOGDMABUF((" success")); + return true; +} + +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(); } + +int 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() { + LOGDMABUF(("DMABufSurfaceYUV::ReleaseSurface() UID %d", mUID)); + ReleaseTextures(); + ReleaseDMABuf(); +} + +already_AddRefed<gfx::DataSourceSurface> +DMABufSurfaceYUV::GetAsSourceSurface() { + LOGDMABUF(("DMABufSurfaceYUV::GetAsSourceSurface UID %d", mUID)); + + StaticMutexAutoLock lock(sSnapshotContextMutex); + RefPtr<GLContext> context = ClaimSnapshotGLContext(); + auto releaseTextures = mozilla::MakeScopeExit([&] { + ReleaseTextures(); + ReturnSnapshotGLContext(context); + }); + + for (int i = 0; i < GetTextureCount(); i++) { + if (!GetTexture(i) && !CreateTexture(context, i)) { + LOGDMABUF(("GetAsSourceSurface: Failed to create DMABuf textures.")); + return nullptr; + } + } + + ScopedTexture scopedTex(context); + ScopedBindTexture boundTex(context, scopedTex.Texture()); + + gfx::IntSize size(GetWidth(), GetHeight()); + context->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, size.width, + size.height, 0, LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, + nullptr); + + ScopedFramebufferForTexture autoFBForTex(context, scopedTex.Texture()); + if (!autoFBForTex.IsComplete()) { + LOGDMABUF(("GetAsSourceSurface: ScopedFramebufferForTexture failed.")); + return nullptr; + } + + const gl::OriginPos destOrigin = gl::OriginPos::BottomLeft; + { + const ScopedBindFramebuffer bindFB(context, autoFBForTex.FB()); + if (!context->BlitHelper()->Blit(this, size, destOrigin)) { + LOGDMABUF(("GetAsSourceSurface: Blit failed.")); + return nullptr; + } + } + + RefPtr<gfx::DataSourceSurface> source = + gfx::Factory::CreateDataSourceSurface(size, gfx::SurfaceFormat::B8G8R8A8); + if (NS_WARN_IF(!source)) { + LOGDMABUF(("GetAsSourceSurface: CreateDataSourceSurface failed.")); + return nullptr; + } + + ScopedBindFramebuffer bind(context, autoFBForTex.FB()); + ReadPixelsIntoDataSurface(context, source); + + return source.forget(); +} + +#if 0 +void DMABufSurfaceYUV::ClearPlane(int aPlane) { + if (!MapInternal(0, 0, mWidth[aPlane], mHeight[aPlane], nullptr, + GBM_BO_TRANSFER_WRITE, aPlane)) { + return; + } + if ((int)mMappedRegionStride[aPlane] < mWidth[aPlane]) { + return; + } + memset((char*)mMappedRegion[aPlane], 0, + mMappedRegionStride[aPlane] * mHeight[aPlane]); + Unmap(aPlane); +} + +# include "gfxUtils.h" + +void DMABufSurfaceYUV::DumpToFile(const char* aFile) { + RefPtr<gfx::DataSourceSurface> surf = GetAsSourceSurface(); + gfxUtils::WriteAsPNG(surf, aFile); +} +#endif diff --git a/widget/gtk/DMABufSurface.h b/widget/gtk/DMABufSurface.h new file mode 100644 index 0000000000..d11530ced3 --- /dev/null +++ b/widget/gtk/DMABufSurface.h @@ -0,0 +1,376 @@ +/* -*- 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 + +#ifndef VA_FOURCC_NV12 +# define VA_FOURCC_NV12 0x3231564E +#endif +#ifndef VA_FOURCC_YV12 +# define VA_FOURCC_YV12 0x32315659 +#endif +#ifndef VA_FOURCC_P010 +# define VA_FOURCC_P010 0x30313050 +#endif + +namespace mozilla { +namespace gfx { +class DataSourceSurface; +} +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 int 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 already_AddRefed<mozilla::gfx::DataSourceSurface> + GetAsSourceSurface() { + return nullptr; + } + + virtual mozilla::gfx::YUVColorSpace GetYUVColorSpace() { + return mozilla::gfx::YUVColorSpace::Default; + }; + + bool IsFullRange() { return mColorRange == mozilla::gfx::ColorRange::FULL; }; + void SetColorRange(mozilla::gfx::ColorRange aColorRange) { + mColorRange = aColorRange; + }; + + 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(); + void GlobalRefCountDelete(); + + // 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; + + // Import global ref count object from IPC by file descriptor. + // This adds global ref count reference to the surface. + void GlobalRefCountImport(int aFd); + // Export global ref count object by file descriptor. + int GlobalRefCountExport(); + + void ReleaseDMABuf(); + + void* MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride, int aGbmFlags, int aPlane = 0); + + // We want to keep number of opened file descriptors low so open/close + // DMABuf file handles only when we need them, i.e. when DMABuf is exported + // to another process or to EGL. + virtual bool OpenFileDescriptorForPlane( + const mozilla::MutexAutoLock& aProofOfLock, int aPlane) = 0; + virtual void CloseFileDescriptorForPlane( + const mozilla::MutexAutoLock& aProofOfLock, int aPlane, + bool aForceClose = false) = 0; + bool OpenFileDescriptors(const mozilla::MutexAutoLock& aProofOfLock); + void CloseFileDescriptors(const mozilla::MutexAutoLock& aProofOfLock, + bool aForceClose = false); + + virtual ~DMABufSurface(); + + SurfaceType mSurfaceType; + uint64_t mBufferModifiers[DMABUF_BUFFER_PLANES]; + + int mBufferPlaneCount; + int mDmabufFds[DMABUF_BUFFER_PLANES]; + int32_t mDrmFormats[DMABUF_BUFFER_PLANES]; + int32_t mStrides[DMABUF_BUFFER_PLANES]; + int32_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; + mozilla::Mutex mSurfaceLock MOZ_UNANNOTATED; + + mozilla::gfx::ColorRange mColorRange = mozilla::gfx::ColorRange::LIMITED; +}; + +class DMABufSurfaceRGBA : public DMABufSurface { + public: + static already_AddRefed<DMABufSurfaceRGBA> CreateDMABufSurface( + int aWidth, int aHeight, int aDMABufSurfaceFlags); + + static already_AddRefed<DMABufSurface> CreateDMABufSurface( + mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, + int aWidth, int aHeight); + + 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; }; + + bool CreateWlBuffer(); + void ReleaseWlBuffer(); + wl_buffer* GetWlBuffer() { return mWlBuffer; }; + + int GetTextureCount() { return 1; }; + +#ifdef DEBUG + virtual void DumpToFile(const char* pFile); +#endif + + DMABufSurfaceRGBA(); + + private: + DMABufSurfaceRGBA(const DMABufSurfaceRGBA&) = delete; + DMABufSurfaceRGBA& operator=(const DMABufSurfaceRGBA&) = delete; + ~DMABufSurfaceRGBA(); + + bool Create(int aWidth, int aHeight, int aDMABufSurfaceFlags); + bool Create(const mozilla::layers::SurfaceDescriptor& aDesc); + bool Create(mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, + int aWidth, int aHeight); + + bool ImportSurfaceDescriptor(const mozilla::layers::SurfaceDescriptor& aDesc); + + bool OpenFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane); + void CloseFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane, bool aForceClose); + + private: + int mSurfaceFlags; + + int mWidth; + int mHeight; + mozilla::widget::GbmFormat* mGmbFormat; + + EGLImageKHR mEGLImage; + GLuint mTexture; + uint32_t mGbmBufferFlags; + wl_buffer* mWlBuffer; +}; + +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, int aWidth, int aHeight); + static already_AddRefed<DMABufSurfaceYUV> CopyYUVSurface( + const VADRMPRIMESurfaceDescriptor& aVaDesc, int aWidth, int aHeight); + static void ReleaseVADRMPRIMESurfaceDescriptor( + VADRMPRIMESurfaceDescriptor& aDesc); + + bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor); + + DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() { return this; }; + already_AddRefed<mozilla::gfx::DataSourceSurface> GetAsSourceSurface(); + + 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]; }; + + int GetTextureCount(); + + void SetYUVColorSpace(mozilla::gfx::YUVColorSpace aColorSpace) { + mColorSpace = aColorSpace; + } + mozilla::gfx::YUVColorSpace GetYUVColorSpace() { return mColorSpace; } + + DMABufSurfaceYUV(); + + bool UpdateYUVData(void** aPixelData, int* aLineSizes); + bool UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, + int aHeight, bool aCopy); + bool VerifyTextureCreation(); + + private: + DMABufSurfaceYUV(const DMABufSurfaceYUV&) = delete; + DMABufSurfaceYUV& operator=(const DMABufSurfaceYUV&) = delete; + ~DMABufSurfaceYUV(); + + bool Create(const mozilla::layers::SurfaceDescriptor& aDesc); + bool Create(int aWidth, int aHeight, void** aPixelData, int* aLineSizes); + bool CreateYUVPlane(int aPlane); + bool CreateLinearYUVPlane(int aPlane, int aWidth, int aHeight, + int aDrmFormat); + void UpdateYUVPlane(int aPlane, void* aPixelData, int aLineSize); + + bool MoveYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, + int aHeight); + bool CopyYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, + int aHeight); + + bool ImportPRIMESurfaceDescriptor(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight); + bool ImportSurfaceDescriptor( + const mozilla::layers::SurfaceDescriptorDMABuf& aDesc); + + bool OpenFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane); + void CloseFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane, bool aForceClose); + + bool CreateEGLImage(mozilla::gl::GLContext* aGLContext, int aPlane); + void ReleaseEGLImages(mozilla::gl::GLContext* aGLContext); + + int mWidth[DMABUF_BUFFER_PLANES]; + int mHeight[DMABUF_BUFFER_PLANES]; + // Aligned size of the surface imported from VADRMPRIMESurfaceDescriptor. + // It's used only internally to create EGLImage as some GL drivers + // needs that (Bug 1724385). + int mWidthAligned[DMABUF_BUFFER_PLANES]; + int mHeightAligned[DMABUF_BUFFER_PLANES]; + EGLImageKHR mEGLImage[DMABUF_BUFFER_PLANES]; + GLuint mTexture[DMABUF_BUFFER_PLANES]; + mozilla::gfx::YUVColorSpace mColorSpace = + mozilla::gfx::YUVColorSpace::Default; +}; + +#endif diff --git a/widget/gtk/GRefPtr.h b/widget/gtk/GRefPtr.h new file mode 100644 index 0000000000..ca3838758c --- /dev/null +++ b/widget/gtk/GRefPtr.h @@ -0,0 +1,89 @@ +/* -*- 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 <gio/gio.h> +#include <gtk/gtk.h> +#include "mozilla/RefPtr.h" + +#ifdef MOZ_ENABLE_DBUS +// TODO: Remove this (we should use GDBus instead, which is not deprecated). +# include <dbus/dbus-glib.h> +#endif + +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); } +}; + +#define GOBJECT_TRAITS(type_) \ + template <> \ + struct RefPtrTraits<type_> : public GObjectRefPtrTraits<type_> {}; + +GOBJECT_TRAITS(GtkWidget) +GOBJECT_TRAITS(GFile) +GOBJECT_TRAITS(GMenu) +GOBJECT_TRAITS(GMenuItem) +GOBJECT_TRAITS(GSimpleAction) +GOBJECT_TRAITS(GSimpleActionGroup) +GOBJECT_TRAITS(GDBusProxy) +GOBJECT_TRAITS(GAppInfo) +GOBJECT_TRAITS(GAppLaunchContext) +GOBJECT_TRAITS(GdkDragContext) +GOBJECT_TRAITS(GDBusMessage) +GOBJECT_TRAITS(GdkPixbuf) +GOBJECT_TRAITS(GCancellable) +GOBJECT_TRAITS(GtkIMContext) +GOBJECT_TRAITS(GUnixFDList) + +#ifdef MOZ_ENABLE_DBUS +GOBJECT_TRAITS(DBusGProxy) +#endif + +#undef GOBJECT_TRAITS + +template <> +struct RefPtrTraits<GVariant> { + static void AddRef(GVariant* aVariant) { g_variant_ref(aVariant); } + static void Release(GVariant* aVariant) { g_variant_unref(aVariant); } +}; + +template <> +struct RefPtrTraits<GHashTable> { + static void AddRef(GHashTable* aObject) { g_hash_table_ref(aObject); } + static void Release(GHashTable* aObject) { g_hash_table_unref(aObject); } +}; + +template <> +struct RefPtrTraits<GDBusNodeInfo> { + static void AddRef(GDBusNodeInfo* aObject) { g_dbus_node_info_ref(aObject); } + static void Release(GDBusNodeInfo* aObject) { + g_dbus_node_info_unref(aObject); + } +}; + +#ifdef MOZ_ENABLE_DBUS +template <> +struct RefPtrTraits<DBusGConnection> { + static void AddRef(DBusGConnection* aObject) { + dbus_g_connection_ref(aObject); + } + static void Release(DBusGConnection* aObject) { + dbus_g_connection_unref(aObject); + } +}; +#endif + +} // namespace mozilla + +#endif diff --git a/widget/gtk/GUniquePtr.h b/widget/gtk/GUniquePtr.h new file mode 100644 index 0000000000..0cd5de7b60 --- /dev/null +++ b/widget/gtk/GUniquePtr.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GUniquePtr_h_ +#define GUniquePtr_h_ + +// Provides GUniquePtr to g_free a given pointer. + +#include <glib.h> +#include <gtk/gtk.h> +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +struct GFreeDeleter { + constexpr GFreeDeleter() = default; + void operator()(GdkEventCrossing* aPtr) const { + gdk_event_free((GdkEvent*)aPtr); + } + void operator()(GdkEventKey* aPtr) const { gdk_event_free((GdkEvent*)aPtr); } + void operator()(GdkEvent* aPtr) const { gdk_event_free(aPtr); } + void operator()(GError* aPtr) const { g_error_free(aPtr); } + void operator()(void* aPtr) const { g_free(aPtr); } + void operator()(GtkPaperSize* aPtr) const { gtk_paper_size_free(aPtr); } +}; + +template <typename T> +using GUniquePtr = UniquePtr<T, GFreeDeleter>; + +} // namespace mozilla + +#endif diff --git a/widget/gtk/GfxInfo.cpp b/widget/gtk/GfxInfo.cpp new file mode 100644 index 0000000000..7382b066d1 --- /dev/null +++ b/widget/gtk/GfxInfo.cpp @@ -0,0 +1,1403 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=8 et : + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GfxInfo.h" + +#include <cctype> +#include <errno.h> +#include <unistd.h> +#include <string> +#include <poll.h> +#include <sys/types.h> +#include <sys/utsname.h> +#include <sys/wait.h> +#include <glib.h> +#include <fcntl.h> + +#include "mozilla/gfx/Logging.h" +#include "mozilla/SSE.h" +#include "mozilla/Telemetry.h" +#include "mozilla/XREAppData.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsCRTGlue.h" +#include "nsExceptionHandler.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsUnicharUtils.h" +#include "nsWhitespaceTokenizer.h" +#include "prenv.h" +#include "WidgetUtilsGtk.h" +#include "MediaCodecsSupport.h" +#include "nsAppRunner.h" + +// How long we wait for data from glxtest/vaapi test process in milliseconds. +#define GFX_TEST_TIMEOUT 4000 +#define VAAPI_TEST_TIMEOUT 2000 + +#define GLX_PROBE_BINARY u"glxtest"_ns +#define VAAPI_PROBE_BINARY u"vaapitest"_ns + +namespace mozilla::widget { + +#ifdef DEBUG +NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug) +#endif + +int GfxInfo::sGLXTestPipe = -1; +pid_t GfxInfo::sGLXTestPID = 0; + +// bits to use decoding codec information returned from glxtest +constexpr int CODEC_HW_H264 = 1 << 4; +constexpr int CODEC_HW_VP8 = 1 << 5; +constexpr int CODEC_HW_VP9 = 1 << 6; +constexpr int CODEC_HW_AV1 = 1 << 7; + +nsresult GfxInfo::Init() { + mGLMajorVersion = 0; + mGLMinorVersion = 0; + mHasTextureFromPixmap = false; + mIsMesa = false; + mIsAccelerated = true; + mIsWayland = false; + mIsXWayland = false; + mHasMultipleGPUs = false; + mGlxTestError = false; + return GfxInfoBase::Init(); +} + +void GfxInfo::AddCrashReportAnnotations() { + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID, + mVendorId); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID, + mDeviceId); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AdapterDriverVendor, mDriverVendor); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AdapterDriverVersion, mDriverVersion); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::IsWayland, + mIsWayland); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::DesktopEnvironment, + GetDesktopEnvironmentIdentifier()); + + if (mHasMultipleGPUs) { + nsAutoCString note; + note.AppendLiteral("Has dual GPUs."); + if (!mSecondaryVendorId.IsEmpty()) { + note.AppendLiteral(" GPU #2: AdapterVendorID2: "); + note.Append(mSecondaryVendorId); + note.AppendLiteral(", AdapterDeviceID2: "); + note.Append(mSecondaryDeviceId); + } + CrashReporter::AppendAppNotesToCrashReport(note); + } +} + +static bool MakeFdNonBlocking(int fd) { + return fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) != -1; +} + +static bool ManageChildProcess(const char* aProcessName, int* aPID, int* aPipe, + int aTimeout, char** aData) { + // Don't try anything if we failed before + if (*aPID < 0) { + return false; + } + + GIOChannel* channel = nullptr; + *aData = nullptr; + + auto free = mozilla::MakeScopeExit([&] { + if (channel) { + g_io_channel_unref(channel); + } + if (*aPipe >= 0) { + close(*aPipe); + *aPipe = -1; + } + }); + + const TimeStamp deadline = + TimeStamp::Now() + TimeDuration::FromMilliseconds(aTimeout); + + struct pollfd pfd {}; + pfd.fd = *aPipe; + pfd.events = POLLIN; + + while (poll(&pfd, 1, aTimeout) != 1) { + if (errno != EAGAIN && errno != EINTR) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): poll failed: " << strerror(errno) << "\n"; + return false; + } + if (TimeStamp::Now() > deadline) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): process hangs\n"; + return false; + } + } + + channel = g_io_channel_unix_new(*aPipe); + MakeFdNonBlocking(*aPipe); + + GUniquePtr<GError> error; + gsize length = 0; + int ret; + do { + error = nullptr; + ret = g_io_channel_read_to_end(channel, aData, &length, + getter_Transfers(error)); + } while (ret == G_IO_STATUS_AGAIN && TimeStamp::Now() < deadline); + if (error || ret != G_IO_STATUS_NORMAL) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): failed to read data from child process: "; + if (error) { + gfxCriticalNote << error->message; + } else { + gfxCriticalNote << "timeout"; + } + return false; + } + + int status = 0; + int pid = *aPID; + *aPID = -1; + + while (true) { + int ret = waitpid(pid, &status, WNOHANG); + if (ret > 0) { + break; + } + if (ret < 0) { + if (errno == ECHILD) { + // Bug 718629 + // ECHILD happens when the glxtest process got reaped got reaped after a + // PR_CreateProcess as per bug 227246. This shouldn't matter, as we + // still seem to get the data from the pipe, and if we didn't, the + // outcome would be to blocklist anyway. + return true; + } + if (errno != EAGAIN && errno != EINTR) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): waitpid failed: " << strerror(errno) << "\n"; + return false; + } + } + if (TimeStamp::Now() > deadline) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): process hangs\n"; + return false; + } + // Wait 50ms to another waitpid() check. + usleep(50000); + } + + return WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS; +} + +// to understand this function, see bug 639842. We retrieve the OpenGL driver +// information in a separate process to protect against bad drivers. +void GfxInfo::GetData() { + if (mInitialized) { + return; + } + mInitialized = true; + + // In some cases (xpcshell test, Profile manager etc.) + // FireGLXTestProcess() is not fired in advance + // so we call it here. + GfxInfo::FireGLXTestProcess(); + + GfxInfoBase::GetData(); + + char* glxData = nullptr; + auto free = mozilla::MakeScopeExit([&] { g_free((void*)glxData); }); + + bool error = !ManageChildProcess("glxtest", &sGLXTestPID, &sGLXTestPipe, + GFX_TEST_TIMEOUT, &glxData); + if (error) { + gfxCriticalNote << "glxtest: ManageChildProcess failed\n"; + } + + nsCString glVendor; + nsCString glRenderer; + nsCString glVersion; + nsCString textureFromPixmap; + nsCString testType; + + // Available if GLX_MESA_query_renderer is supported. + nsCString mesaVendor; + nsCString mesaDevice; + nsCString mesaAccelerated; + // Available if using a DRI-based libGL stack. + nsCString driDriver; + nsCString adapterRam; + + nsCString drmRenderDevice; + + nsCString ddxDriver; + + AutoTArray<nsCString, 2> pciVendors; + AutoTArray<nsCString, 2> pciDevices; + + nsCString* stringToFill = nullptr; + bool logString = false; + bool errorLog = false; + + char* bufptr = glxData; + + while (true) { + char* line = NS_strtok("\n", &bufptr); + if (!line) break; + if (stringToFill) { + stringToFill->Assign(line); + stringToFill = nullptr; + } else if (logString) { + gfxCriticalNote << "glxtest: " << line; + logString = false; + } else if (!strcmp(line, "VENDOR")) { + stringToFill = &glVendor; + } else if (!strcmp(line, "RENDERER")) { + stringToFill = &glRenderer; + } else if (!strcmp(line, "VERSION")) { + stringToFill = &glVersion; + } else if (!strcmp(line, "TFP")) { + stringToFill = &textureFromPixmap; + } else if (!strcmp(line, "MESA_VENDOR_ID")) { + stringToFill = &mesaVendor; + } else if (!strcmp(line, "MESA_DEVICE_ID")) { + stringToFill = &mesaDevice; + } else if (!strcmp(line, "MESA_ACCELERATED")) { + stringToFill = &mesaAccelerated; + } else if (!strcmp(line, "MESA_VRAM")) { + stringToFill = &adapterRam; + } else if (!strcmp(line, "DDX_DRIVER")) { + stringToFill = &ddxDriver; + } else if (!strcmp(line, "DRI_DRIVER")) { + stringToFill = &driDriver; + } else if (!strcmp(line, "PCI_VENDOR_ID")) { + stringToFill = pciVendors.AppendElement(); + } else if (!strcmp(line, "PCI_DEVICE_ID")) { + stringToFill = pciDevices.AppendElement(); + } else if (!strcmp(line, "DRM_RENDERDEVICE")) { + stringToFill = &drmRenderDevice; + } else if (!strcmp(line, "TEST_TYPE")) { + stringToFill = &testType; + } else if (!strcmp(line, "WARNING")) { + logString = true; + } else if (!strcmp(line, "ERROR")) { + logString = true; + errorLog = true; + } + } + + MOZ_ASSERT(pciDevices.Length() == pciVendors.Length(), + "Missing PCI vendors/devices"); + + size_t pciLen = std::min(pciVendors.Length(), pciDevices.Length()); + mHasMultipleGPUs = pciLen > 1; + + if (!strcmp(textureFromPixmap.get(), "TRUE")) mHasTextureFromPixmap = true; + + // only useful for Linux kernel version check for FGLRX driver. + // assumes X client == X server, which is sad. + struct utsname unameobj {}; + if (uname(&unameobj) >= 0) { + mOS.Assign(unameobj.sysname); + mOSRelease.Assign(unameobj.release); + } + + const char* spoofedVendor = PR_GetEnv("MOZ_GFX_SPOOF_GL_VENDOR"); + if (spoofedVendor) glVendor.Assign(spoofedVendor); + const char* spoofedRenderer = PR_GetEnv("MOZ_GFX_SPOOF_GL_RENDERER"); + if (spoofedRenderer) glRenderer.Assign(spoofedRenderer); + const char* spoofedVersion = PR_GetEnv("MOZ_GFX_SPOOF_GL_VERSION"); + if (spoofedVersion) glVersion.Assign(spoofedVersion); + const char* spoofedOS = PR_GetEnv("MOZ_GFX_SPOOF_OS"); + if (spoofedOS) mOS.Assign(spoofedOS); + const char* spoofedOSRelease = PR_GetEnv("MOZ_GFX_SPOOF_OS_RELEASE"); + if (spoofedOSRelease) mOSRelease.Assign(spoofedOSRelease); + + // Scan the GL_VERSION string for the GL and driver versions. + nsCWhitespaceTokenizer tokenizer(glVersion); + while (tokenizer.hasMoreTokens()) { + nsCString token(tokenizer.nextToken()); + unsigned int major = 0, minor = 0, revision = 0, patch = 0; + if (sscanf(token.get(), "%u.%u.%u.%u", &major, &minor, &revision, &patch) >= + 2) { + // A survey of GL_VENDOR strings indicates that the first version is + // always the GL version, the second is usually the driver version. + if (mGLMajorVersion == 0) { + mGLMajorVersion = major; + mGLMinorVersion = minor; + } else if (mDriverVersion.IsEmpty()) { // Not already spoofed. + mDriverVersion = + nsPrintfCString("%u.%u.%u.%u", major, minor, revision, patch); + } + } + } + + if (mGLMajorVersion == 0) { + NS_WARNING("Failed to parse GL version!"); + } + + mDrmRenderDevice = std::move(drmRenderDevice); + mTestType = std::move(testType); + + // Mesa always exposes itself in the GL_VERSION string, but not always the + // GL_VENDOR string. + mIsMesa = glVersion.Find("Mesa") != -1; + + // We need to use custom driver vendor IDs for mesa so we can treat them + // differently than the proprietary drivers. + if (mIsMesa) { + mIsAccelerated = !mesaAccelerated.Equals("FALSE"); + // Process software rasterizers before the DRI driver string; we may be + // forcing software rasterization on a DRI-accelerated X server by using + // LIBGL_ALWAYS_SOFTWARE or a similar restriction. + if (strcasestr(glRenderer.get(), "llvmpipe")) { + CopyUTF16toUTF8( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaLLVMPipe), + mDriverVendor); + mIsAccelerated = false; + } else if (strcasestr(glRenderer.get(), "softpipe")) { + CopyUTF16toUTF8( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSoftPipe), + mDriverVendor); + mIsAccelerated = false; + } else if (strcasestr(glRenderer.get(), "software rasterizer")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSWRast), + mDriverVendor); + mIsAccelerated = false; + } else if (strcasestr(driDriver.get(), "vmwgfx")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaVM), + mDriverVendor); + mIsAccelerated = false; + } else if (!mIsAccelerated) { + CopyUTF16toUTF8( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSWUnknown), + mDriverVendor); + } else if (!driDriver.IsEmpty()) { + mDriverVendor = nsPrintfCString("mesa/%s", driDriver.get()); + } else { + // Some other mesa configuration where we couldn't get enough info. + NS_WARNING("Failed to detect Mesa driver being used!"); + CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaUnknown), + mDriverVendor); + } + + if (!mesaVendor.IsEmpty()) { + mVendorId = mesaVendor; + } + + if (!mesaDevice.IsEmpty()) { + mDeviceId = mesaDevice; + } + + if (!mIsAccelerated && mVendorId.IsEmpty()) { + mVendorId.Assign(glVendor.get()); + } + + if (!mIsAccelerated && mDeviceId.IsEmpty()) { + mDeviceId.Assign(glRenderer.get()); + } + } else if (glVendor.EqualsLiteral("NVIDIA Corporation")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA), + mVendorId); + mDriverVendor.AssignLiteral("nvidia/unknown"); + // TODO: Use NV-CONTROL X11 extension to query Device ID and VRAM. + } else if (glVendor.EqualsLiteral("ATI Technologies Inc.")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI), + mVendorId); + mDriverVendor.AssignLiteral("ati/unknown"); + // TODO: Look into ways to find the device ID on FGLRX. + } else { + NS_WARNING("Failed to detect GL vendor!"); + } + + if (!adapterRam.IsEmpty()) { + mAdapterRAM = (uint32_t)atoi(adapterRam.get()); + } + + // If we have the DRI driver, we can derive the vendor ID from that if needed. + if (mVendorId.IsEmpty() && !driDriver.IsEmpty()) { + const char* nvidiaDrivers[] = {"nouveau", "tegra", nullptr}; + for (size_t i = 0; nvidiaDrivers[i]; ++i) { + if (driDriver.Equals(nvidiaDrivers[i])) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA), + mVendorId); + break; + } + } + + if (mVendorId.IsEmpty()) { + const char* intelDrivers[] = {"iris", "crocus", "i915", "i965", + "i810", "intel", nullptr}; + for (size_t i = 0; intelDrivers[i]; ++i) { + if (driDriver.Equals(intelDrivers[i])) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel), + mVendorId); + break; + } + } + } + + if (mVendorId.IsEmpty()) { + const char* amdDrivers[] = {"r600", "r200", "r100", + "radeon", "radeonsi", nullptr}; + for (size_t i = 0; amdDrivers[i]; ++i) { + if (driDriver.Equals(amdDrivers[i])) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI), + mVendorId); + break; + } + } + } + + if (mVendorId.IsEmpty()) { + if (driDriver.EqualsLiteral("freedreno")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::Qualcomm), + mVendorId); + } + } + } + + // If we still don't have a vendor ID, we can try the PCI vendor list. + if (mVendorId.IsEmpty()) { + if (pciVendors.IsEmpty()) { + gfxCriticalNote << "No GPUs detected via PCI\n"; + } else { + for (size_t i = 0; i < pciVendors.Length(); ++i) { + if (mVendorId.IsEmpty()) { + mVendorId = pciVendors[i]; + } else if (mVendorId != pciVendors[i]) { + gfxCriticalNote << "More than 1 GPU vendor detected via PCI, cannot " + "deduce vendor\n"; + mVendorId.Truncate(); + break; + } + } + } + } + + // If we know the vendor ID, but didn't get a device ID, we can guess from the + // PCI device list. + if (mDeviceId.IsEmpty() && !mVendorId.IsEmpty()) { + for (size_t i = 0; i < pciLen; ++i) { + if (mVendorId.Equals(pciVendors[i])) { + if (mDeviceId.IsEmpty()) { + mDeviceId = pciDevices[i]; + } else if (mDeviceId != pciDevices[i]) { + gfxCriticalNote << "More than 1 GPU from same vendor detected via " + "PCI, cannot deduce device\n"; + mDeviceId.Truncate(); + break; + } + } + } + } + + // Assuming we know the vendor, we should check for a secondary card. + if (!mVendorId.IsEmpty()) { + if (pciLen > 2) { + gfxCriticalNote + << "More than 2 GPUs detected via PCI, secondary GPU is arbitrary\n"; + } + for (size_t i = 0; i < pciLen; ++i) { + if (!mVendorId.Equals(pciVendors[i]) || + (!mDeviceId.IsEmpty() && !mDeviceId.Equals(pciDevices[i]))) { + mSecondaryVendorId = pciVendors[i]; + mSecondaryDeviceId = pciDevices[i]; + break; + } + } + } + + // If we couldn't choose, log them. + if (mVendorId.IsEmpty()) { + for (size_t i = 0; i < pciLen; ++i) { + gfxCriticalNote << "PCI candidate " << pciVendors[i].get() << "/" + << pciDevices[i].get() << "\n"; + } + } + + // Fallback to GL_VENDOR and GL_RENDERER. + if (mVendorId.IsEmpty()) { + mVendorId.Assign(glVendor.get()); + } + if (mDeviceId.IsEmpty()) { + mDeviceId.Assign(glRenderer.get()); + } + + mAdapterDescription.Assign(glRenderer); + + // Make a best effort guess at whether or not we are using the XWayland compat + // layer. For all intents and purposes, we should otherwise believe we are + // using X11. + mIsWayland = GdkIsWaylandDisplay(); + mIsXWayland = IsXWaylandProtocol(); + + if (!ddxDriver.IsEmpty()) { + PRInt32 start = 0; + PRInt32 loc = ddxDriver.Find(";", start); + while (loc != kNotFound) { + nsCString line(ddxDriver.get() + start, loc - start); + mDdxDrivers.AppendElement(std::move(line)); + + start = loc + 1; + loc = ddxDriver.Find(";", start); + } + } + + if (error || errorLog || mTestType.IsEmpty()) { + if (!mAdapterDescription.IsEmpty()) { + mAdapterDescription.AppendLiteral(" (See failure log)"); + } else { + mAdapterDescription.AppendLiteral("See failure log"); + } + + mGlxTestError = true; + } + + AddCrashReportAnnotations(); +} + +int GfxInfo::FireTestProcess(const nsAString& aBinaryFile, int* aOutPipe, + const char** aStringArgs) { + nsCOMPtr<nsIFile> appFile; + nsresult rv = XRE_GetBinaryPath(getter_AddRefs(appFile)); + if (NS_FAILED(rv)) { + gfxCriticalNote << "Couldn't find application file.\n"; + return false; + } + nsCOMPtr<nsIFile> exePath; + rv = appFile->GetParent(getter_AddRefs(exePath)); + if (NS_FAILED(rv)) { + gfxCriticalNote << "Couldn't get application directory.\n"; + return false; + } + exePath->Append(aBinaryFile); + +#define MAX_ARGS 8 + char* argv[MAX_ARGS + 2]; + + argv[0] = strdup(exePath->NativePath().get()); + for (int i = 0; i < MAX_ARGS; i++) { + if (aStringArgs[i]) { + argv[i + 1] = strdup(aStringArgs[i]); + } else { + argv[i + 1] = nullptr; + break; + } + } + + // Use G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD flags + // to g_spawn_async_with_pipes() run posix_spawn() directly. + int pid; + GUniquePtr<GError> err; + g_spawn_async_with_pipes( + nullptr, argv, nullptr, + GSpawnFlags(G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD), + nullptr, nullptr, &pid, nullptr, aOutPipe, nullptr, + getter_Transfers(err)); + if (err) { + gfxCriticalNote << "FireTestProcess failed: " << err->message << "\n"; + pid = 0; + } + for (auto& arg : argv) { + if (!arg) { + break; + } + free(arg); + } + return pid; +} + +bool GfxInfo::FireGLXTestProcess() { + if (sGLXTestPID != 0) { + return true; + } + + int pfd[2]; + if (pipe(pfd) == -1) { + gfxCriticalNote << "FireGLXTestProcess failed to create pipe\n"; + return false; + } + sGLXTestPipe = pfd[0]; + + auto pipeID = std::to_string(pfd[1]); + const char* args[] = {"-f", pipeID.c_str(), + IsWaylandEnabled() ? "-w" : nullptr, nullptr}; + sGLXTestPID = FireTestProcess(GLX_PROBE_BINARY, nullptr, args); + // Set pid to -1 to avoid further test launch. + if (!sGLXTestPID) { + sGLXTestPID = -1; + } + close(pfd[1]); + return true; +} + +void GfxInfo::GetDataVAAPI() { + if (mIsVAAPISupported.isSome()) { + return; + } + mIsVAAPISupported = Some(false); + +#ifdef MOZ_ENABLE_VAAPI + char* vaapiData = nullptr; + auto free = mozilla::MakeScopeExit([&] { g_free((void*)vaapiData); }); + + int vaapiPipe = -1; + int vaapiPID = 0; + const char* args[] = {"-d", mDrmRenderDevice.get(), nullptr}; + vaapiPID = FireTestProcess(VAAPI_PROBE_BINARY, &vaapiPipe, args); + if (!vaapiPID) { + return; + } + + if (!ManageChildProcess("vaapitest", &vaapiPID, &vaapiPipe, + VAAPI_TEST_TIMEOUT, &vaapiData)) { + gfxCriticalNote << "vaapitest: ManageChildProcess failed\n"; + return; + } + + char* bufptr = vaapiData; + char* line; + while ((line = NS_strtok("\n", &bufptr))) { + if (!strcmp(line, "VAAPI_SUPPORTED")) { + line = NS_strtok("\n", &bufptr); + if (!line) { + gfxCriticalNote << "vaapitest: Failed to get VAAPI support\n"; + return; + } + mIsVAAPISupported = Some(!strcmp(line, "TRUE")); + } else if (!strcmp(line, "VAAPI_HWCODECS")) { + line = NS_strtok("\n", &bufptr); + if (!line) { + gfxCriticalNote << "vaapitest: Failed to get VAAPI codecs\n"; + return; + } + + std::istringstream(line) >> mVAAPISupportedCodecs; + if (mVAAPISupportedCodecs & CODEC_HW_H264) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::H264HardwareDecode); + } + if (mVAAPISupportedCodecs & CODEC_HW_VP8) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::VP8HardwareDecode); + } + if (mVAAPISupportedCodecs & CODEC_HW_VP9) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::VP9HardwareDecode); + } + if (mVAAPISupportedCodecs & CODEC_HW_AV1) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::AV1HardwareDecode); + } + } else if (!strcmp(line, "WARNING") || !strcmp(line, "ERROR")) { + gfxCriticalNote << "vaapitest: " << line; + line = NS_strtok("\n", &bufptr); + if (line) { + gfxCriticalNote << "vaapitest: " << line << "\n"; + } + return; + } + } +#endif +} + +const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() { + if (!sDriverInfo->Length()) { + // Mesa 10.0 provides the GLX_MESA_query_renderer extension, which allows us + // to query device IDs backing a GL context for blocklisting. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(10, 0, 0, 0), "FEATURE_FAILURE_OLD_MESA", + "Mesa 10.0"); + + // NVIDIA Mesa baseline (see bug 1714391). + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaNouveau, DeviceFamily::All, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(11, 0, 0, 0), "FEATURE_FAILURE_OLD_NV_MESA", + "Mesa 11.0"); + + // NVIDIA baseline (ported from old blocklist) + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(257, 21, 0, 0), "FEATURE_FAILURE_OLD_NVIDIA", + "NVIDIA 257.21"); + + // fglrx baseline (chosen arbitrarily as 2013-07-22 release). + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(13, 15, 100, 1), "FEATURE_FAILURE_OLD_FGLRX", + "fglrx 13.15.100.1"); + + //////////////////////////////////// + // FEATURE_WEBRENDER + + // All Mesa software drivers, they should get Software WebRender instead. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::SoftwareMesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_SOFTWARE_GL", + ""); + + // Older generation Intel devices do not perform well with WebRender. + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::IntelWebRenderBlocked, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "INTEL_DEVICE_GEN5_OR_OLDER", + ""); + + // Nvidia Mesa baseline, see bug 1563859. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(18, 2, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA", "Mesa 18.2.0.0"); + + // Disable on all older Nvidia drivers due to stability issues. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, V(470, 82, 0, 0), + "FEATURE_FAILURE_WEBRENDER_OLD_NVIDIA", "470.82.0"); + + // Older generation NVIDIA devices do not perform well with WebRender. + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::NvidiaWebRenderBlocked, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "NVIDIA_EARLY_TESLA_AND_C67_C68", ""); + + // Mesa baseline, chosen arbitrarily. Linux users are generally good about + // updating their Mesa libraries so we don't want to arbitarily support + // WebRender on old drivers with outstanding bugs to work around. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(17, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA", "Mesa 17.0.0.0"); + + // Mesa baseline for non-Intel/NVIDIA/ATI devices. These other devices will + // often have less mature drivers so let's block older Mesa versions. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaNonIntelNvidiaAtiAll, + DeviceFamily::All, nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(22, 2, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA_OTHER", + "Mesa 22.2.0.0"); + + // Bug 1690568 / Bug 1393793 - Require Mesa 17.3.0+ for devices using the + // AMD r600 driver to avoid shader compilation issues. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaR600, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(17, 3, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA_R600", + "Mesa 17.3.0.0"); + + // Disable on all ATI devices not using Mesa for now. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_WEBRENDER_NO_LINUX_ATI", ""); + + // Disable R600 GPUs with Mesa drivers. + // Bug 1673939 - Garbled text on RS880 GPUs with Mesa drivers. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AmdR600, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_WEBRENDER_BUG_1673939", + "https://gitlab.freedesktop.org/mesa/mesa/-/issues/3720"); + + // Bug 1635186 - Poor performance with video playing in a background window + // on XWayland. Keep in sync with FEATURE_X11_EGL below to only enable them + // together by default. Only Mesa and Nvidia binary drivers are expected + // on Wayland rigth now. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::XWayland, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(21, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_BUG_1635186", + "Mesa 21.0.0.0"); + + // Bug 1815481 - Disable mesa drivers in virtual machines. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaVM, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_WEBRENDER_MESA_VM", ""); + // Disable hardware mesa drivers in virtual machines due to instability. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaVM, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBGL_USE_HARDWARE, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_WEBGL_MESA_VM", ""); + + //////////////////////////////////// + // FEATURE_WEBRENDER_COMPOSITOR + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_COMPOSITOR_DISABLED", ""); + + //////////////////////////////////// + // FEATURE_X11_EGL + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(17, 0, 0, 0), "FEATURE_X11_EGL_OLD_MESA", + "Mesa 17.0.0.0"); + + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(18, 2, 0, 0), "FEATURE_X11_EGL_OLD_MESA_NOUVEAU", + "Mesa 18.2.0.0"); + + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(470, 82, 0, 0), + "FEATURE_ROLLOUT_X11_EGL_NVIDIA_BINARY", "470.82.0"); + + // Disable on all AMD devices not using Mesa. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_X11_EGL_NO_LINUX_ATI", ""); + + //////////////////////////////////// + // FEATURE_DMABUF + // Disabled due to high volume crash tracked in bug 1788573. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_DMABUF, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1788573", + ""); + + //////////////////////////////////// + // FEATURE_DMABUF_SURFACE_EXPORT + // Disabled due to: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6666 + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6796 + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", ""); + + // Disabled due to: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6688 + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", ""); + + // Disabled due to: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6988 + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::QualcommAll, + nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", ""); + + //////////////////////////////////// + // FEATURE_HARDWARE_VIDEO_DECODING + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(21, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_MESA", + "Mesa 21.0.0.0"); + + // Disable on all NVIDIA hardware + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::All, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_LINUX_NVIDIA", ""); + + // Disable on all AMD devices not using Mesa. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_LINUX_AMD", ""); + + // Disable on r600 driver due to decoding artifacts (Bug 1824307) + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaR600, DeviceFamily::All, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_R600", ""); + + // Disable on Release/late Beta on AMD +#if !defined(EARLY_BETA_OR_EARLIER) + APPEND_TO_DRIVER_BLOCKLIST(OperatingSystem::Linux, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_HARDWARE_VIDEO_DECODING_DISABLE", ""); +#endif + //////////////////////////////////// + // FEATURE_HW_DECODED_VIDEO_ZERO_COPY - ALLOWLIST + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Linux, DeviceFamily::All, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_ALLOW_ALWAYS, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_ROLLOUT_ALL"); + + // Disable on all AMD devices using Mesa (Bug 1802844). + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_ZERO_COPY_LINUX_AMD_DISABLE", + ""); + + //////////////////////////////////// + // FEATURE_WEBRENDER_PARTIAL_PRESENT + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::X11, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER_PARTIAL_PRESENT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_ROLLOUT_WR_PARTIAL_PRESENT_NVIDIA_BINARY", ""); + + //////////////////////////////////// + + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaNouveau, DeviceFamily::All, + nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_THREADSAFE_GL_NOUVEAU", ""); + + // Disabled due to high volume crash tracked in bug 1788573. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1788573", + ""); + } + return *sDriverInfo; +} + +bool GfxInfo::DoesWindowProtocolMatch(const nsAString& aBlocklistWindowProtocol, + const nsAString& aWindowProtocol) { + if (mIsWayland && + aBlocklistWindowProtocol.Equals( + GfxDriverInfo::GetWindowProtocol(WindowProtocol::WaylandAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (!mIsWayland && + aBlocklistWindowProtocol.Equals( + GfxDriverInfo::GetWindowProtocol(WindowProtocol::X11All), + nsCaseInsensitiveStringComparator)) { + return true; + } + return GfxInfoBase::DoesWindowProtocolMatch(aBlocklistWindowProtocol, + aWindowProtocol); +} + +bool GfxInfo::DoesDriverVendorMatch(const nsAString& aBlocklistVendor, + const nsAString& aDriverVendor) { + if (mIsMesa) { + if (aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (mIsAccelerated && + aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::HardwareMesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (!mIsAccelerated && + aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::SoftwareMesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (aBlocklistVendor.Equals(GfxDriverInfo::GetDriverVendor( + DriverVendor::MesaNonIntelNvidiaAtiAll), + nsCaseInsensitiveStringComparator)) { + return !mVendorId.Equals("0x8086") && !mVendorId.Equals("0x10de") && + !mVendorId.Equals("0x1002"); + } + } + if (!mIsMesa && aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::NonMesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + return GfxInfoBase::DoesDriverVendorMatch(aBlocklistVendor, aDriverVendor); +} + +nsresult GfxInfo::GetFeatureStatusImpl( + int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion, + const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId, + OperatingSystem* aOS /* = nullptr */) + +{ + NS_ENSURE_ARG_POINTER(aStatus); + *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN; + aSuggestedDriverVersion.SetIsVoid(true); + OperatingSystem os = OperatingSystem::Linux; + if (aOS) *aOS = os; + + if (sShutdownOccurred) { + return NS_OK; + } + + GetData(); + + if (aFeature == nsIGfxInfo::FEATURE_BACKDROP_FILTER) { + *aStatus = nsIGfxInfo::FEATURE_STATUS_OK; + return NS_OK; + } + + if (mGlxTestError) { + // If glxtest failed, block all features by default. + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_GLXTEST_FAILED"; + return NS_OK; + } + + if (mGLMajorVersion == 1) { + // We're on OpenGL 1. In most cases that indicates really old hardware. + // We better block them, rather than rely on them to fail gracefully, + // because they don't! see bug 696636 + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_OPENGL_1"; + return NS_OK; + } + + // Blocklist software GL implementations from using layers acceleration. + // On the test infrastructure, we'll force-enable layers acceleration. + if (aFeature == nsIGfxInfo::FEATURE_OPENGL_LAYERS && !mIsAccelerated && + !PR_GetEnv("MOZ_LAYERS_ALLOW_SOFTWARE_GL")) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_SOFTWARE_GL"; + return NS_OK; + } + + if (aFeature == nsIGfxInfo::FEATURE_WEBRENDER) { + // Don't try Webrender on devices where we are guaranteed to fail. + if (mGLMajorVersion < 3) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_OPENGL_LESS_THAN_3"; + return NS_OK; + } + + // Bug 1710400: Disable Webrender on the deprecated Intel DDX driver + for (const nsCString& driver : mDdxDrivers) { + if (strcasestr(driver.get(), "Intel")) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_DDX_INTEL"; + return NS_OK; + } + } + } + + const struct { + int32_t mFeature; + int32_t mCodec; + } kFeatureToCodecs[] = {{nsIGfxInfo::FEATURE_H264_HW_DECODE, CODEC_HW_H264}, + {nsIGfxInfo::FEATURE_VP8_HW_DECODE, CODEC_HW_VP8}, + {nsIGfxInfo::FEATURE_VP9_HW_DECODE, CODEC_HW_VP9}, + {nsIGfxInfo::FEATURE_AV1_HW_DECODE, CODEC_HW_AV1}}; + + for (const auto& pair : kFeatureToCodecs) { + if (aFeature != pair.mFeature) { + continue; + } + if (mVAAPISupportedCodecs & pair.mCodec) { + *aStatus = nsIGfxInfo::FEATURE_STATUS_OK; + } else { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST; + aFailureId = "FEATURE_FAILURE_VIDEO_DECODING_MISSING"; + } + return NS_OK; + } + + auto ret = GfxInfoBase::GetFeatureStatusImpl( + aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os); + + // Probe VA-API on supported devices only + if (aFeature == nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING) { + if (!StaticPrefs::media_hardware_video_decoding_enabled_AtStartup()) { + return ret; + } + bool probeHWDecode = false; +#ifdef MOZ_WAYLAND + probeHWDecode = + mIsAccelerated && + (*aStatus == nsIGfxInfo::FEATURE_STATUS_OK || + StaticPrefs::media_hardware_video_decoding_force_enabled_AtStartup() || + StaticPrefs::media_ffmpeg_vaapi_enabled_AtStartup()); +#endif + if (probeHWDecode) { + GetDataVAAPI(); + } else { + mIsVAAPISupported = Some(false); + } + if (!mIsVAAPISupported.value()) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST; + aFailureId = "FEATURE_FAILURE_VIDEO_DECODING_TEST_FAILED"; + } + } + + return ret; +} + +NS_IMETHODIMP +GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP GfxInfo::GetHasBattery(bool* aHasBattery) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) { + GetData(); + if (mIsWayland) { + aWindowProtocol = GfxDriverInfo::GetWindowProtocol(WindowProtocol::Wayland); + } else if (mIsXWayland) { + aWindowProtocol = + GfxDriverInfo::GetWindowProtocol(WindowProtocol::XWayland); + } else { + aWindowProtocol = GfxDriverInfo::GetWindowProtocol(WindowProtocol::X11); + } + Telemetry::ScalarSet(Telemetry::ScalarID::GFX_LINUX_WINDOW_PROTOCOL, + aWindowProtocol); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetTestType(nsAString& aTestType) { + GetData(); + AppendASCIItoUTF16(mTestType, aTestType); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) { + GetData(); + AppendASCIItoUTF16(mAdapterDescription, aAdapterDescription); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) { + GetData(); + *aAdapterRAM = mAdapterRAM; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) { + aAdapterDriver.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) { + GetData(); + CopyASCIItoUTF16(mDriverVendor, aAdapterDriverVendor); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) { + GetData(); + CopyASCIItoUTF16(mDriverVersion, aAdapterDriverVersion); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) { + aAdapterDriverDate.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) { + GetData(); + CopyUTF8toUTF16(mVendorId, aAdapterVendorID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) { + GetData(); + CopyUTF8toUTF16(mSecondaryVendorId, aAdapterVendorID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) { + GetData(); + CopyUTF8toUTF16(mDeviceId, aAdapterDeviceID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) { + GetData(); + CopyUTF8toUTF16(mSecondaryDeviceId, aAdapterDeviceID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) { + // This is never the case, as the active GPU should be the primary GPU. + *aIsGPU2Active = false; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) { + GetData(); + aDrmRenderDevice.Assign(mDrmRenderDevice); + return NS_OK; +} + +#ifdef DEBUG + +// Implement nsIGfxInfoDebug +// We don't support spoofing anything on Linux + +NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) { + GetData(); + CopyUTF16toUTF8(aVendorID, mVendorId); + mIsAccelerated = true; + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) { + GetData(); + CopyUTF16toUTF8(aDeviceID, mDeviceId); + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) { + GetData(); + CopyUTF16toUTF8(aDriverVersion, mDriverVersion); + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) { + // We don't support OS versioning on Linux. There's just "Linux". + return NS_OK; +} + +#endif + +} // namespace mozilla::widget diff --git a/widget/gtk/GfxInfo.h b/widget/gtk/GfxInfo.h new file mode 100644 index 0000000000..639d793b9a --- /dev/null +++ b/widget/gtk/GfxInfo.h @@ -0,0 +1,133 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_GTK_GFXINFO_h__ +#define WIDGET_GTK_GFXINFO_h__ + +#include "GfxInfoBase.h" +#include "nsString.h" + +namespace mozilla { +namespace widget { + +class GfxInfo final : public GfxInfoBase { + public: + // We only declare the subset of nsIGfxInfo that we actually implement. The + // rest is brought forward from GfxInfoBase. + NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override; + NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override; + NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override; + NS_IMETHOD GetEmbeddedInFirefoxReality( + bool* aEmbeddedInFirefoxReality) override; + NS_IMETHOD GetHasBattery(bool* aHasBattery) override; + NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override; + NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override; + NS_IMETHOD GetTestType(nsAString& aTestType) override; + NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override; + NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override; + NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override; + NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override; + NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override; + NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override; + NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override; + NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override; + NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override; + NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override; + NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override; + NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override; + NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override; + NS_IMETHOD GetAdapterDriverVersion2( + nsAString& aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override; + NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override; + NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override; + using GfxInfoBase::GetFeatureStatus; + using GfxInfoBase::GetFeatureSuggestedDriverVersion; + + virtual nsresult Init() override; + + NS_IMETHOD_(void) GetData() override; + + static bool FireGLXTestProcess(); + +#ifdef DEBUG + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIGFXINFODEBUG +#endif + + protected: + ~GfxInfo() = default; + + OperatingSystem GetOperatingSystem() override { + return OperatingSystem::Linux; + } + virtual nsresult GetFeatureStatusImpl( + int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion, + const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId, + OperatingSystem* aOS = nullptr) override; + virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override; + + virtual bool DoesWindowProtocolMatch( + const nsAString& aBlocklistWindowProtocol, + const nsAString& aWindowProtocol) override; + + virtual bool DoesDriverVendorMatch(const nsAString& aBlocklistVendor, + const nsAString& aDriverVendor) override; + static int FireTestProcess(const nsAString& aBinaryFile, int* aOutPipe, + const char** aStringArgs); + + private: + bool mInitialized = false; + nsCString mVendorId; + nsCString mDeviceId; + nsCString mDriverVendor; + nsCString mDriverVersion; + nsCString mAdapterDescription; + uint32_t mAdapterRAM; + nsCString mOS; + nsCString mOSRelease; + nsCString mTestType; + + nsCString mSecondaryVendorId; + nsCString mSecondaryDeviceId; + + nsCString mDrmRenderDevice; + + nsTArray<nsCString> mDdxDrivers; + + struct ScreenInfo { + uint32_t mWidth; + uint32_t mHeight; + bool mIsDefault; + }; + + nsTArray<ScreenInfo> mScreenInfo; + bool mHasTextureFromPixmap; + unsigned int mGLMajorVersion, mGLMinorVersion; + bool mIsMesa; + bool mIsAccelerated; + bool mIsWayland; + bool mIsXWayland; + bool mHasMultipleGPUs; + bool mGlxTestError; + mozilla::Maybe<bool> mIsVAAPISupported; + int mVAAPISupportedCodecs = 0; + + static int sGLXTestPipe; + static pid_t sGLXTestPID; + + void GetDataVAAPI(); + void AddCrashReportAnnotations(); +}; + +} // namespace widget +} // namespace mozilla + +#endif /* WIDGET_GTK_GFXINFO_h__ */ diff --git a/widget/gtk/GfxInfoUtils.h b/widget/gtk/GfxInfoUtils.h new file mode 100644 index 0000000000..b6174842f2 --- /dev/null +++ b/widget/gtk/GfxInfoUtils.h @@ -0,0 +1,100 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_GTK_GFXINFO_UTILS_h__ +#define WIDGET_GTK_GFXINFO_UTILS_h__ + +// An alternative to mozilla::Unused for use in (a) C code and (b) code where +// linking with unused.o is difficult. +#define MOZ_UNUSED(expr) \ + do { \ + if (expr) { \ + (void)0; \ + } \ + } while (0) + +#define LOG_PIPE 2 + +static bool enable_logging = false; +static void log(const char* format, ...) { + if (!enable_logging) { + return; + } + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +static int output_pipe = 1; +static void close_logging() { + // we want to redirect to /dev/null stdout, stderr, and while we're at it, + // any PR logging file descriptors. To that effect, we redirect all positive + // file descriptors up to what open() returns here. In particular, 1 is stdout + // and 2 is stderr. + int fd = open("/dev/null", O_WRONLY); + for (int i = 1; i < fd; i++) { + if (output_pipe != i) { + dup2(fd, i); + } + } + close(fd); +} + +// C++ standard collides with C standard in that it doesn't allow casting void* +// to function pointer types. So the work-around is to convert first to size_t. +// http://www.trilithium.com/johan/2004/12/problem-with-dlsym/ +template <typename func_ptr_type> +static func_ptr_type cast(void* ptr) { + return reinterpret_cast<func_ptr_type>(reinterpret_cast<size_t>(ptr)); +} + +#define BUFFER_SIZE_STEP 4000 + +static char* test_buf = nullptr; +static int test_bufsize = 0; +static int test_length = 0; + +static void record_value(const char* format, ...) { + if (!test_buf || test_length + BUFFER_SIZE_STEP / 2 > test_bufsize) { + test_bufsize += BUFFER_SIZE_STEP; + test_buf = (char*)realloc(test_buf, test_bufsize); + } + int remaining = test_bufsize - test_length; + + // Append the new values to the buffer, not to exceed the remaining space. + va_list args; + va_start(args, format); + int max_added = vsnprintf(test_buf + test_length, remaining, format, args); + va_end(args); + + if (max_added >= remaining) { + test_length += remaining; + } else { + test_length += max_added; + } +} + +static void record_error(const char* str) { record_value("ERROR\n%s\n", str); } + +static void record_warning(const char* str) { + record_value("WARNING\n%s\n", str); +} + +static void record_flush() { + if (!test_buf) { + return; + } + MOZ_UNUSED(write(output_pipe, test_buf, test_length)); + if (enable_logging) { + MOZ_UNUSED(write(LOG_PIPE, test_buf, test_length)); + } + free(test_buf); + test_buf = nullptr; +} + +#endif /* WIDGET_GTK_GFXINFO_h__ */ diff --git a/widget/gtk/GtkCompositorWidget.cpp b/widget/gtk/GtkCompositorWidget.cpp new file mode 100644 index 0000000000..2c29d5de03 --- /dev/null +++ b/widget/gtk/GtkCompositorWidget.cpp @@ -0,0 +1,246 @@ +/* -*- 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 "mozilla/gfx/gfxVars.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/widget/InProcessCompositorWidget.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsWindow.h" + +#ifdef MOZ_X11 +# include "mozilla/X11Util.h" +#endif + +#ifdef MOZ_WAYLAND +# include "mozilla/layers/NativeLayerWayland.h" +#endif + +#ifdef MOZ_LOGGING +# undef LOG +# define LOG(str, ...) \ + MOZ_LOG(IsPopup() ? gWidgetPopupLog : gWidgetLog, \ + mozilla::LogLevel::Debug, \ + ("[%p]: " str, mWidget.get(), ##__VA_ARGS__)) +#endif /* MOZ_LOGGING */ + +namespace mozilla { +namespace widget { + +GtkCompositorWidget::GtkCompositorWidget( + const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, RefPtr<nsWindow> aWindow) + : CompositorWidget(aOptions), + mWidget(std::move(aWindow)), + mClientSize(LayoutDeviceIntSize(aInitData.InitialClientSize()), + "GtkCompositorWidget::mClientSize") { +#if defined(MOZ_X11) + if (GdkIsX11Display()) { + mXWindow = (Window)aInitData.XWindow(); + ConfigureX11Backend(mXWindow, aInitData.Shaped()); + LOG("GtkCompositorWidget::GtkCompositorWidget() [%p] mXWindow %p " + "mIsRenderingSuspended %d\n", + (void*)mWidget.get(), (void*)mXWindow, !!mIsRenderingSuspended); + } +#endif +#if defined(MOZ_WAYLAND) + if (GdkIsWaylandDisplay()) { + ConfigureWaylandBackend(); + LOG("GtkCompositorWidget::GtkCompositorWidget() [%p] mWidget %p " + "mIsRenderingSuspended %d\n", + (void*)mWidget.get(), (void*)mWidget, !!mIsRenderingSuspended); + } +#endif +} + +GtkCompositorWidget::~GtkCompositorWidget() { + LOG("GtkCompositorWidget::~GtkCompositorWidget [%p]\n", (void*)mWidget.get()); + DisableRendering(); + RefPtr<nsIWidget> widget = mWidget.forget(); + NS_ReleaseOnMainThread("GtkCompositorWidget::mWidget", widget.forget()); +} + +already_AddRefed<gfx::DrawTarget> GtkCompositorWidget::StartRemoteDrawing() { + return nullptr; +} +void GtkCompositorWidget::EndRemoteDrawing() {} + +already_AddRefed<gfx::DrawTarget> +GtkCompositorWidget::StartRemoteDrawingInRegion( + const 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) { + LOG("GtkCompositorWidget::NotifyClientSizeChanged() to %d x %d", + aClientSize.width, aClientSize.height); + + auto size = mClientSize.Lock(); + *size = aClientSize; +} + +LayoutDeviceIntSize GtkCompositorWidget::GetClientSize() { + auto size = mClientSize.Lock(); + return *size; +} + +void GtkCompositorWidget::RemoteLayoutSizeUpdated( + const LayoutDeviceRect& aSize) { + if (!mWidget || !mWidget->IsWaitingForCompositorResume()) { + return; + } + + LOG("GtkCompositorWidget::RemoteLayoutSizeUpdated() %d x %d", + (int)aSize.width, (int)aSize.height); + + // We're waiting for layout to match widget size. + auto clientSize = mClientSize.Lock(); + if (clientSize->width != (int)aSize.width || + clientSize->height != (int)aSize.height) { + LOG("quit, client size doesn't match (%d x %d)", clientSize->width, + clientSize->height); + return; + } + + mWidget->ResumeCompositorFromCompositorThread(); +} + +EGLNativeWindowType GtkCompositorWidget::GetEGLNativeWindow() { + EGLNativeWindowType window = nullptr; + if (mWidget) { + window = (EGLNativeWindowType)mWidget->GetNativeData(NS_NATIVE_EGL_WINDOW); + } +#if defined(MOZ_X11) + if (mXWindow) { + window = (EGLNativeWindowType)mXWindow; + } +#endif + LOG("GtkCompositorWidget::GetEGLNativeWindow [%p] window %p\n", mWidget.get(), + window); + return window; +} + +#if defined(MOZ_WAYLAND) +void GtkCompositorWidget::SetEGLNativeWindowSize( + const LayoutDeviceIntSize& aEGLWindowSize) { + if (mWidget) { + mWidget->SetEGLNativeWindowSize(aEGLWindowSize); + } +} +#endif + +LayoutDeviceIntRegion GtkCompositorWidget::GetTransparentRegion() { + // We need to clear target buffer alpha values of popup windows as + // SW-WR paints with alpha blending (see Bug 1674473). + if (!mWidget || mWidget->IsPopup()) { + return LayoutDeviceIntRect(LayoutDeviceIntPoint(0, 0), GetClientSize()); + } + + // Clear background of titlebar area to render titlebar + // transparent corners correctly. + return mWidget->GetTitlebarRect(); +} + +#ifdef MOZ_WAYLAND +RefPtr<mozilla::layers::NativeLayerRoot> +GtkCompositorWidget::GetNativeLayerRoot() { + if (gfx::gfxVars::UseWebRenderCompositor()) { + if (!mNativeLayerRoot) { + MOZ_ASSERT(mWidget && mWidget->GetMozContainer()); + mNativeLayerRoot = NativeLayerRootWayland::CreateForMozContainer( + mWidget->GetMozContainer()); + } + return mNativeLayerRoot; + } + return nullptr; +} +#endif + +void GtkCompositorWidget::DisableRendering() { + LOG("GtkCompositorWidget::DisableRendering [%p]\n", (void*)mWidget.get()); + mIsRenderingSuspended = true; + mProvider.CleanupResources(); +#if defined(MOZ_X11) + mXWindow = {}; +#endif +} + +#if defined(MOZ_WAYLAND) +bool GtkCompositorWidget::ConfigureWaylandBackend() { + mProvider.Initialize(this); + return true; +} +#endif + +#if defined(MOZ_X11) +bool GtkCompositorWidget::ConfigureX11Backend(Window aXWindow, bool aShaped) { + mXWindow = aXWindow; + + // We don't have X window yet. + if (!mXWindow) { + mIsRenderingSuspended = true; + return false; + } + + // Grab the window's visual and depth + XWindowAttributes windowAttrs; + if (!XGetWindowAttributes(DefaultXDisplay(), mXWindow, &windowAttrs)) { + NS_WARNING("GtkCompositorWidget(): XGetWindowAttributes() failed!"); + return false; + } + + Visual* visual = windowAttrs.visual; + int depth = windowAttrs.depth; + + // Initialize the window surface provider + mProvider.Initialize(mXWindow, visual, depth, aShaped); + return true; +} +#endif + +void GtkCompositorWidget::EnableRendering(const uintptr_t aXWindow, + const bool aShaped) { + LOG("GtkCompositorWidget::EnableRendering() [%p]\n", mWidget.get()); + + if (!mIsRenderingSuspended) { + LOG(" quit, mIsRenderingSuspended = false\n"); + return; + } +#if defined(MOZ_WAYLAND) + if (GdkIsWaylandDisplay()) { + LOG(" configure widget %p\n", mWidget.get()); + if (!ConfigureWaylandBackend()) { + return; + } + } +#endif +#if defined(MOZ_X11) + if (GdkIsX11Display()) { + LOG(" configure XWindow %p shaped %d\n", (void*)aXWindow, aShaped); + if (!ConfigureX11Backend((Window)aXWindow, aShaped)) { + return; + } + } +#endif + mIsRenderingSuspended = false; +} +#ifdef MOZ_LOGGING +bool GtkCompositorWidget::IsPopup() { + return mWidget ? mWidget->IsPopup() : false; +} +#endif + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/GtkCompositorWidget.h b/widget/gtk/GtkCompositorWidget.h new file mode 100644 index 0000000000..601172d09c --- /dev/null +++ b/widget/gtk/GtkCompositorWidget.h @@ -0,0 +1,137 @@ +/* -*- 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 layers { +class NativeLayerRootWayland; +} // namespace layers + +namespace widget { + +class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate { + public: + virtual void NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) = 0; + virtual GtkCompositorWidget* AsGtkCompositorWidget() { return nullptr; }; + + virtual void DisableRendering() = 0; + virtual void EnableRendering(const uintptr_t aXWindow, + const bool aShaped) = 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, + RefPtr<nsWindow> aWindow /* = nullptr*/); + ~GtkCompositorWidget(); + + // CompositorWidget Overrides + + already_AddRefed<gfx::DrawTarget> StartRemoteDrawing() override; + void EndRemoteDrawing() override; + + already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) override; + void EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion) override; + + LayoutDeviceIntSize GetClientSize() override; + void RemoteLayoutSizeUpdated(const LayoutDeviceRect& aSize); + + nsIWidget* RealWidget() override; + GtkCompositorWidget* AsGTK() override { return this; } + CompositorWidgetDelegate* AsDelegate() override { return this; } + + EGLNativeWindowType GetEGLNativeWindow(); + + LayoutDeviceIntRegion GetTransparentRegion() override; + + // Suspend rendering of this remote widget and clear all resources. + // Can be used when underlying window is hidden/unmapped. + void DisableRendering() override; + + // Resume rendering with to given aXWindow (X11) or nsWindow (Wayland). + void EnableRendering(const uintptr_t aXWindow, const bool aShaped) override; + +#if defined(MOZ_X11) + Window XWindow() const { return mXWindow; } +#endif +#if defined(MOZ_WAYLAND) + void SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize); + RefPtr<mozilla::layers::NativeLayerRoot> GetNativeLayerRoot() override; +#endif + + bool PreRender(WidgetRenderingContext* aContext) override { + return !mIsRenderingSuspended; + } + bool IsHidden() const override { return mIsRenderingSuspended; } + + // PlatformCompositorWidgetDelegate Overrides + + void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override; + GtkCompositorWidget* AsGtkCompositorWidget() override { return this; } + + private: +#if defined(MOZ_WAYLAND) + bool ConfigureWaylandBackend(); +#endif +#if defined(MOZ_X11) + bool ConfigureX11Backend(Window aXWindow, bool aShaped); +#endif +#ifdef MOZ_LOGGING + bool IsPopup(); +#endif + + protected: + RefPtr<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) + Window mXWindow = {}; +#endif +#ifdef MOZ_WAYLAND + RefPtr<mozilla::layers::NativeLayerRootWayland> mNativeLayerRoot; +#endif + Atomic<bool> mIsRenderingSuspended{true}; +}; + +} // 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..2e438bbc5c --- /dev/null +++ b/widget/gtk/IMContextWrapper.cpp @@ -0,0 +1,3360 @@ +/* -*- 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 "nsString.h" +#include "prtime.h" +#include "prenv.h" + +#include "IMContextWrapper.h" + +#include "GRefPtr.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/StaticPrefs_intl.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/WritingModes.h" + +// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync` +// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too +// big file. +// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. +mozilla::LazyLogModule gIMELog("IMEHandler"); + +namespace mozilla { +namespace widget { + +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 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. + ******************************************************************************/ + +static Maybe<nscolor> GetSystemColor(LookAndFeel::ColorID aId) { + return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light, + LookAndFeel::UseStandins::No); +} + +class SelectionStyleProvider final { + public: + static SelectionStyleProvider* GetExistingInstance() { return sInstance; } + + 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. + if (auto selectionForegroundColor = + GetSystemColor(LookAndFeel::ColorID::Highlight)) { + 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(");"); + } + if (auto selectionBackgroundColor = + GetSystemColor(LookAndFeel::ColorID::Highlighttext)) { + 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), + mFallbackToKeyEvent(false), + mKeyboardEventWasDispatched(false), + mKeyboardEventWasConsumed(false), + mIsDeletingSurrounding(false), + mLayoutChanged(false), + mSetCursorPositionOnKeyEvent(true), + mPendingResettingIMContext(false), + mRetrieveSurroundingSignalReceived(false), + mMaybeInDeadKeySequence(false), + mIsIMInAsyncKeyHandlingMode(false), + mSetInputPurposeAndInputHints(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("@", 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(gIMELog, 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() { + MOZ_ASSERT(!mContext); + MOZ_ASSERT(!mComposingContext); + if (this == sLastFocusedContext) { + sLastFocusedContext = nullptr; + } + MOZ_LOG(gIMELog, 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 IsComposing() ? EndIMEComposition(window) : NS_OK; + } + 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( + gIMELog, 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) { + if (IsComposing()) { + EndIMEComposition(aWindow); + } + NotifyIMEOfFocusChange(IMEFocusState::Blurred); + mLastFocusedWindow = nullptr; + } + + if (mOwnerWindow != aWindow) { + return; + } + + if (sLastFocusedContext == this) { + sLastFocusedContext = nullptr; + } + + /** + * NOTE: + * The given window is the owner of this, so, we must disconnect from 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_signal_handlers_disconnect_by_data(mContext, this); + g_object_unref(mContext); + mContext = nullptr; + } + + if (mSimpleContext) { + gtk_im_context_set_client_window(mSimpleContext, nullptr); + g_signal_handlers_disconnect_by_data(mSimpleContext, this); + 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(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p PrepareToDestroyContext(), added to reference to " + "GtkIMContextIIIM class to prevent it from being unloaded", + this)); + } else { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this, + aWindow, mLastFocusedWindow)); + mLastFocusedWindow = aWindow; +} + +void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, " + "mIMEFocusState=%s", + this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str())); + + if (mLastFocusedWindow != aWindow) { + return; + } + + NotifyIMEOfFocusChange(IMEFocusState::Blurred); +} + +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(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>")); + MOZ_LOG( + gIMELog, 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( + gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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; + } + + if (aEvent->type == GDK_KEY_RELEASE) { + if (const GdkEventKey* pendingKeyPressEvent = + mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event " + "because GDK_KEY_RELEASE for the event is handled", + this)); + mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent); + } + } + + MOZ_LOG( + gIMELog, 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(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, " + "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s", + this, ToChar(aFocus), GetCompositionStateName(), + ToString(mIMEFocusState).c_str(), + ToChar(mSetInputPurposeAndInputHints))); + + // We shouldn't carry over the removed string to another editor. + mSelectedStringRemovedByComposition.Truncate(); + mContentSelection.reset(); + + if (aFocus) { + if (mSetInputPurposeAndInputHints) { + mSetInputPurposeAndInputHints = false; + SetInputPurposeAndInputHints(); + } + NotifyIMEOfFocusChange(IMEFocusState::Focused); + } else { + NotifyIMEOfFocusChange(IMEFocusState::Blurred); + } + + // 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 && EnsureToCacheContentSelection()) { + SetCursorPosition(GetActiveContext()); + } +} + +void IMContextWrapper::ResetIME() { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this, + GetCompositionStateName(), ToString(mIMEFocusState).c_str())); + + GtkIMContext* activeContext = GetActiveContext(); + if (MOZ_UNLIKELY(!activeContext)) { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Debug, + ("0x%p ResetIME() called gtk_im_context_reset(), " + "activeContext=0x%p, mCompositionState=%s, compositionString=%s, " + "mIMEFocusState=%s", + this, activeContext, GetCompositionStateName(), + NS_ConvertUTF16toUTF8(compositionString).get(), + ToString(mIMEFocusState).c_str())); + + // 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(gIMELog, LogLevel::Info, + ("0x%p EndIMEComposition(aCaller=0x%p), " + "mCompositionState=%s", + this, aCaller, GetCompositionStateName())); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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 + mContentSelection.reset(); + EnsureToCacheContentSelection(); + 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(gIMELog, 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(gIMELog, LogLevel::Error, + ("0x%p SetInputContext(), FAILED, " + "the caller isn't focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return; + } + + if (!mContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetInputContext(), FAILED, " + "there are no context", + this)); + return; + } + + if (sLastFocusedContext != this) { + mInputContext = *aContext; + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p SetInputContext(), succeeded, " + "but we're not active", + this)); + return; + } + + const bool changingEnabledState = + aContext->IsInputAttributeChanged(mInputContext); + + // Release current IME focus if IME is enabled. + if (changingEnabledState && mInputContext.mIMEState.IsEditable()) { + if (IsComposing()) { + EndIMEComposition(mLastFocusedWindow); + } + if (mIMEFocusState == IMEFocusState::Focused) { + NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange); + } + } + + mInputContext = *aContext; + mSetInputPurposeAndInputHints = false; + + if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) { + return; + } + + // If the input context was temporarily disabled without a focus change, + // it must be ready to query content even if the focused content is in + // a remote process. In this case, we should set IME focus right now. + if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) { + SetInputPurposeAndInputHints(); + NotifyIMEOfFocusChange(IMEFocusState::Focused); + return; + } + + // Otherwise, we cannot set input-purpose and input-hints right now because + // setting them may require to set focus immediately for IME own's UI. + // However, at this moment, `ContentCacheInParent` does not have content + // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification. + // Therefore, we set them at receiving the notification. + mSetInputPurposeAndInputHints = true; +} + +void IMContextWrapper::SetInputPurposeAndInputHints() { + GtkIMContext* currentContext = GetCurrentContext(); + if (!currentContext) { + return; + } + + 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.mHTMLInputMode.EqualsLiteral("decimal")) { + purpose = GTK_INPUT_PURPOSE_NUMBER; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) { + purpose = GTK_INPUT_PURPOSE_EMAIL; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) { + purpose = GTK_INPUT_PURPOSE_DIGITS; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) { + purpose = GTK_INPUT_PURPOSE_PHONE; + } else if (mInputContext.mHTMLInputMode.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.mHTMLInputMode.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); +} + +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::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) { + MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange, + mIMEFocusState != IMEFocusState::Blurred); + if (mIMEFocusState == aIMEFocusState) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, " + "sLastFocusedContext=0x%p", + this, ToString(aIMEFocusState).c_str(), + ToString(mIMEFocusState).c_str(), sLastFocusedContext)); + MOZ_ASSERT(!mSetInputPurposeAndInputHints); + + // If we've already made IME blurred at setting the input context disabled + // and it's now completely blurred by a focus move, we need only to update + // mIMEFocusState and when the input context gets enabled, we cannot set + // IME focus immediately. + if (aIMEFocusState == IMEFocusState::Blurred && + mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) { + mIMEFocusState = IMEFocusState::Blurred; + return; + } + + auto Blur = [&](IMEFocusState aInternalState) { + GtkIMContext* currentContext = GetCurrentContext(); + if (MOZ_UNLIKELY(!currentContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p NotifyIMEOfFocusChange()::Blur(), FAILED, " + "there is no context", + this)); + return; + } + gtk_im_context_focus_out(currentContext); + mIMEFocusState = aInternalState; + }; + + if (aIMEFocusState != IMEFocusState::Focused) { + return Blur(aIMEFocusState); + } + + GtkIMContext* currentContext = GetCurrentContext(); + if (MOZ_UNLIKELY(!currentContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p NotifyIMEOfFocusChange(), FAILED, " + "there is no context", + this)); + return; + } + + if (sLastFocusedContext && sLastFocusedContext != this) { + sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred); + } + + 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); + mIMEFocusState = aIMEFocusState; + 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(IMEFocusState::BlurredWithoutFocusChange); + } +} + +void IMContextWrapper::OnSelectionChange( + nsWindow* aCaller, const IMENotification& aIMENotification) { + const bool isSelectionRangeChanged = + mContentSelection.isNothing() || + !aIMENotification.mSelectionChangeData.EqualsRange( + mContentSelection.ref()); + mContentSelection = + Some(ContentSelection(aIMENotification.mSelectionChangeData)); + const bool retrievedSurroundingSignalReceived = + mRetrieveSurroundingSignalReceived; + mRetrieveSurroundingSignalReceived = false; + + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + const IMENotification::SelectionChangeDataBase& selectionChangeData = + aIMENotification.mSelectionChangeData; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ " + "mSelectionChangeData=%s }), " + "mCompositionState=%s, mIsDeletingSurrounding=%s, " + "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s", + this, aCaller, ToString(selectionChangeData).c_str(), + GetCompositionStateName(), ToChar(mIsDeletingSurrounding), + ToChar(retrievedSurroundingSignalReceived), + ToChar(isSelectionRangeChanged))); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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(mContentSelection.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnSelectionChange(), FAILED, " + "new offset is too large, cannot keep composing", + this)); + } else if (mContentSelection->HasRange()) { + // Modify the selection start offset with new offset. + mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset(); + // XXX We should modify mSelectedStringRemovedByComposition? + // But how? + MOZ_LOG(gIMELog, 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; + } else { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p OnSelectionChange(), ignored, because of no selection range", + this)); + 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. + // Don't do this even if selection is not changed actually. For example, + // fcitx has direct input mode which does not insert composing string, but + // inserts commited text for each key sequence (i.e., there is "invisible" + // composition string). In the world after bug 1712269, we don't use a + // set of composition events for this kind of IME. Therefore, + // SelectionChangeData.mCausedByComposition is not expected value for here + // if this call is caused by a preceding commit. And if the preceding commit + // is triggered by a key type for next word, resetting IME state makes fcitx + // discard the pending input for the next word. Thus, we need to check + // whether the selection range is actually changed here. + if (!selectionChangeData.mCausedByComposition && + !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged && + !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 (auto* provider = SelectionStyleProvider::GetExistingInstance()) { + provider->OnThemeChanged(); + } +} + +/* static */ +void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + aModule->OnStartCompositionNative(aContext); +} + +void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) { + // IME may synthesize composition asynchronously after filtering a + // GDK_KEY_PRESS event. In that case, we should handle composition with + // emulating the usual case, i.e., this is called in the stack of + // OnKeyEvent(). + Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent; + if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) { + GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent(); + if (keyEvent && keyEvent->type == GDK_KEY_PRESS && + KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) == + KEY_NAME_INDEX_USE_STRING) { + maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent); + mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent(); + } + } + + MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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) { + RefPtr module = aModule; + module->OnChangeCompositionNative(aContext); + + if (module->IsDestroyed()) { + // A strong reference is already held during "preedit-changed" emission, + // but _ibus_context_destroy_cb() in ibus 1.5.28 and + // _fcitx_im_context_close_im_cb() in fcitx 4.2.9.9 want their + // GtkIMContexts to live a little longer. See bug 1824634. + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, [context = RefPtr{aContext}]() {})); + } +} + +void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) { + // IME may synthesize composition asynchronously after filtering a + // GDK_KEY_PRESS event. In that case, we should handle composition with + // emulating the usual case, i.e., this is called in the stack of + // OnKeyEvent(). + Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent; + if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) { + GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent(); + if (keyEvent && keyEvent->type == GDK_KEY_PRESS && + KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) == + KEY_NAME_INDEX_USE_STRING) { + maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent); + mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent(); + } + } + + MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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); + + // IME may synthesize composition asynchronously after filtering a + // GDK_KEY_PRESS event. In that case, we should handle composition with + // emulating the usual case, i.e., this is called in the stack of + // OnKeyEvent(). + Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent; + if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) { + GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent(); + if (keyEvent && keyEvent->type == GDK_KEY_PRESS && + KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) == + KEY_NAME_INDEX_USE_STRING) { + maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent); + mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent(); + } + } + + MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "dispatched eKeyDown event for the committed character", + this)); + + // Next, dispatch eKeyPress event. + dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status, + mProcessingKeyEvent); + MOZ_LOG(gIMELog, 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(gIMELog, 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( + gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup " + "event is dispatched", + this)); + + if (!mProcessingKeyEvent) { + MOZ_LOG(gIMELog, 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: + case eContentCommandInsertText: + 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(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(" + "aFollowingEvent=%s), dispatch fake eKeyDown event", + this, ToChar(aFollowingEvent))); + + KeymapWrapper::DispatchKeyDownOrKeyUpEvent( + lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), " + "fake keydown event is dispatched", + this)); + } + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext)); + + if (IsComposing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "we're already in composition", + this)); + return true; + } + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + if (NS_WARN_IF(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "cannot query the selection offset", + this)); + return false; + } + + if (NS_WARN_IF(!mContentSelection->HasRange())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "due to no selection", + 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 = mContentSelection->OffsetAndDataRef().StartOffset(); + 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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( + gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext)); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + if (!IsComposing()) { + MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, 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(!EnsureToCacheContentSelection( + &mSelectedStringRemovedByComposition))) { + // XXX How should we behave in this case?? + } else if (mContentSelection->HasRange()) { + // XXX We should assume, for now, any web applications don't change + // selection at handling this compositionchange event. + mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset(); + } else { + // If there is no selection range, we should keep previously storing + // mCompositionStart. + } + } + + RefPtr<TextRangeArray> rangeArray = + CreateTextRangeArray(aContext, aCompositionString); + + rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to FlushPendingComposition() failure", + this)); + return false; + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, " + "aCommitString=0x%p, (\"%s\"))", + this, aContext, aCommitString, + aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "")); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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()?) + RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow); + RefPtr<TextEventDispatcher> dispatcher; + if (!IsComposing() && + !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) { + if (!aCommitString || aCommitString->IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "did nothing due to inserting empty string without composition", + this)); + return true; + } + if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "Failed to cache selection before dispatching " + "eContentCommandInsertText event", + this)); + } + if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return false; + } + // Emulate selection until receiving actual selection range. This is + // important for OnSelectionChange. If selection is not changed by web + // apps, i.e., selection range is same as what selection expects, we + // shouldn't reset IME because the trigger of causing this commit may be an + // input for next composition and we shouldn't cancel it. + if (mContentSelection.isSome()) { + mContentSelection->Collapse( + (mContentSelection->HasRange() + ? mContentSelection->OffsetAndDataRef().StartOffset() + : mCompositionStart) + + aCommitString->Length()); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionCommitEvent(), mContentSelection=%s", + this, ToString(mContentSelection).c_str())); + } + MOZ_ASSERT(!dispatcher); + } else { + if (!IsComposing()) { + if (!aCommitString || aCommitString->IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "there is no composition and empty commit string", + this)); + return true; + } + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + mCompositionState = eCompositionState_NotComposing; + return false; + } + + dispatcher = GetTextEventDispatcher(); + MOZ_ASSERT(dispatcher); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + // Emulate selection until receiving actual selection range. + const uint32_t offsetToPutCaret = + mCompositionStart + (aCommitString + ? aCommitString->Length() + : mDispatchedCompositionString.Length()); + if (mContentSelection.isSome()) { + mContentSelection->Collapse(offsetToPutCaret); + } else { + // TODO: We should guarantee that there should be at least fake selection + // for IME at here. Then, we can keep the last writing mode. + mContentSelection.emplace(offsetToPutCaret, WritingMode()); + } + } + + 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(); + + if (!dispatcher) { + MOZ_ASSERT(aCommitString); + MOZ_ASSERT(!aCommitString->IsEmpty()); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText, + lastFocusedWindow); + insertTextEvent.mString.emplace(*aCommitString); + lastFocusedWindow->DispatchEvent(&insertTextEvent, status); + + if (!insertTextEvent.mSucceeded) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, inserting " + "text failed", + this)); + return false; + } + } else { + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = dispatcher->CommitComposition(status, aCommitString); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to CommitComposition() failure", + this)); + return false; + } + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p CreateTextRangeArray(aContext=0x%p, " + "aCompositionString=\"%s\" (Length()=%zu))", + 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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( + gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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( + gIMELog, LogLevel::Info, + ("0x%p SetCursorPosition(aContext=0x%p), " + "mCompositionTargetRange={ mOffset=%u, mLength=%u }, " + "mContentSelection=%s", + this, aContext, mCompositionTargetRange.mOffset, + mCompositionTargetRange.mLength, ToString(mContentSelection).c_str())); + + bool useCaret = false; + if (!mCompositionTargetRange.IsValid()) { + if (mContentSelection.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, " + "mCompositionTargetRange and mContentSelection are invalid", + this)); + return; + } + if (!mContentSelection->HasRange()) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p SetCursorPosition(), FAILED, " + "mCompositionTargetRange is invalid and there is no selection", + this)); + return; + } + useCaret = true; + } + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, due to no focused " + "window", + this)); + return; + } + + if (MOZ_UNLIKELY(!aContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, due to no context", this)); + return; + } + + WidgetQueryContentEvent queryCaretOrTextRectEvent( + true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow); + if (useCaret) { + queryCaretOrTextRectEvent.InitForQueryCaretRect( + mContentSelection->OffsetAndDataRef().StartOffset()); + } else { + if (mContentSelection->WritingModeRef().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); + } + } + nsEventStatus status; + mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status); + if (queryCaretOrTextRectEvent.Failed()) { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Info, + ("0x%p GetCurrentParagraph(), mCompositionState=%s", this, + GetCompositionStateName())); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, 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(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, due to no " + "valid selection information", + this)); + return NS_ERROR_FAILURE; + } + + if (mContentSelection.isSome() && mContentSelection->HasRange()) { + selOffset = mContentSelection->OffsetAndDataRef().StartOffset(); + selLength = mContentSelection->OffsetAndDataRef().Length(); + } else { + // If there is no range, let's get all text instead... + selOffset = 0u; + selLength = INT32_MAX; // TODO: Change to UINT32_MAX, but see below + } + } + + MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, 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 = 0; + if (selOffset > 0) { + parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1; + } + int32_t parEnd = textContent.Find(u"\n", selOffset + selLength); + if (parEnd < 0) { + parEnd = textContent.Length(); + } + aText = nsDependentSubstring(textContent, parStart, parEnd - parStart); + aCursorPos = selOffset - uint32_t(parStart); + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p GetCurrentParagraph(), succeeded, aText=%s, " + "aText.Length()=%zu, 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(gIMELog, LogLevel::Info, + ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), " + "mCompositionState=%s", + this, aContext, aOffset, aNChars, GetCompositionStateName())); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, there are no focused window " + "in this module", + this)); + return NS_ERROR_NULL_POINTER; + } + + if (!aNChars) { + MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, quitting from DeletText", this)); + return NS_ERROR_FAILURE; + } + } else { + if (NS_WARN_IF(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, due to no valid selection " + "information", + this)); + return NS_ERROR_FAILURE; + } + if (!mContentSelection->HasRange()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p DeleteText(), does nothing, due to no selection range", + this)); + return NS_OK; + } + selOffset = mContentSelection->OffsetAndDataRef().StartOffset(); + } + + // 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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(gIMELog, 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( + gIMELog, 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( + gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, restoring composition string", this)); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +bool IMContextWrapper::EnsureToCacheContentSelection( + nsAString* aSelectedString) { + if (aSelectedString) { + aSelectedString->Truncate(); + } + + if (mContentSelection.isSome()) { + if (mContentSelection->HasRange() && aSelectedString) { + aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef()); + } + return true; + } + + RefPtr<nsWindow> dispatcherWindow = + mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow; + if (NS_WARN_IF(!dispatcherWindow)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p EnsureToCacheContentSelection(), FAILED, due to " + "no focused window", + this)); + return false; + } + + nsEventStatus status; + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + dispatcherWindow); + dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status); + if (NS_WARN_IF(querySelectedTextEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p EnsureToCacheContentSelection(), FAILED, due to " + "failure of query selection event", + this)); + return false; + } + + mContentSelection = Some(ContentSelection(querySelectedTextEvent)); + if (mContentSelection->HasRange()) { + if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() && + aSelectedString) { + aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef()); + } + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s", + this, ToString(mContentSelection).c_str())); + return true; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/IMContextWrapper.h b/widget/gtk/IMContextWrapper.h new file mode 100644 index 0000000000..cf1d2638e0 --- /dev/null +++ b/widget/gtk/IMContextWrapper.h @@ -0,0 +1,688 @@ +/* -*- 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 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/ContentData.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Maybe.h" +#include "mozilla/TextEventDispatcherListener.h" +#include "mozilla/WritingModes.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/widget/IMEData.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(); + + /** + * SetInputPurposeAndInputHints() sets input-purpose and input-hints of + * current IM context to the values computed with mInputContext. + */ + void SetInputPurposeAndInputHints(); + + // 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() { mEvents.Clear(); } + + /** + * 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; + } + mEvents.RemoveElementAt(index); + } + + /** + * Return corresponding GDK_KEY_PRESS event for aEvent. aEvent must be a + * GDK_KEY_RELEASE event. + */ + const GdkEventKey* GetCorrespondingKeyPressEvent( + const GdkEventKey* aEvent) const { + MOZ_ASSERT(aEvent->type == GDK_KEY_RELEASE); + for (const GUniquePtr<GdkEventKey>& pendingKeyEvent : mEvents) { + if (pendingKeyEvent->type == GDK_KEY_PRESS && + aEvent->hardware_keycode == pendingKeyEvent->hardware_keycode) { + return pendingKeyEvent.get(); + } + } + return nullptr; + } + + /** + * FirstEvent() returns oldest event in the queue. + */ + GdkEventKey* GetFirstEvent() const { + if (mEvents.IsEmpty()) { + return nullptr; + } + return mEvents[0].get(); + } + + 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].get(); + // 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<GUniquePtr<GdkEventKey>> mEvents; + }; + // 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; + + // If mContentSelection is Nothing, it means that + // EnsureToCacheContentSelection failed to get selection or just not caching + // the selection. + Maybe<ContentSelection> mContentSelection; + + /** + * Return true if mContentSelection is set to some. Otherwise, false. + */ + bool EnsureToCacheContentSelection(nsAString* aSelectedString = nullptr); + + enum class IMEFocusState : uint8_t { + // IME has focus + Focused, + // IME was blurred + Blurred, + // IME was blurred without a focus change + BlurredWithoutFocusChange, + }; + friend std::ostream& operator<<(std::ostream& aStream, IMEFocusState aState) { + switch (aState) { + case IMEFocusState::Focused: + return aStream << "IMEFocusState::Focused"; + case IMEFocusState::Blurred: + return aStream << "IMEFocusState::Blurred"; + case IMEFocusState::BlurredWithoutFocusChange: + return aStream << "IMEFocusState::BlurredWithoutFocusChange"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid value"); + return aStream << "<illegal value>"; + } + } + IMEFocusState mIMEFocusState = IMEFocusState::Blurred; + + // 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; + // mSetInputPurposeAndInputHints is set if `SetInputContext` wants `Focus` + // to set input-purpose and input-hints. + bool mSetInputPurposeAndInputHints; + + // 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; } + + void NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState); + + // 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); + + // 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..bffd6f5a7f --- /dev/null +++ b/widget/gtk/InProcessGtkCompositorWidget.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 "HeadlessCompositorWidget.h" +#include "HeadlessWidget.h" +#include "mozilla/widget/PlatformWidgetTypes.h" + +#include "InProcessGtkCompositorWidget.h" +#include "VsyncDispatcher.h" +#include "nsWindow.h" + +namespace mozilla::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 mozilla::widget 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..4080b56c17 --- /dev/null +++ b/widget/gtk/MPRISServiceHandler.cpp @@ -0,0 +1,856 @@ +/* -*- 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/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/Maybe.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "nsXULAppAPI.h" +#include "nsIXULAppInfo.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "WidgetUtilsGtk.h" +#include "prio.h" + +#define LOGMPRIS(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<dom::MediaControlKey> GetMediaControlKey( + const gchar* aMethodName) { + const std::unordered_map<std::string, dom::MediaControlKey> map = { + {"Raise", dom::MediaControlKey::Focus}, + {"Next", dom::MediaControlKey::Nexttrack}, + {"Previous", dom::MediaControlKey::Previoustrack}, + {"Pause", dom::MediaControlKey::Pause}, + {"PlayPause", dom::MediaControlKey::Playpause}, + {"Stop", dom::MediaControlKey::Stop}, + {"Play", 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<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<dom::MediaControlKey> GetPairedKey(Property aProperty) { + switch (aProperty) { + case Property::eCanRaise: + return Some(dom::MediaControlKey::Focus); + case Property::eCanGoNext: + return Some(dom::MediaControlKey::Nexttrack); + case Property::eCanGoPrevious: + return Some(dom::MediaControlKey::Previoustrack); + case Property::eCanPlay: + return Some(dom::MediaControlKey::Play); + case Property::eCanPause: + return Some(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<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) { + LOGMPRIS("OnNameAcquired: %s", aName); + mConnection = aConnection; +} + +void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection, + const gchar* aName) { + LOGMPRIS("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. + LOGMPRIS("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. + LOGMPRIS("Unable to unregister object from within onNameLost!"); + } +} + +void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection, + const gchar* aName) { + GUniquePtr<GError> error; + LOGMPRIS("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 */ + getter_Transfers(error)); /* GError** */ + + if (mRootRegistrationId == 0) { + LOGMPRIS("Failed at root registration: %s", + error ? error->message : "Unknown 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 */ + getter_Transfers(error)); /* GError** */ + + if (mPlayerRegistrationId == 0) { + LOGMPRIS("Failed at object registration: %s", + error ? error->message : "Unknown Error"); + } +} + +bool MPRISServiceHandler::Open() { + MOZ_ASSERT(!mInitialized); + MOZ_ASSERT(NS_IsMainThread()); + GUniquePtr<GError> error; + 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 = dont_AddRef( + g_dbus_node_info_new_for_xml(introspection_xml, getter_Transfers(error))); + + if (!mIntrospectionData) { + LOGMPRIS("Failed at parsing XML Interface definition: %s", + error ? error->message : "Unknown Error"); + return false; + } + + mInitialized = true; + return true; +} + +MPRISServiceHandler::MPRISServiceHandler() = default; +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); + } + + mIntrospectionData = nullptr; + + 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 { + NS_WARNING_ASSERTION(mInitialized, + "MPRISServiceHandler should have been initialized."); + return mIdentity.get(); +} + +const char* MPRISServiceHandler::DesktopEntry() const { + NS_WARNING_ASSERTION(mInitialized, + "MPRISServiceHandler should have been initialized."); + return mDesktopEntry.get(); +} + +bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey) const { + MOZ_ASSERT(mInitialized); + if (!IsMediaKeySupported(aKey)) { + LOGMPRIS("%s is not supported", ToMediaControlKeyStr(aKey)); + return false; + } + LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey)); + EmitEvent(aKey); + return true; +} + +void MPRISServiceHandler::SetPlaybackState( + dom::MediaSessionPlaybackState aState) { + LOGMPRIS("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); + + LOGMPRIS("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 (dom::IsImageIn(aMetadata.mArtwork, mFetchingUrl)) { + LOGMPRIS( + "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 (dom::IsImageIn(aMetadata.mArtwork, mCurrentImageUrl)) { + LOGMPRIS("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); + + LOGMPRIS("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()) { + LOGMPRIS("Stop loading image to MPRIS. No available image"); + mImageFetchRequest.DisconnectIfExists(); + return; + } + + const dom::MediaImage& image = mMPRISMetadata.mArtwork[aIndex]; + + if (!dom::IsValidImageUrl(image.mSrc)) { + LOGMPRIS("Skip the image with invalid URL. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + mImageFetchRequest.DisconnectIfExists(); + mFetchingUrl = image.mSrc; + + mImageFetcher = MakeUnique<dom::FetchImageHelper>(image); + RefPtr<MPRISServiceHandler> self = this; + mImageFetcher->FetchImage() + ->Then( + AbstractThread::MainThread(), __func__, + [this, self](const nsCOMPtr<imgIContainer>& aImage) { + LOGMPRIS("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 = dom::GetEncodedImageBuffer( + aImage, mMimeType, getter_AddRefs(inputStream), &size, &data); + if (NS_FAILED(rv) || !inputStream || size == 0 || !data) { + LOGMPRIS("Failed to get the image buffer info. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + if (SetImageToDisplay(data, size)) { + mCurrentImageUrl = mFetchingUrl; + LOGMPRIS("The MPRIS image is updated to the image from: %s", + NS_ConvertUTF16toUTF8(mCurrentImageUrl).get()); + } else { + LOGMPRIS("Failed to set image to MPRIS"); + mCurrentImageUrl.Truncate(); + } + + mFetchingUrl.Truncate(); + }, + [this, self](bool) { + LOGMPRIS("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()); + + LOGMPRIS("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()) { + LOGMPRIS("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) { + LOGMPRIS("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)) { + LOGMPRIS("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)) { + LOGMPRIS("Failed to create an image filename"); + return false; + } + + rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to create an image file"); + return false; + } + + cleanup.release(); + return true; +} + +bool MPRISServiceHandler::InitLocalImageFolder() { + if (mLocalImageFolder && LocalImageFolderExists()) { + return true; + } + + nsresult rv = NS_ERROR_FAILURE; + if (IsRunningUnderFlatpak()) { + // The XDG_DATA_HOME points to the same location in the host and guest + // filesystem. + if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) { + rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome), true, + getter_AddRefs(mLocalImageFolder)); + } + } else { + rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR, + getter_AddRefs(mLocalImageFolder)); + } + + if (NS_FAILED(rv) || !mLocalImageFolder) { + LOGMPRIS("Failed to get the image folder"); + return false; + } + + auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; }); + + rv = mLocalImageFolder->Append(u"firefox-mpris"_ns); + if (NS_FAILED(rv)) { + LOGMPRIS("Failed to name an image folder"); + return false; + } + + if (!LocalImageFolderExists()) { + rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_FAILED(rv)) { + LOGMPRIS("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. + LOGMPRIS("Failed to remove images"); + } + + LOGMPRIS("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(dom::MediaControlKey aKey) const { + for (const auto& listener : mListeners) { + listener->OnActionPerformed(dom::MediaControlAction(aKey)); + } +} + +struct InterfaceProperty { + const char* interface; + const char* property; +}; +static const std::unordered_map<dom::MediaControlKey, InterfaceProperty> + gKeyProperty = { + {dom::MediaControlKey::Focus, {DBUS_MPRIS_INTERFACE, "CanRaise"}}, + {dom::MediaControlKey::Nexttrack, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}}, + {dom::MediaControlKey::Previoustrack, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}}, + {dom::MediaControlKey::Play, {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}}, + {dom::MediaControlKey::Pause, + {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}}}; + +void MPRISServiceHandler::SetSupportedMediaKeys( + const MediaKeysArray& aSupportedKeys) { + uint32_t supportedKeys = 0; + for (const dom::MediaControlKey& key : aSupportedKeys) { + supportedKeys |= GetMediaKeyMask(key); + } + + if (mSupportedKeys == supportedKeys) { + LOGMPRIS("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) { + LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface, + it.second.property, keyIsSupported ? "true" : "false"); + EmitSupportedKeyChanged(it.first, keyIsSupported); + } + } +} + +bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const { + return mSupportedKeys & GetMediaKeyMask(aKey); +} + +bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey, + bool aSupported) const { + auto it = gKeyProperty.find(aKey); + if (it == gKeyProperty.end()) { + LOGMPRIS("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); + + LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it->second.interface, + it->second.property); + return EmitPropertiesChangedSignal(parameters); +} + +bool MPRISServiceHandler::EmitPropertiesChangedSignal( + GVariant* aParameters) const { + if (!mConnection) { + LOGMPRIS("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)) { + LOGMPRIS("Failed to emit MPRIS property changes: %s", + error ? error->message : "Unknown Error"); + if (error) { + g_error_free(error); + } + return false; + } + + return true; +} + +#undef LOGMPRIS + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/MPRISServiceHandler.h b/widget/gtk/MPRISServiceHandler.h new file mode 100644 index 0000000000..f2b171c87e --- /dev/null +++ b/widget/gtk/MPRISServiceHandler.h @@ -0,0 +1,187 @@ +/* -*- 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. + + MPRISServiceHandler(); + 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; + RefPtr<GDBusNodeInfo> mIntrospectionData; + GDBusConnection* mConnection = nullptr; + bool mInitialized = false; + nsAutoCString mIdentity; + nsAutoCString mDesktopEntry; + + // 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. + nsCString mMimeType{IMAGE_PNG}; + + // 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; + + UniquePtr<dom::FetchImageHelper> mImageFetcher; + MozPromiseRequestHolder<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(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..fe6b003bed --- /dev/null +++ b/widget/gtk/MozContainer.cpp @@ -0,0 +1,374 @@ +/* -*- 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 <stdio.h> +#include "mozilla/WidgetUtilsGtk.h" + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetLog; +# define LOGCONTAINER(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args) +#else +# define LOGCONTAINER(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); +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 (mozilla::widget::GdkIsWaylandDisplay()) { + 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)); + } + + 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->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_widget_set_redraw_on_allocate(GTK_WIDGET(container), FALSE); +#ifdef MOZ_WAYLAND + if (mozilla::widget::GdkIsWaylandDisplay()) { + moz_container_wayland_init(&container->wl_container); + } +#endif + LOGCONTAINER(("%s [%p]\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(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); + + LOGCONTAINER(("moz_container_map() [%p]", + (void*)moz_container_get_nsWindow(container))); + + 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)); + + LOGCONTAINER(("moz_container_unmap() [%p]", + (void*)moz_container_get_nsWindow(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); + + LOGCONTAINER(("moz_container_realize() [%p] GdkWindow %p\n", + (void*)moz_container_get_nsWindow(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)); + + LOGCONTAINER(("moz_container_size_allocate [%p] %d,%d -> %d x %d\n", + (void*)moz_container_get_nsWindow(MOZ_CONTAINER(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; +} + +nsWindow* moz_container_get_nsWindow(MozContainer* container) { + gpointer user_data = g_object_get_data(G_OBJECT(container), "nsWindow"); + return static_cast<nsWindow*>(user_data); +} + +#undef LOGCONTAINER diff --git a/widget/gtk/MozContainer.h b/widget/gtk/MozContainer.h new file mode 100644 index 0000000000..6d4465f189 --- /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)) + +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_unmap(GtkWidget* widget); +void moz_container_put(MozContainer* container, GtkWidget* child_widget, gint x, + gint y); +void moz_container_force_default_visual(MozContainer* container); + +class nsWindow; +nsWindow* moz_container_get_nsWindow(MozContainer* container); + +#endif /* __MOZ_CONTAINER_H__ */ diff --git a/widget/gtk/MozContainerWayland.cpp b/widget/gtk/MozContainerWayland.cpp new file mode 100644 index 0000000000..0d9aacc954 --- /dev/null +++ b/widget/gtk/MozContainerWayland.cpp @@ -0,0 +1,819 @@ +/* -*- 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/. */ +/* + * MozContainerWayland is a wrapper over MozContainer which provides + * wl_surface for MozContainer widget. + * + * The widget scheme looks like: + * + * --------------------------------------------------------- + * | mShell Gtk widget (contains wl_surface owned by Gtk+) | + * | | + * | --------------------------------------------------- | + * | | mContainer (contains wl_surface owned by Gtk+) | | + * | | | | + * | | --------------------------------------------- | | + * | | | wl_subsurface (attached to wl_surface | | | + * | | | of mContainer) | | | + * | | | | | | + * | | | | | | + * | | --------------------------------------------- | | + * | --------------------------------------------------- | + * --------------------------------------------------------- + * + * We draw to wl_subsurface owned by MozContainerWayland. + * We need to wait until wl_surface of mContainer is created + * and then we create and attach our wl_subsurface to it. + * + * First wl_subsurface creation has these steps: + * + * 1) moz_container_wayland_size_allocate() handler is called when + * mContainer size/position is known. + * It calls moz_container_wayland_surface_create_locked(), registers + * a frame callback handler + * moz_container_wayland_frame_callback_handler(). + * + * 2) moz_container_wayland_frame_callback_handler() is called + * when wl_surface owned by mozContainer is ready. + * We call initial_draw_cbs() handler and we can create our wl_subsurface + * on top of wl_surface owned by mozContainer. + * + * When MozContainer hides/show again, moz_container_wayland_size_allocate() + * handler may not be called as MozContainer size is set. So after first + * show/hide sequence use moz_container_wayland_map_event() to create + * wl_subsurface of MozContainer. + */ + +#include "MozContainer.h" + +#include <dlfcn.h> +#include <glib.h> +#include <stdio.h> +#include <wayland-egl.h> + +#include "mozilla/gfx/gfxVars.h" +#include "mozilla/StaticPrefs_widget.h" +#include "nsGtkUtils.h" +#include "nsWaylandDisplay.h" +#include "base/task.h" + +#ifdef MOZ_LOGGING + +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +# include "nsWindow.h" +extern mozilla::LazyLogModule gWidgetWaylandLog; +extern mozilla::LazyLogModule gWidgetLog; +# define LOGWAYLAND(...) \ + MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define LOGCONTAINER(...) \ + MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOGWAYLAND(...) +# define LOGCONTAINER(...) +#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_size_allocate(GtkWidget* widget, + GtkAllocation* allocation); +static bool moz_container_wayland_surface_create_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container); +static void moz_container_wayland_set_opaque_region_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container); + +// Lock mozcontainer and get wayland surface of it. You need to pair with +// moz_container_wayland_surface_unlock() even +// if moz_container_wayland_surface_lock() fails and returns nullptr. +static struct wl_surface* moz_container_wayland_surface_lock( + MozContainer* container); +static void moz_container_wayland_surface_unlock(MozContainer* container, + struct wl_surface** surface); + +MozContainerSurfaceLock::MozContainerSurfaceLock(MozContainer* aContainer) { + mContainer = aContainer; + mSurface = moz_container_wayland_surface_lock(aContainer); +} +MozContainerSurfaceLock::~MozContainerSurfaceLock() { + moz_container_wayland_surface_unlock(mContainer, &mSurface); +} +struct wl_surface* MozContainerSurfaceLock::GetSurface() { return mSurface; } + +// Imlemented in MozContainer.cpp +void moz_container_realize(GtkWidget* widget); + +// Invalidate gtk wl_surface to commit changes to wl_subsurface. +// wl_subsurface changes are effective when parent surface is commited. +static void moz_container_wayland_invalidate(MozContainer* container) { + LOGWAYLAND("moz_container_wayland_invalidate [%p]\n", + (void*)moz_container_get_nsWindow(container)); + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + if (!window) { + LOGWAYLAND(" Failed - missing GdkWindow!\n"); + return; + } + gdk_window_invalidate_rect(window, nullptr, true); +} + +// Route input to parent wl_surface owned by Gtk+ so we get input +// events from Gtk+. +static void moz_container_clear_input_region(MozContainer* container) { + struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor(); + MozContainerWayland* wl_container = &container->wl_container; + wl_region* region = wl_compositor_create_region(compositor); + wl_surface_set_input_region(wl_container->surface, region); + wl_region_destroy(region); +} + +static void moz_container_wayland_move_locked(const MutexAutoLock& aProofOfLock, + MozContainer* container, int dx, + int dy) { + LOGCONTAINER("moz_container_wayland_move [%p] %d,%d\n", + (void*)moz_container_get_nsWindow(container), dx, dy); + + MozContainerWayland* wl_container = &container->wl_container; + if (!wl_container->subsurface || (wl_container->subsurface_dx == dx && + wl_container->subsurface_dy == dy)) { + return; + } + + wl_container->subsurface_dx = dx; + wl_container->subsurface_dy = dy; + wl_subsurface_set_position(wl_container->subsurface, + wl_container->subsurface_dx, + wl_container->subsurface_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, + nsIntSize aSize) { + MozContainerWayland* wl_container = &container->wl_container; + MutexAutoLock lock(*wl_container->container_lock); + if (wl_container->eglwindow) { + wl_egl_window_resize(wl_container->eglwindow, aSize.width, aSize.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->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->viewport = nullptr; + container->ready_to_draw = false; + container->opaque_region_needs_updates = false; + container->opaque_region_corner_radius = 0; + container->opaque_region_used = false; + container->subsurface_dx = 0; + container->subsurface_dy = 0; + container->before_first_size_alloc = true; + container->buffer_scale = 1; + container->initial_draw_cbs.clear(); + container->container_lock = new mozilla::Mutex("MozContainer lock"); + container->commit_to_parent = false; + container->waiting_to_show = false; +} + +static void moz_container_wayland_destroy(GtkWidget* widget) { + MozContainerWayland* container = &MOZ_CONTAINER(widget)->wl_container; + if (!container->container_lock) { + // moz_container_wayland_init was not called - it's a hidden container. + return; + } + moz_container_wayland_clear_initial_draw_callback(MOZ_CONTAINER(widget)); + delete container->container_lock; + container->container_lock = nullptr; +} + +void moz_container_wayland_add_initial_draw_callback_locked( + MozContainer* container, const std::function<void(void)>& initial_draw_cb) { + MozContainerWayland* wl_container = &MOZ_CONTAINER(container)->wl_container; + + if (wl_container->ready_to_draw && !wl_container->surface) { + NS_WARNING( + "moz_container_wayland_add_or_fire_initial_draw_callback:" + " ready to draw without wayland surface!"); + } + MOZ_DIAGNOSTIC_ASSERT(!wl_container->ready_to_draw || !wl_container->surface); + wl_container->initial_draw_cbs.push_back(initial_draw_cb); +} + +void moz_container_wayland_add_or_fire_initial_draw_callback( + MozContainer* container, const std::function<void(void)>& initial_draw_cb) { + MozContainerWayland* wl_container = &MOZ_CONTAINER(container)->wl_container; + { + MutexAutoLock lock(*container->wl_container.container_lock); + if (wl_container->ready_to_draw && !wl_container->surface) { + NS_WARNING( + "moz_container_wayland_add_or_fire_initial_draw_callback: ready to " + "draw " + "without wayland surface!"); + } + if (!wl_container->ready_to_draw || !wl_container->surface) { + wl_container->initial_draw_cbs.push_back(initial_draw_cb); + return; + } + } + + // We're ready to draw as + // wl_container->ready_to_draw && wl_container->surface + // call the callback directly instead of store them. + initial_draw_cb(); +} + +static void moz_container_wayland_clear_initial_draw_callback_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container) { + MozContainerWayland* wl_container = &MOZ_CONTAINER(container)->wl_container; + MozClearPointer(wl_container->frame_callback_handler, wl_callback_destroy); + wl_container->initial_draw_cbs.clear(); +} + +void moz_container_wayland_clear_initial_draw_callback( + MozContainer* container) { + MutexAutoLock lock(*container->wl_container.container_lock); + moz_container_wayland_clear_initial_draw_callback_locked(lock, container); +} + +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_get_nsWindow(MOZ_CONTAINER(data)), + (void*)wl_container->frame_callback_handler, wl_container->ready_to_draw, + wl_container->initial_draw_cbs.size()); + + std::vector<std::function<void(void)>> cbs; + { + // Protect mozcontainer internals changes by container_lock. + MutexAutoLock lock(*wl_container->container_lock); + MozClearPointer(wl_container->frame_callback_handler, wl_callback_destroy); + // It's possible that container is already unmapped so quit in such case. + if (!wl_container->surface) { + LOGWAYLAND(" container is unmapped, quit."); + if (!wl_container->initial_draw_cbs.empty()) { + NS_WARNING("Unmapping MozContainer with active draw callback!"); + wl_container->initial_draw_cbs.clear(); + } + return; + } + if (wl_container->ready_to_draw) { + return; + } + wl_container->ready_to_draw = true; + cbs = std::move(wl_container->initial_draw_cbs); + } + + // Call the callbacks registered by + // moz_container_wayland_add_or_fire_initial_draw_callback(). + // and we can't do that under mozcontainer lock. + for (auto const& cb : cbs) { + cb(); + } +} + +static const struct wl_callback_listener moz_container_frame_listener = { + moz_container_wayland_frame_callback_handler}; + +static void after_frame_clock_after_paint(GdkFrameClock* clock, + MozContainer* container) { + MozContainerSurfaceLock lock(container); + struct wl_surface* surface = lock.GetSurface(); + if (surface) { + wl_surface_commit(surface); + } +} + +static bool moz_gdk_wayland_window_add_frame_callback_surface_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container) { + static auto sGdkWaylandWindowAddCallbackSurface = + (void (*)(GdkWindow*, struct wl_surface*))dlsym( + RTLD_DEFAULT, "gdk_wayland_window_add_frame_callback_surface"); + + if (!StaticPrefs::widget_wayland_opaque_region_enabled_AtStartup() || + !sGdkWaylandWindowAddCallbackSurface) { + return false; + } + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + MozContainerWayland* wl_container = &container->wl_container; + + sGdkWaylandWindowAddCallbackSurface(window, wl_container->surface); + + GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window); + g_signal_connect_after(frame_clock, "after-paint", + G_CALLBACK(after_frame_clock_after_paint), container); + return true; +} + +static void moz_gdk_wayland_window_remove_frame_callback_surface_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container) { + static auto sGdkWaylandWindowRemoveCallbackSurface = + (void (*)(GdkWindow*, struct wl_surface*))dlsym( + RTLD_DEFAULT, "gdk_wayland_window_remove_frame_callback_surface"); + + if (!sGdkWaylandWindowRemoveCallbackSurface) { + return; + } + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + MozContainerWayland* wl_container = &container->wl_container; + + if (wl_container->surface) { + sGdkWaylandWindowRemoveCallbackSurface(window, wl_container->surface); + } + + GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window); + g_signal_handlers_disconnect_by_func( + frame_clock, FuncToGpointer(after_frame_clock_after_paint), container); +} + +void moz_container_wayland_unmap(GtkWidget* widget) { + MozContainer* container = MOZ_CONTAINER(widget); + MozContainerWayland* wl_container = &container->wl_container; + MutexAutoLock lock(*wl_container->container_lock); + + LOGCONTAINER("%s [%p]\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(container)); + + moz_container_wayland_clear_initial_draw_callback_locked(lock, container); + + if (wl_container->opaque_region_used) { + moz_gdk_wayland_window_remove_frame_callback_surface_locked(lock, + container); + } + if (wl_container->commit_to_parent) { + wl_container->surface = nullptr; + } + + MozClearPointer(wl_container->eglwindow, wl_egl_window_destroy); + MozClearPointer(wl_container->subsurface, wl_subsurface_destroy); + MozClearPointer(wl_container->surface, wl_surface_destroy); + MozClearPointer(wl_container->viewport, wp_viewport_destroy); + + wl_container->ready_to_draw = false; + wl_container->buffer_scale = 1; +} + +static gboolean moz_container_wayland_map_event(GtkWidget* widget, + GdkEventAny* event) { + MozContainerWayland* wl_container = &MOZ_CONTAINER(widget)->wl_container; + + LOGCONTAINER("%s [%p]\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget))); + + // We need to mark MozContainer as mapped to make sure + // moz_container_wayland_unmap() is called on hide/withdraw. + gtk_widget_set_mapped(widget, TRUE); + + // Make sure we're on main thread as we can't lock mozContainer here + // due to moz_container_wayland_add_or_fire_initial_draw_callback() call + // below. + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + // Set waiting_to_show flag. It means the mozcontainer is cofigured/mapped + // and it's supposed to be visible. *But* it's really visible when we get + // moz_container_wayland_add_or_fire_initial_draw_callback() which means + // wayland compositor makes it live. + wl_container->waiting_to_show = true; + MozContainer* container = MOZ_CONTAINER(widget); + moz_container_wayland_add_or_fire_initial_draw_callback( + container, [container]() -> void { + LOGCONTAINER( + "[%p] moz_container_wayland_add_or_fire_initial_draw_callback set " + "visible", + moz_container_get_nsWindow(container)); + moz_container_wayland_clear_waiting_to_show_flag(container); + }); + + MutexAutoLock lock(*wl_container->container_lock); + + // Don't create wl_subsurface in map_event when it's already created or + // if we create it for the first time. + if (wl_container->ready_to_draw || wl_container->before_first_size_alloc) { + return FALSE; + } + + if (!wl_container->surface) { + if (!moz_container_wayland_surface_create_locked(lock, + MOZ_CONTAINER(widget))) { + return FALSE; + } + } + + moz_container_wayland_set_scale_factor_locked(lock, MOZ_CONTAINER(widget)); + moz_container_wayland_set_opaque_region_locked(lock, MOZ_CONTAINER(widget)); + moz_container_clear_input_region(MOZ_CONTAINER(widget)); + moz_container_wayland_invalidate(MOZ_CONTAINER(widget)); + return FALSE; +} + +void moz_container_wayland_map(GtkWidget* widget) { + LOGCONTAINER("%s [%p]\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(MOZ_CONTAINER(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)); + } +} + +void moz_container_wayland_size_allocate(GtkWidget* widget, + GtkAllocation* allocation) { + MozContainer* container; + GtkAllocation tmp_allocation; + + g_return_if_fail(IS_MOZ_CONTAINER(widget)); + + LOGCONTAINER("moz_container_wayland_size_allocate [%p] %d,%d -> %d x %d\n", + (void*)moz_container_get_nsWindow(MOZ_CONTAINER(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. + MutexAutoLock lock(*container->wl_container.container_lock); + if (!container->wl_container.surface) { + if (!moz_container_wayland_surface_create_locked(lock, container)) { + return; + } + } + moz_container_wayland_set_scale_factor_locked(lock, container); + moz_container_wayland_set_opaque_region_locked(lock, container); + moz_container_wayland_move_locked(lock, container, allocation->x, + allocation->y); + moz_container_clear_input_region(container); + moz_container_wayland_invalidate(MOZ_CONTAINER(widget)); + container->wl_container.before_first_size_alloc = false; + } +} + +static wl_region* moz_container_wayland_create_opaque_region( + int aX, int aY, int aWidth, int aHeight, int aCornerRadius) { + struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor(); + wl_region* region = wl_compositor_create_region(compositor); + wl_region_add(region, aX, aY, aWidth, aHeight); + if (aCornerRadius) { + wl_region_subtract(region, aX, aY, aCornerRadius, aCornerRadius); + wl_region_subtract(region, aX + aWidth - aCornerRadius, aY, aCornerRadius, + aCornerRadius); + } + return region; +} + +static void moz_container_wayland_set_opaque_region_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + + if (!wl_container->opaque_region_needs_updates) { + return; + } + + if (!wl_container->opaque_region_used) { + wl_container->opaque_region_needs_updates = false; + 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_corner_radius); + wl_surface_set_opaque_region(wl_container->surface, region); + wl_region_destroy(region); + wl_container->opaque_region_needs_updates = false; +} + +static void moz_container_wayland_set_opaque_region(MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + MutexAutoLock lock(*wl_container->container_lock); + if (wl_container->surface) { + moz_container_wayland_set_opaque_region_locked(lock, container); + } +} + +static void moz_container_wayland_surface_set_scale_locked( + const MutexAutoLock& aProofOfLock, MozContainerWayland* wl_container, + int scale) { + if (wl_container->buffer_scale == scale) { + return; + } + + LOGCONTAINER("%s scale %d\n", __FUNCTION__, scale); + + // There is a chance that the attached wl_buffer has not yet been doubled + // on the main thread when scale factor changed to 2. This leads to + // crash with the following message: + // Buffer size (AxB) must be an integer multiple of the buffer_scale (2) + // Removing the possibly wrong wl_buffer to prevent that crash: + wl_surface_attach(wl_container->surface, nullptr, 0, 0); + wl_surface_set_buffer_scale(wl_container->surface, scale); + wl_container->buffer_scale = scale; +} + +void moz_container_wayland_set_scale_factor_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container) { + if (gfx::gfxVars::UseWebRenderCompositor()) { + // the compositor backend handles scaling itself + return; + } + + MozContainerWayland* wl_container = &container->wl_container; + wl_container->container_lock->AssertCurrentThreadOwns(); + + nsWindow* window = moz_container_get_nsWindow(container); + MOZ_DIAGNOSTIC_ASSERT(window); + if (window->UseFractionalScale()) { + if (!wl_container->viewport) { + wl_container->viewport = wp_viewporter_get_viewport( + WaylandDisplayGet()->GetViewporter(), wl_container->surface); + } + + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container)); + wp_viewport_set_destination(wl_container->viewport, + gdk_window_get_width(gdkWindow), + gdk_window_get_height(gdkWindow)); + } else { + moz_container_wayland_surface_set_scale_locked( + aProofOfLock, wl_container, window->GdkCeiledScaleFactor()); + } +} + +void moz_container_wayland_set_scale_factor(MozContainer* container) { + MutexAutoLock lock(*container->wl_container.container_lock); + if (container->wl_container.surface) { + moz_container_wayland_set_scale_factor_locked(lock, container); + } +} + +bool moz_container_wayland_size_matches_scale_factor_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container, int aWidth, + int aHeight) { + return aWidth % container->wl_container.buffer_scale == 0 && + aHeight % container->wl_container.buffer_scale == 0; +} + +static bool moz_container_wayland_surface_create_locked( + const MutexAutoLock& aProofOfLock, MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + + LOGWAYLAND("%s [%p]\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(container)); + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + MOZ_DIAGNOSTIC_ASSERT(window); + + wl_surface* parent_surface = gdk_wayland_window_get_wl_surface(window); + if (!parent_surface) { + LOGWAYLAND(" Failed - missing parent surface!"); + return false; + } + LOGWAYLAND(" gtk wl_surface %p ID %d\n", (void*)parent_surface, + wl_proxy_get_id((struct wl_proxy*)parent_surface)); + + if (wl_container->commit_to_parent) { + LOGWAYLAND(" commit to parent"); + wl_container->surface = parent_surface; + NS_DispatchToCurrentThread(NewRunnableFunction( + "moz_container_wayland_frame_callback_handler", + &moz_container_wayland_frame_callback_handler, container, nullptr, 0)); + return true; + } + + // Available as of GTK 3.8+ + struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor(); + wl_container->surface = wl_compositor_create_surface(compositor); + if (!wl_container->surface) { + LOGWAYLAND(" Failed - can't create surface!"); + return false; + } + + wl_container->subsurface = + wl_subcompositor_get_subsurface(WaylandDisplayGet()->GetSubcompositor(), + wl_container->surface, parent_surface); + if (!wl_container->subsurface) { + MozClearPointer(wl_container->surface, wl_surface_destroy); + LOGWAYLAND(" Failed - can't create sub-surface!"); + return false; + } + wl_subsurface_set_desync(wl_container->subsurface); + + // Try to guess subsurface offset to avoid potential flickering. + int dx, dy; + if (moz_container_get_nsWindow(container)->GetCSDDecorationOffset(&dx, &dy)) { + wl_container->subsurface_dx = dx; + wl_container->subsurface_dy = dy; + wl_subsurface_set_position(wl_container->subsurface, dx, dy); + LOGWAYLAND(" guessing subsurface position %d %d\n", dx, dy); + } + + // If there's pending frame callback it's for wrong parent surface, + // so delete it. + if (wl_container->frame_callback_handler) { + MozClearPointer(wl_container->frame_callback_handler, wl_callback_destroy); + } + wl_container->frame_callback_handler = wl_surface_frame(parent_surface); + wl_callback_add_listener(wl_container->frame_callback_handler, + &moz_container_frame_listener, container); + LOGWAYLAND( + " created frame callback ID %d\n", + wl_proxy_get_id((struct wl_proxy*)wl_container->frame_callback_handler)); + + wl_surface_commit(wl_container->surface); + wl_display_flush(WaylandDisplayGet()->GetDisplay()); + + wl_container->opaque_region_used = + moz_gdk_wayland_window_add_frame_callback_surface_locked(aProofOfLock, + container); + + LOGWAYLAND(" created surface %p ID %d\n", (void*)wl_container->surface, + wl_proxy_get_id((struct wl_proxy*)wl_container->surface)); + return true; +} + +struct wl_surface* moz_container_wayland_surface_lock(MozContainer* container) + MOZ_NO_THREAD_SAFETY_ANALYSIS { + // LOGWAYLAND("%s [%p] surface %p ready_to_draw %d\n", __FUNCTION__, + // (void*)container, (void*)container->wl_container.surface, + // container->wl_container.ready_to_draw); + container->wl_container.container_lock->Lock(); + if (!container->wl_container.surface || + !container->wl_container.ready_to_draw) { + return nullptr; + } + return container->wl_container.surface; +} + +void moz_container_wayland_surface_unlock(MozContainer* container, + struct wl_surface** surface) + MOZ_NO_THREAD_SAFETY_ANALYSIS { + // Temporarily disabled to avoid log noise + // LOGWAYLAND("%s [%p] surface %p\n", __FUNCTION__, (void*)container, + // (void*)container->wl_container.surface); + if (*surface) { + *surface = nullptr; + } + container->wl_container.container_lock->Unlock(); +} + +bool moz_container_wayland_egl_window_needs_size_update(MozContainer* container, + nsIntSize aSize, + int aScale) { + MozContainerWayland* wl_container = &container->wl_container; + if (!wl_container->eglwindow) { + return false; + } + if (wl_container->buffer_scale != aScale) { + return true; + } + nsIntSize recentSize; + wl_egl_window_get_attached_size(wl_container->eglwindow, &recentSize.width, + &recentSize.height); + return aSize != recentSize; +} + +struct wl_egl_window* moz_container_wayland_get_egl_window( + MozContainer* container, double scale) { + MozContainerWayland* wl_container = &container->wl_container; + + LOGCONTAINER("%s [%p] eglwindow %p scale %d\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(container), + (void*)wl_container->eglwindow, (int)scale); + + MutexAutoLock lock(*wl_container->container_lock); + if (!wl_container->surface || !wl_container->ready_to_draw) { + LOGCONTAINER( + " quit, wl_container->surface %p wl_container->ready_to_draw %d\n", + wl_container->surface, wl_container->ready_to_draw); + return nullptr; + } + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container)); + nsIntSize requestedSize((int)round(gdk_window_get_width(window) * scale), + (int)round(gdk_window_get_height(window) * scale)); + + if (!wl_container->eglwindow) { + wl_container->eglwindow = wl_egl_window_create( + wl_container->surface, requestedSize.width, requestedSize.height); + + LOGCONTAINER("%s [%p] created eglwindow %p size %d x %d (with scale %f)\n", + __FUNCTION__, (void*)moz_container_get_nsWindow(container), + (void*)wl_container->eglwindow, requestedSize.width, + requestedSize.height, scale); + } else { + nsIntSize recentSize; + wl_egl_window_get_attached_size(wl_container->eglwindow, &recentSize.width, + &recentSize.height); + if (requestedSize != recentSize) { + LOGCONTAINER("%s [%p] resized to %d x %d (with scale %f)\n", __FUNCTION__, + (void*)moz_container_get_nsWindow(container), + requestedSize.width, requestedSize.height, scale); + wl_egl_window_resize(wl_container->eglwindow, requestedSize.width, + requestedSize.height, 0, 0); + } + } + moz_container_wayland_surface_set_scale_locked(lock, wl_container, + static_cast<int>(scale)); + return wl_container->eglwindow; +} + +gboolean moz_container_wayland_has_egl_window(MozContainer* container) { + return container->wl_container.eglwindow != nullptr; +} + +void moz_container_wayland_update_opaque_region(MozContainer* container, + int corner_radius) { + MozContainerWayland* wl_container = &container->wl_container; + wl_container->opaque_region_needs_updates = true; + wl_container->opaque_region_corner_radius = corner_radius; + + // 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) { + MozContainerWayland* wl_container = &container->wl_container; + MutexAutoLock lock(*wl_container->container_lock); + return wl_container->ready_to_draw; +} + +double moz_container_wayland_get_scale(MozContainer* container) { + nsWindow* window = moz_container_get_nsWindow(container); + return window ? window->FractionalScaleFactor() : 1; +} + +void moz_container_wayland_set_commit_to_parent(MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + MOZ_DIAGNOSTIC_ASSERT(!wl_container->surface); + wl_container->commit_to_parent = true; +} + +bool moz_container_wayland_is_commiting_to_parent(MozContainer* container) { + MozContainerWayland* wl_container = &container->wl_container; + return wl_container->commit_to_parent; +} + +bool moz_container_wayland_is_waiting_to_show(MozContainer* container) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + return container->wl_container.waiting_to_show; +} + +void moz_container_wayland_clear_waiting_to_show_flag(MozContainer* container) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + container->wl_container.waiting_to_show = false; +} diff --git a/widget/gtk/MozContainerWayland.h b/widget/gtk/MozContainerWayland.h new file mode 100644 index 0000000000..369d40a552 --- /dev/null +++ b/widget/gtk/MozContainerWayland.h @@ -0,0 +1,108 @@ +/* -*- 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" +#include "WindowSurface.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; + struct wp_viewport* viewport; + gboolean opaque_region_needs_updates; + int opaque_region_corner_radius; + gboolean opaque_region_used; + gboolean ready_to_draw; + gboolean commit_to_parent; + gboolean before_first_size_alloc; + gboolean waiting_to_show; + int buffer_scale; + 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; + +class MozContainerSurfaceLock { + MozContainer* mContainer; + struct wl_surface* mSurface; + + public: + explicit MozContainerSurfaceLock(MozContainer* aContainer); + ~MozContainerSurfaceLock(); + struct wl_surface* GetSurface(); +}; + +void moz_container_wayland_class_init(MozContainerClass* klass); +void moz_container_wayland_init(MozContainerWayland* container); +void moz_container_wayland_unmap(GtkWidget* widget); + +struct wl_egl_window* moz_container_wayland_get_egl_window( + MozContainer* container, double scale); + +gboolean moz_container_wayland_has_egl_window(MozContainer* container); +void moz_container_wayland_egl_window_set_size(MozContainer* container, + nsIntSize aSize); +bool moz_container_wayland_egl_window_needs_size_update(MozContainer* container, + nsIntSize aSize, + int scale); +void moz_container_wayland_set_scale_factor(MozContainer* container); +void moz_container_wayland_set_scale_factor_locked( + const mozilla::MutexAutoLock& aProofOfLock, MozContainer* container); +bool moz_container_wayland_size_matches_scale_factor_locked( + const mozilla::MutexAutoLock& aProofOfLock, MozContainer* container, + int aWidth, int aHeight); + +void moz_container_wayland_add_initial_draw_callback_locked( + MozContainer* container, const std::function<void(void)>& initial_draw_cb); +void moz_container_wayland_add_or_fire_initial_draw_callback( + MozContainer* container, const std::function<void(void)>& initial_draw_cb); +void moz_container_wayland_clear_initial_draw_callback(MozContainer* container); + +wl_surface* moz_gtk_widget_get_wl_surface(GtkWidget* aWidget); +void moz_container_wayland_update_opaque_region(MozContainer* container, + int corner_radius); +gboolean moz_container_wayland_can_draw(MozContainer* container); +double moz_container_wayland_get_scale(MozContainer* container); +void moz_container_wayland_set_commit_to_parent(MozContainer* container); +bool moz_container_wayland_is_commiting_to_parent(MozContainer* container); +bool moz_container_wayland_is_waiting_to_show(MozContainer* container); +void moz_container_wayland_clear_waiting_to_show_flag(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..f5b15367a6 --- /dev/null +++ b/widget/gtk/NativeKeyBindings.cpp @@ -0,0 +1,527 @@ +/* -*- 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/Maybe.h" +#include "mozilla/NativeKeyBindingsType.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include "mozilla/WritingModes.h" + +#include "NativeKeyBindings.h" +#include "nsString.h" +#include "nsGtkKeyUtils.h" + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <gdk/gdkkeysyms-compat.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* aWidget, gboolean aSelect, + gpointer aUserData) { + // We don't support "Unselect All" command. + // Note that if we'd support it, `Ctrl-Shift-a` will be mapped to it and + // overrides open `about:addons` shortcut. + if (aSelect) { + AddCommand(Command::SelectAll); + } + g_signal_stop_emission_by_name(aWidget, "select_all"); + // Although we prevent the default of `GtkTExtView` with + // `g_signal_stop_emission_by_name`, but `gHandled` is used for asserting + // if it does not match with the emptiness of the command array. + // Therefore, we should not set it to `true` if we don't add a command. + gHandled |= aSelect; +} + +NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr; +NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr; + +// static +NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) { + switch (aType) { + case NativeKeyBindingsType::SingleLineEditor: + 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 NativeKeyBindingsType::MultiLineEditor: + case NativeKeyBindingsType::RichTextEditor: + 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 NativeKeyBindingsType::SingleLineEditor: + mNativeTarget = gtk_entry_new(); + break; + default: + mNativeTarget = gtk_text_view_new(); + 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, + const Maybe<WritingMode>& aWritingMode, + nsTArray<CommandInt>& aCommands) { + MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests); + MOZ_ASSERT(aCommands.IsEmpty()); + + // It must be a DOM event dispached by chrome script. + if (!aEvent.mNativeKeyEvent) { + return; + } + + guint keyval; + if (aEvent.mCharCode) { + keyval = gdk_unicode_to_keyval(aEvent.mCharCode); + } else if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() && + aWritingMode.ref().IsVertical()) { + // TODO: Use KeyNameIndex rather than legacy keyCode. + uint32_t remappedGeckoKeyCode = + aEvent.GetRemappedKeyCode(aWritingMode.ref()); + switch (remappedGeckoKeyCode) { + case NS_VK_UP: + keyval = GDK_Up; + break; + case NS_VK_DOWN: + keyval = GDK_Down; + break; + case NS_VK_LEFT: + keyval = GDK_Left; + break; + case NS_VK_RIGHT: + keyval = GDK_Right; + break; + default: + MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key"); + return; + } + } 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; + } + } + } + + // If the key event does not cause any commands, and we're for single line + // editor, let's check whether the key combination is for "select-all" in + // GtkTextView because the signal is not supported by GtkEntry. + if (aCommands.IsEmpty() && this == sInstanceForSingleLineEditor && + StaticPrefs::ui_key_use_select_all_in_single_line_editor()) { + if (NativeKeyBindings* bindingsForMultilineEditor = + GetInstance(NativeKeyBindingsType::MultiLineEditor)) { + bindingsForMultilineEditor->GetEditCommands(aEvent, aWritingMode, + aCommands); + if (aCommands.Length() == 1u && + aCommands[0u] == static_cast<CommandInt>(Command::SelectAll)) { + return; + } + aCommands.Clear(); + } + } + + /* + 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; + + return gHandled; +} + +// static +void NativeKeyBindings::GetEditCommandsForTests( + NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent, + const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) { + MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted()); + + if (aEvent.IsAlt() || aEvent.IsMeta() || aEvent.IsOS()) { + return; + } + + static const size_t kBackward = 0; + static const size_t kForward = 1; + const size_t extentSelection = aEvent.IsShift() ? 1 : 0; + // https://github.com/GNOME/gtk/blob/1f141c19533f4b3f397c3959ade673ce243b6138/gtk/gtktext.c#L1289 + // https://github.com/GNOME/gtk/blob/c5dd34344f0c660ceffffb3bf9da43c263db16e1/gtk/gtktextview.c#L1534 + Command command = Command::DoNothing; + const KeyNameIndex remappedKeyNameIndex = + aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref()) + : aEvent.mKeyNameIndex; + switch (remappedKeyNameIndex) { + case KEY_NAME_INDEX_USE_STRING: + switch (aEvent.PseudoCharCode()) { + case 'a': + case 'A': + if (aEvent.IsControl()) { + command = Command::SelectAll; + } + break; + case 'c': + case 'C': + if (aEvent.IsControl() && !aEvent.IsShift()) { + command = Command::Copy; + } + break; + case 'u': + case 'U': + if (aType == NativeKeyBindingsType::SingleLineEditor && + aEvent.IsControl() && !aEvent.IsShift()) { + command = sDeleteCommands[GTK_DELETE_PARAGRAPH_ENDS][kBackward]; + } + break; + case 'v': + case 'V': + if (aEvent.IsControl() && !aEvent.IsShift()) { + command = Command::Paste; + } + break; + case 'x': + case 'X': + if (aEvent.IsControl() && !aEvent.IsShift()) { + command = Command::Cut; + } + break; + case '/': + if (aEvent.IsControl() && !aEvent.IsShift()) { + command = Command::SelectAll; + } + break; + default: + break; + } + break; + case KEY_NAME_INDEX_Insert: + if (aEvent.IsControl() && !aEvent.IsShift()) { + command = Command::Copy; + } else if (aEvent.IsShift() && !aEvent.IsControl()) { + command = Command::Paste; + } + break; + case KEY_NAME_INDEX_Delete: + if (aEvent.IsShift()) { + command = Command::Cut; + break; + } + [[fallthrough]]; + case KEY_NAME_INDEX_Backspace: { + const size_t direction = + remappedKeyNameIndex == KEY_NAME_INDEX_Delete ? kForward : kBackward; + const GtkDeleteType amount = + aEvent.IsControl() && aEvent.IsShift() + ? GTK_DELETE_PARAGRAPH_ENDS + // FYI: Shift key for Backspace is ignored to help mis-typing. + : (aEvent.IsControl() ? GTK_DELETE_WORD_ENDS : GTK_DELETE_CHARS); + command = sDeleteCommands[amount][direction]; + break; + } + case KEY_NAME_INDEX_ArrowLeft: + case KEY_NAME_INDEX_ArrowRight: { + const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowRight + ? kForward + : kBackward; + const GtkMovementStep amount = aEvent.IsControl() + ? GTK_MOVEMENT_WORDS + : GTK_MOVEMENT_VISUAL_POSITIONS; + command = sMoveCommands[amount][extentSelection][direction]; + break; + } + case KEY_NAME_INDEX_ArrowUp: + case KEY_NAME_INDEX_ArrowDown: { + const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowDown + ? kForward + : kBackward; + const GtkMovementStep amount = aEvent.IsControl() + ? GTK_MOVEMENT_PARAGRAPHS + : GTK_MOVEMENT_DISPLAY_LINES; + command = sMoveCommands[amount][extentSelection][direction]; + break; + } + case KEY_NAME_INDEX_Home: + case KEY_NAME_INDEX_End: { + const size_t direction = + remappedKeyNameIndex == KEY_NAME_INDEX_End ? kForward : kBackward; + const GtkMovementStep amount = aEvent.IsControl() + ? GTK_MOVEMENT_BUFFER_ENDS + : GTK_MOVEMENT_DISPLAY_LINE_ENDS; + command = sMoveCommands[amount][extentSelection][direction]; + break; + } + case KEY_NAME_INDEX_PageUp: + case KEY_NAME_INDEX_PageDown: { + const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_PageDown + ? kForward + : kBackward; + const GtkMovementStep amount = aEvent.IsControl() + ? GTK_MOVEMENT_HORIZONTAL_PAGES + : GTK_MOVEMENT_PAGES; + command = sMoveCommands[amount][extentSelection][direction]; + break; + } + default: + break; + } + if (command != Command::DoNothing) { + aCommands.AppendElement(static_cast<CommandInt>(command)); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/NativeKeyBindings.h b/widget/gtk/NativeKeyBindings.h new file mode 100644 index 0000000000..1d0c528621 --- /dev/null +++ b/widget/gtk/NativeKeyBindings.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 NativeKeyBindings_h +#define NativeKeyBindings_h + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsIWidget.h" + +#include <glib.h> // for guint + +using GtkWidget = struct _GtkWidget; + +namespace mozilla { +enum class NativeKeyBindingsType : uint8_t; + +class WritingMode; +template <typename T> +class Maybe; + +namespace widget { + +class NativeKeyBindings final { + public: + static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType); + static void Shutdown(); + + /** + * GetEditCommandsForTests() returns commands performed in native widget + * in typical environment. I.e., this does NOT refer customized shortcut + * key mappings of the environment. + */ + static void GetEditCommandsForTests(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + const Maybe<WritingMode>& aWritingMode, + nsTArray<CommandInt>& aCommands); + + void Init(NativeKeyBindingsType aType); + + void GetEditCommands(const WidgetKeyboardEvent& aEvent, + const Maybe<WritingMode>& aWritingMode, + 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 // NativeKeyBindings_h diff --git a/widget/gtk/NativeMenuGtk.cpp b/widget/gtk/NativeMenuGtk.cpp new file mode 100644 index 0000000000..9d413d475e --- /dev/null +++ b/widget/gtk/NativeMenuGtk.cpp @@ -0,0 +1,424 @@ +/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "NativeMenuGtk.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/XULCommandEvent.h" +#include "mozilla/WidgetUtilsGtk.h" +#include "mozilla/EventDispatcher.h" +#include "nsPresContext.h" +#include "nsIWidget.h" +#include "nsWindow.h" +#include "nsStubMutationObserver.h" +#include "mozilla/dom/Element.h" +#include "mozilla/StaticPrefs_widget.h" + +#include <dlfcn.h> +#include <gtk/gtk.h> + +namespace mozilla::widget { + +using GtkMenuPopupAtRect = void (*)(GtkMenu* menu, GdkWindow* rect_window, + const GdkRectangle* rect, + GdkGravity rect_anchor, + GdkGravity menu_anchor, + const GdkEvent* trigger_event); + +static bool IsDisabled(const dom::Element& aElement) { + return aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters) || + aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters); +} +static bool NodeIsRelevant(const nsINode& aNode) { + return aNode.IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuseparator, + nsGkAtoms::menuitem, nsGkAtoms::menugroup); +} + +// If this is a radio / checkbox menuitem, get the current value. +static Maybe<bool> GetChecked(const dom::Element& aMenuItem) { + static dom::Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox, + nsGkAtoms::radio, nullptr}; + switch (aMenuItem.FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, strings, + eCaseMatters)) { + case 0: + break; + case 1: + break; + default: + return Nothing(); + } + + return Some(aMenuItem.AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, + nsGkAtoms::_true, eCaseMatters)); +} + +struct Actions { + RefPtr<GSimpleActionGroup> mGroup; + size_t mNextActionIndex = 0; + + nsPrintfCString Register(const dom::Element&, bool aForSubmenu); + void Clear(); +}; + +static MOZ_CAN_RUN_SCRIPT void ActivateItem(dom::Element& aElement) { + if (Maybe<bool> checked = GetChecked(aElement)) { + if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, + nsGkAtoms::_false, eCaseMatters)) { + bool newValue = !*checked; + if (newValue) { + aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, + true); + } else { + aElement.UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true); + } + } + } + + RefPtr doc = aElement.OwnerDoc(); + RefPtr event = new dom::XULCommandEvent(doc, doc->GetPresContext(), nullptr); + IgnoredErrorResult rv; + event->InitCommandEvent(u"command"_ns, true, true, + nsGlobalWindowInner::Cast(doc->GetInnerWindow()), 0, + /* ctrlKey = */ false, /* altKey = */ false, + /* shiftKey = */ false, /* cmdKey = */ false, + /* button = */ MouseButton::ePrimary, nullptr, 0, rv); + if (MOZ_UNLIKELY(rv.Failed())) { + return; + } + aElement.DispatchEvent(*event); +} + +static MOZ_CAN_RUN_SCRIPT void ActivateSignal(GSimpleAction* aAction, + GVariant* aParam, + gpointer aUserData) { + RefPtr element = static_cast<dom::Element*>(aUserData); + ActivateItem(*element); +} + +static MOZ_CAN_RUN_SCRIPT void FireEvent(dom::Element* aTarget, + EventMessage aPopupMessage) { + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, aPopupMessage, nullptr, WidgetMouseEvent::eReal); + EventDispatcher::Dispatch(aTarget, nullptr, &event, nullptr, &status); +} + +static MOZ_CAN_RUN_SCRIPT void ChangeStateSignal(GSimpleAction* aAction, + GVariant* aParam, + gpointer aUserData) { + // TODO: Fire events when safe. These run at a bad time for now. + static constexpr bool kEnabled = false; + if (!kEnabled) { + return; + } + const bool open = g_variant_get_boolean(aParam); + RefPtr popup = static_cast<dom::Element*>(aUserData); + if (open) { + FireEvent(popup, eXULPopupShowing); + FireEvent(popup, eXULPopupShown); + } else { + FireEvent(popup, eXULPopupHiding); + FireEvent(popup, eXULPopupHidden); + } +} + +nsPrintfCString Actions::Register(const dom::Element& aMenuItem, + bool aForSubmenu) { + nsPrintfCString actionName("item-%zu", mNextActionIndex++); + Maybe<bool> paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem); + RefPtr<GSimpleAction> action; + if (paramValue) { + action = dont_AddRef(g_simple_action_new_stateful( + actionName.get(), nullptr, g_variant_new_boolean(*paramValue))); + } else { + action = dont_AddRef(g_simple_action_new(actionName.get(), nullptr)); + } + if (aForSubmenu) { + g_signal_connect(action, "change-state", G_CALLBACK(ChangeStateSignal), + gpointer(&aMenuItem)); + } else { + g_signal_connect(action, "activate", G_CALLBACK(ActivateSignal), + gpointer(&aMenuItem)); + } + g_action_map_add_action(G_ACTION_MAP(mGroup.get()), G_ACTION(action.get())); + return actionName; +} + +void Actions::Clear() { + for (size_t i = 0; i < mNextActionIndex; ++i) { + g_action_map_remove_action(G_ACTION_MAP(mGroup.get()), + nsPrintfCString("item-%zu", i).get()); + } + mNextActionIndex = 0; +} + +class MenuModel final : public nsStubMutationObserver { + NS_DECL_ISUPPORTS + + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + + public: + explicit MenuModel(dom::Element* aElement) : mElement(aElement) { + mElement->AddMutationObserver(this); + mGMenu = dont_AddRef(g_menu_new()); + mActions.mGroup = dont_AddRef(g_simple_action_group_new()); + } + + GMenuModel* GetModel() { return G_MENU_MODEL(mGMenu.get()); } + GActionGroup* GetActionGroup() { + return G_ACTION_GROUP(mActions.mGroup.get()); + } + + dom::Element* Element() { return mElement; } + + void RecomputeModelIfNeeded(); + + bool IsShowing() { return mPoppedUp; } + void WillShow() { + mPoppedUp = true; + RecomputeModelIfNeeded(); + } + void DidHide() { mPoppedUp = false; } + + private: + virtual ~MenuModel() { mElement->RemoveMutationObserver(this); } + + void DirtyModel() { + mDirty = true; + if (mPoppedUp) { + RecomputeModelIfNeeded(); + } + } + + RefPtr<dom::Element> mElement; + RefPtr<GMenu> mGMenu; + Actions mActions; + bool mDirty = true; + bool mPoppedUp = false; +}; + +NS_IMPL_ISUPPORTS(MenuModel, nsIMutationObserver) + +void MenuModel::ContentRemoved(nsIContent* aChild, nsIContent*) { + if (NodeIsRelevant(*aChild)) { + DirtyModel(); + } +} + +void MenuModel::ContentInserted(nsIContent* aChild) { + if (NodeIsRelevant(*aChild)) { + DirtyModel(); + } +} + +void MenuModel::ContentAppended(nsIContent* aChild) { + if (NodeIsRelevant(*aChild)) { + DirtyModel(); + } +} + +void MenuModel::AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + if (NodeIsRelevant(*aElement) && + (aAttribute == nsGkAtoms::label || aAttribute == nsGkAtoms::aria_label || + aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::hidden)) { + DirtyModel(); + } +} + +static const dom::Element* GetMenuPopupChild(const dom::Element& aElement) { + for (const nsIContent* child = aElement.GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsXULElement(nsGkAtoms::menupopup)) { + return child->AsElement(); + } + } + return nullptr; +} + +static void RecomputeModelFor(GMenu* aMenu, Actions& aActions, + const dom::Element& aElement) { + RefPtr<GMenu> sectionMenu; + auto FlushSectionMenu = [&] { + if (sectionMenu) { + g_menu_append_section(aMenu, nullptr, G_MENU_MODEL(sectionMenu.get())); + sectionMenu = nullptr; + } + }; + + for (const nsIContent* child = aElement.GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsXULElement(nsGkAtoms::menuitem) && + !IsDisabled(*child->AsElement())) { + nsAutoString label; + child->AsElement()->GetAttr(nsGkAtoms::label, label); + if (label.IsEmpty()) { + child->AsElement()->GetAttr(nsGkAtoms::aria_label, label); + } + nsPrintfCString actionName( + "menu.%s", + aActions.Register(*child->AsElement(), /* aForSubmenu = */ false) + .get()); + g_menu_append(sectionMenu ? sectionMenu.get() : aMenu, + NS_ConvertUTF16toUTF8(label).get(), actionName.get()); + continue; + } + if (child->IsXULElement(nsGkAtoms::menuseparator)) { + FlushSectionMenu(); + sectionMenu = dont_AddRef(g_menu_new()); + continue; + } + if (child->IsXULElement(nsGkAtoms::menugroup)) { + FlushSectionMenu(); + sectionMenu = dont_AddRef(g_menu_new()); + RecomputeModelFor(sectionMenu, aActions, *child->AsElement()); + FlushSectionMenu(); + continue; + } + if (child->IsXULElement(nsGkAtoms::menu) && + !IsDisabled(*child->AsElement())) { + if (const auto* popup = GetMenuPopupChild(*child->AsElement())) { + RefPtr<GMenu> submenu = dont_AddRef(g_menu_new()); + RecomputeModelFor(submenu, aActions, *popup); + nsAutoString label; + child->AsElement()->GetAttr(nsGkAtoms::label, label); + RefPtr<GMenuItem> submenuItem = dont_AddRef(g_menu_item_new_submenu( + NS_ConvertUTF16toUTF8(label).get(), G_MENU_MODEL(submenu.get()))); + nsPrintfCString actionName( + "menu.%s", + aActions.Register(*popup, /* aForSubmenu = */ true).get()); + g_menu_item_set_attribute_value(submenuItem.get(), "submenu-action", + g_variant_new_string(actionName.get())); + g_menu_append_item(sectionMenu ? sectionMenu.get() : aMenu, + submenuItem.get()); + } + } + } + + FlushSectionMenu(); +} + +void MenuModel::RecomputeModelIfNeeded() { + if (!mDirty) { + return; + } + mActions.Clear(); + g_menu_remove_all(mGMenu.get()); + RecomputeModelFor(mGMenu.get(), mActions, *mElement); +} + +static GtkMenuPopupAtRect GetPopupAtRectFn() { + static GtkMenuPopupAtRect sFunc = + (GtkMenuPopupAtRect)dlsym(RTLD_DEFAULT, "gtk_menu_popup_at_rect"); + return sFunc; +} + +bool NativeMenuGtk::CanUse() { + return StaticPrefs::widget_gtk_native_context_menus() && GetPopupAtRectFn(); +} + +void NativeMenuGtk::FireEvent(EventMessage aPopupMessage) { + RefPtr target = Element(); + widget::FireEvent(target, aPopupMessage); +} + +#define METHOD_SIGNAL(name_) \ + static MOZ_CAN_RUN_SCRIPT_BOUNDARY void On##name_##Signal( \ + GtkWidget* widget, gpointer user_data) { \ + RefPtr menu = static_cast<NativeMenuGtk*>(user_data); \ + return menu->On##name_(); \ + } + +METHOD_SIGNAL(Unmap); + +#undef METHOD_SIGNAL + +NativeMenuGtk::NativeMenuGtk(dom::Element* aElement) + : mMenuModel(MakeRefPtr<MenuModel>(aElement)) { + // Floating, so no need to dont_AddRef. + mNativeMenu = gtk_menu_new_from_model(mMenuModel->GetModel()); + gtk_widget_insert_action_group(mNativeMenu.get(), "menu", + mMenuModel->GetActionGroup()); + g_signal_connect(mNativeMenu, "unmap", G_CALLBACK(OnUnmapSignal), this); +} + +NativeMenuGtk::~NativeMenuGtk() { + g_signal_handlers_disconnect_by_data(mNativeMenu, this); +} + +RefPtr<dom::Element> NativeMenuGtk::Element() { return mMenuModel->Element(); } + +void NativeMenuGtk::ShowAsContextMenu(nsIFrame* aClickedFrame, + const CSSIntPoint& aPosition, + bool aIsContextMenu) { + if (mMenuModel->IsShowing()) { + return; + } + RefPtr<nsIWidget> widget = aClickedFrame->PresContext()->GetRootWidget(); + if (NS_WARN_IF(!widget)) { + // XXX Do we need to close menus here? + return; + } + auto* win = static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW)); + if (NS_WARN_IF(!win)) { + return; + } + + auto* geckoWin = static_cast<nsWindow*>(widget.get()); + // The position needs to be relative to our window. + auto pos = (aPosition * aClickedFrame->PresContext()->CSSToDevPixelScale()) - + geckoWin->WidgetToScreenOffset(); + auto gdkPos = geckoWin->DevicePixelsToGdkPointRoundDown( + LayoutDeviceIntPoint::Round(pos)); + + mMenuModel->WillShow(); + const GdkRectangle rect = {gdkPos.x, gdkPos.y, 1, 1}; + auto openFn = GetPopupAtRectFn(); + openFn(GTK_MENU(mNativeMenu.get()), win, &rect, GDK_GRAVITY_NORTH_WEST, + GDK_GRAVITY_NORTH_WEST, GetLastMousePressEvent()); + + RefPtr pin{this}; + FireEvent(eXULPopupShown); +} + +bool NativeMenuGtk::Close() { + if (!mMenuModel->IsShowing()) { + return false; + } + gtk_menu_popdown(GTK_MENU(mNativeMenu.get())); + return true; +} + +void NativeMenuGtk::OnUnmap() { + FireEvent(eXULPopupHiding); + + mMenuModel->DidHide(); + + FireEvent(eXULPopupHidden); + + for (NativeMenu::Observer* observer : mObservers.Clone()) { + observer->OnNativeMenuClosed(); + } +} + +void NativeMenuGtk::ActivateItem(dom::Element* aItemElement, Modifiers, + int16_t aButton, ErrorResult&) { + // TODO: For testing only. +} + +void NativeMenuGtk::OpenSubmenu(dom::Element*) { + // TODO: For testing mostly. +} + +void NativeMenuGtk::CloseSubmenu(dom::Element*) { + // TODO: For testing mostly. +} + +} // namespace mozilla::widget diff --git a/widget/gtk/NativeMenuGtk.h b/widget/gtk/NativeMenuGtk.h new file mode 100644 index 0000000000..3f1f3213c1 --- /dev/null +++ b/widget/gtk/NativeMenuGtk.h @@ -0,0 +1,64 @@ + +/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_widget_NativeMenuGtk_h +#define mozilla_widget_NativeMenuGtk_h + +#include "mozilla/widget/NativeMenu.h" +#include "mozilla/EventForwards.h" +#include "GRefPtr.h" + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace widget { + +class MenuModel; + +class NativeMenuGtk : public NativeMenu { + public: + // Whether we can use native menu popups on GTK. + static bool CanUse(); + + explicit NativeMenuGtk(dom::Element* aElement); + + // NativeMenu + MOZ_CAN_RUN_SCRIPT_BOUNDARY void ShowAsContextMenu( + nsIFrame* aClickedFrame, const CSSIntPoint& aPosition, + bool aIsContextMenu) override; + bool Close() override; + void ActivateItem(dom::Element* aItemElement, Modifiers aModifiers, + int16_t aButton, ErrorResult& aRv) override; + void OpenSubmenu(dom::Element* aMenuElement) override; + void CloseSubmenu(dom::Element* aMenuElement) override; + RefPtr<dom::Element> Element() override; + void AddObserver(NativeMenu::Observer* aObserver) override { + mObservers.AppendElement(aObserver); + } + void RemoveObserver(NativeMenu::Observer* aObserver) override { + mObservers.RemoveElement(aObserver); + } + + MOZ_CAN_RUN_SCRIPT void OnUnmap(); + + protected: + virtual ~NativeMenuGtk(); + + MOZ_CAN_RUN_SCRIPT void FireEvent(EventMessage); + + bool mPoppedUp = false; + RefPtr<GtkWidget> mNativeMenu; + RefPtr<MenuModel> mMenuModel; + nsTArray<NativeMenu::Observer*> mObservers; +}; + +} // namespace widget +} // namespace mozilla + +#endif diff --git a/widget/gtk/NativeMenuSupport.cpp b/widget/gtk/NativeMenuSupport.cpp new file mode 100644 index 0000000000..4360867fff --- /dev/null +++ b/widget/gtk/NativeMenuSupport.cpp @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/widget/NativeMenuSupport.h" + +#include "MainThreadUtils.h" +#include "NativeMenuGtk.h" + +namespace mozilla::widget { + +void NativeMenuSupport::CreateNativeMenuBar(nsIWidget* aParent, + dom::Element* aMenuBarElement) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), + "Attempting to create native menu bar on wrong thread!"); + // TODO +} + +already_AddRefed<NativeMenu> NativeMenuSupport::CreateNativeContextMenu( + dom::Element* aPopup) { + return MakeAndAddRef<NativeMenuGtk>(aPopup); +} + +bool NativeMenuSupport::ShouldUseNativeContextMenus() { + return NativeMenuGtk::CanUse(); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/PCompositorWidget.ipdl b/widget/gtk/PCompositorWidget.ipdl new file mode 100644 index 0000000000..d554a33144 --- /dev/null +++ b/widget/gtk/PCompositorWidget.ipdl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=99: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PCompositorBridge; + +include "mozilla/GfxMessageUtils.h"; + +using mozilla::LayoutDeviceIntSize from "Units.h"; + +namespace mozilla { +namespace widget { + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +sync protocol PCompositorWidget +{ + manager PCompositorBridge; + +parent: + async __delete__(); + + async NotifyClientSizeChanged(LayoutDeviceIntSize aClientSize); + async DisableRendering(); + async EnableRendering(uintptr_t aXWindow, bool aShaped); + +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..c85da1711e --- /dev/null +++ b/widget/gtk/PlatformWidgetTypes.ipdlh @@ -0,0 +1,33 @@ +/* -*- 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; + +include "mozilla/GfxMessageUtils.h"; + +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..a4bd69f33b --- /dev/null +++ b/widget/gtk/ScreenHelperGTK.cpp @@ -0,0 +1,553 @@ +/* -*- 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 "mozilla/WidgetUtilsGtk.h" +#include "nsGtkUtils.h" +#include "nsTArray.h" +#include "nsWindow.h" + +#ifdef MOZ_WAYLAND +# include "nsWaylandDisplay.h" +#endif + +namespace mozilla::widget { + +#ifdef MOZ_LOGGING +static LazyLogModule sScreenLog("WidgetScreen"); +# define LOG_SCREEN(...) MOZ_LOG(sScreenLog, LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOG_SCREEN(...) +#endif /* MOZ_LOGGING */ + +using GdkMonitor = struct _GdkMonitor; + +static UniquePtr<ScreenGetter> gScreenGetter; +struct MonitorConfig { + int id = 0; + int x = 0; + int y = 0; + int width_mm = 0; + int height_mm = 0; + int width = 0; + int height = 0; + int scale = 0; + int refresh = 0; + int transform = 0; + + explicit MonitorConfig(int aId) : id(aId) {} +}; + +static void monitors_changed(GdkScreen* aScreen, gpointer aClosure) { + LOG_SCREEN("Received monitors-changed event"); + auto* self = static_cast<ScreenGetterGtk*>(aClosure); + self->RefreshScreens(); +} + +static void screen_resolution_changed(GdkScreen* aScreen, GParamSpec* aPspec, + ScreenGetterGtk* self) { + self->RefreshScreens(); +} + +static GdkFilterReturn root_window_event_filter(GdkXEvent* aGdkXEvent, + GdkEvent* aGdkEvent, + gpointer aClosure) { +#ifdef MOZ_X11 + ScreenGetterGtk* self = static_cast<ScreenGetterGtk*>(aClosure); + XEvent* xevent = static_cast<XEvent*>(aGdkXEvent); + + switch (xevent->type) { + case PropertyNotify: { + XPropertyEvent* propertyEvent = &xevent->xproperty; + if (propertyEvent->atom == self->NetWorkareaAtom()) { + LOG_SCREEN("Work area size changed"); + self->RefreshScreens(); + } + } break; + default: + break; + } +#endif + + return GDK_FILTER_CONTINUE; +} + +ScreenGetterGtk::ScreenGetterGtk() + : mRootWindow(nullptr) +#ifdef MOZ_X11 + , + mNetWorkareaAtom(0) +#endif +{ +} + +void ScreenGetterGtk::Init() { + LOG_SCREEN("ScreenGetterGtk 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 (GdkIsX11Display()) { + mNetWorkareaAtom = XInternAtom(GDK_WINDOW_XDISPLAY(mRootWindow), + "_NET_WORKAREA", X11False); + } +#endif + RefreshScreens(); +} + +ScreenGetterGtk::~ScreenGetterGtk() { + 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; + } +} + +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> MakeScreenGtk(GdkScreen* aScreen, + gint aMonitorNum) { + 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. + gint geometryScaleFactor = gdkScaleFactor; + + LayoutDeviceIntRect rect; + + gint refreshRate = [&] { + // Since gtk 3.22 + static auto s_gdk_display_get_monitor = (GdkMonitor * (*)(GdkDisplay*, int)) + dlsym(RTLD_DEFAULT, "gdk_display_get_monitor"); + static auto s_gdk_monitor_get_refresh_rate = (int (*)(GdkMonitor*))dlsym( + RTLD_DEFAULT, "gdk_monitor_get_refresh_rate"); + + if (!s_gdk_monitor_get_refresh_rate) { + return 0; + } + GdkMonitor* monitor = + s_gdk_display_get_monitor(gdk_display_get_default(), aMonitorNum); + if (!monitor) { + return 0; + } + // Convert to Hz. + return NSToIntRound(s_gdk_monitor_get_refresh_rate(monitor) / 1000.0f); + }(); + + GdkRectangle workarea; + gdk_screen_get_monitor_workarea(aScreen, aMonitorNum, &workarea); + LayoutDeviceIntRect availRect(workarea.x * geometryScaleFactor, + workarea.y * geometryScaleFactor, + workarea.width * geometryScaleFactor, + workarea.height * geometryScaleFactor); + if (GdkIsX11Display()) { + GdkRectangle monitor; + gdk_screen_get_monitor_geometry(aScreen, aMonitorNum, &monitor); + rect = LayoutDeviceIntRect(monitor.x * geometryScaleFactor, + monitor.y * geometryScaleFactor, + monitor.width * geometryScaleFactor, + monitor.height * geometryScaleFactor); + } else { + // We use Gtk workarea on Wayland as it matches our needs (Bug 1732682). + rect = availRect; + } + + uint32_t pixelDepth = GetGTKPixelDepth(); + + // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise. + DesktopToLayoutDeviceScale contentsScale(1.0); +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + contentsScale.scale = gdkScaleFactor; + } +#endif + + CSSToLayoutDeviceScale defaultCssScale(gdkScaleFactor); + + 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); + } + + LOG_SCREEN( + "New monitor %d size [%d,%d -> %d x %d] depth %d scale %f CssScale %f " + "DPI %f refresh %d ]", + aMonitorNum, rect.x, rect.y, rect.width, rect.height, pixelDepth, + contentsScale.scale, defaultCssScale.scale, dpi, refreshRate); + return MakeAndAddRef<Screen>(rect, availRect, pixelDepth, pixelDepth, + refreshRate, contentsScale, defaultCssScale, dpi, + Screen::IsPseudoDisplay::No); +} + +void ScreenGetterGtk::RefreshScreens() { + LOG_SCREEN("ScreenGetterGtk::RefreshScreens()"); + AutoTArray<RefPtr<Screen>, 4> screenList; + + GdkScreen* defaultScreen = gdk_screen_get_default(); + gint numScreens = gdk_screen_get_n_monitors(defaultScreen); + LOG_SCREEN("GDK reports %d screens", numScreens); + + for (gint i = 0; i < numScreens; i++) { + screenList.AppendElement(MakeScreenGtk(defaultScreen, i)); + } + + ScreenManager::Refresh(std::move(screenList)); +} + +#ifdef MOZ_WAYLAND +static void output_handle_geometry(void* data, struct wl_output* wl_output, + int x, int y, int physical_width, + int physical_height, int subpixel, + const char* make, const char* model, + int32_t transform) { + auto* monitor = static_cast<MonitorConfig*>(data); + LOG_SCREEN( + "wl_output: geometry position %d %d physical size %d %d, subpixel %d, " + "transform %d", + x, y, physical_width, physical_height, subpixel, transform); + monitor->x = x; + monitor->y = y; + monitor->width_mm = physical_width; + monitor->height_mm = physical_height; + monitor->transform = transform; +} + +static void output_handle_done(void* data, struct wl_output* wl_output) { + LOG_SCREEN("done"); + gScreenGetter->RefreshScreens(); +} + +static void output_handle_scale(void* data, struct wl_output* wl_output, + int32_t scale) { + auto* monitor = static_cast<MonitorConfig*>(data); + LOG_SCREEN("wl_output: scale %d", scale); + monitor->scale = scale; +} + +static void output_handle_mode(void* data, struct wl_output* wl_output, + uint32_t flags, int width, int height, + int refresh) { + auto* monitor = static_cast<MonitorConfig*>(data); + LOG_SCREEN("wl_output: mode output size %d x %d refresh %d", width, height, + refresh); + + if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) return; + + monitor->refresh = NSToIntRound(refresh / 1000.0f); + monitor->width = width; + monitor->height = height; +} + +static const struct wl_output_listener output_listener = { + output_handle_geometry, + output_handle_mode, + output_handle_done, + output_handle_scale, +}; + +static void screen_registry_handler(void* data, wl_registry* registry, + uint32_t id, const char* interface, + uint32_t version) { + auto* getter = static_cast<ScreenGetterWayland*>(data); + if (strcmp(interface, "wl_output") == 0 && version > 1) { + auto* output = + WaylandRegistryBind<wl_output>(registry, id, &wl_output_interface, 2); + wl_output_add_listener(output, &output_listener, + getter->AddMonitorConfig(id)); + } +} + +static void screen_registry_remover(void* data, struct wl_registry* registry, + uint32_t id) { + auto* getter = static_cast<ScreenGetterWayland*>(data); + if (getter->RemoveMonitorConfig(id)) { + getter->RefreshScreens(); + } + /* TODO: the object needs to be destroyed here, we're leaking */ +} + +static const struct wl_registry_listener screen_registry_listener = { + screen_registry_handler, screen_registry_remover}; + +void ScreenGetterWayland::Init() { + MOZ_ASSERT(GdkIsWaylandDisplay()); + LOG_SCREEN("ScreenGetterWayland created"); + wl_display* display = WaylandDisplayGetWLDisplay(); + mRegistry = wl_display_get_registry(display); + wl_registry_add_listener((wl_registry*)mRegistry, &screen_registry_listener, + this); + wl_display_roundtrip(display); + wl_display_roundtrip(display); +} + +MonitorConfig* ScreenGetterWayland::AddMonitorConfig(int aId) { + LOG_SCREEN("Add Monitor ID %d num %d", aId, (int)(mMonitors.Length() - 1)); + UniquePtr<MonitorConfig> monitor = MakeUnique<MonitorConfig>(aId); + mMonitors.AppendElement(std::move(monitor)); + return mMonitors.LastElement().get(); +} + +bool ScreenGetterWayland::RemoveMonitorConfig(int aId) { + for (unsigned int i = 0; i < mMonitors.Length(); i++) { + if (mMonitors[i]->id == aId) { + LOG_SCREEN("Remove Monitor ID %d num %d", aId, i); + mMonitors.RemoveElementAt(i); + return true; + } + } + return false; +} + +ScreenGetterWayland::ScreenGetterWayland() = default; + +ScreenGetterWayland::~ScreenGetterWayland() { + MozClearPointer(mRegistry, wl_registry_destroy); +} + +static bool GdkMonitorGetWorkarea(GdkMonitor* monitor, GdkRectangle* workarea) { + static auto s_gdk_monitor_get_workarea = + (void (*)(GdkMonitor*, GdkRectangle*))dlsym(RTLD_DEFAULT, + "gdk_monitor_get_workarea"); + if (!s_gdk_monitor_get_workarea) { + return false; + } + + s_gdk_monitor_get_workarea(monitor, workarea); + return true; +} + +already_AddRefed<Screen> ScreenGetterWayland::MakeScreenWayland(gint aMonitor) { + MonitorConfig* monitor = mMonitors[aMonitor].get(); + + // On GNOME/Mutter we use results from wl_output directly + LayoutDeviceIntRect rect(monitor->x, monitor->y, monitor->width, + monitor->height); + + uint32_t pixelDepth = GetGTKPixelDepth(); + + // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise. + DesktopToLayoutDeviceScale contentsScale(monitor->scale); + + CSSToLayoutDeviceScale defaultCssScale(monitor->scale); + + float dpi = 96.0f; + gint heightMM = monitor->height_mm; + if (heightMM > 0) { + dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT); + } + + bool defaultIsLandscape; + if (monitor->transform == WL_OUTPUT_TRANSFORM_90 || + monitor->transform == WL_OUTPUT_TRANSFORM_270) { + defaultIsLandscape = rect.width < rect.height; + } else { + defaultIsLandscape = rect.width >= rect.height; + } + + hal::ScreenOrientation orientation; + Screen::OrientationAngle angle; + // transform is counter-clockwise, but Screen Orientation API is clockwise. + switch (monitor->transform) { + case WL_OUTPUT_TRANSFORM_NORMAL: + orientation = defaultIsLandscape + ? hal::ScreenOrientation::LandscapePrimary + : hal::ScreenOrientation::PortraitPrimary; + angle = 0; + break; + case WL_OUTPUT_TRANSFORM_90: + orientation = defaultIsLandscape + ? hal::ScreenOrientation::PortraitPrimary + : hal::ScreenOrientation::LandscapeSecondary; + angle = 270; + break; + case WL_OUTPUT_TRANSFORM_180: + orientation = defaultIsLandscape + ? hal::ScreenOrientation::LandscapeSecondary + : hal::ScreenOrientation::PortraitSecondary; + angle = 180; + break; + case WL_OUTPUT_TRANSFORM_270: + orientation = defaultIsLandscape + ? hal::ScreenOrientation::PortraitSecondary + : hal::ScreenOrientation::LandscapePrimary; + angle = 90; + break; + default: + // WL_OUTPUT_TRANSFORM_FLIPPED_* is ignore since this is unused on normal + // situation. + orientation = hal::ScreenOrientation::None; + angle = 0; + break; + } + + LOG_SCREEN( + "Monitor %d [%d %d -> %d x %d depth %d content scale %f css scale %f " + "DPI %f, refresh %d, orientation %u, angle %u]", + aMonitor, rect.x, rect.y, rect.width, rect.height, pixelDepth, + contentsScale.scale, defaultCssScale.scale, dpi, monitor->refresh, + static_cast<uint32_t>(orientation), angle); + + // We report zero screen shift on Wayland. All popups positions are relative + // to toplevel and we can't get toplevel position from Wayland compositor. + rect.x = rect.y = 0; + return MakeAndAddRef<Screen>( + rect, rect, pixelDepth, pixelDepth, monitor->refresh, contentsScale, + defaultCssScale, dpi, Screen::IsPseudoDisplay::No, orientation, angle); +} + +void ScreenGetterWayland::RefreshScreens() { + LOG_SCREEN("ScreenGetterWayland::RefreshScreens()"); + AutoTArray<RefPtr<Screen>, 4> managerScreenList; + + mScreenList.Clear(); + const gint numScreens = mMonitors.Length(); + LOG_SCREEN("Wayland reports %d monitors", numScreens); + for (gint i = 0; i < numScreens; i++) { + RefPtr<Screen> screen = MakeScreenWayland(i); + mScreenList.AppendElement(screen); + managerScreenList.AppendElement(screen); + } + + ScreenManager::Refresh(std::move(managerScreenList)); +} + +int ScreenGetterWayland::GetMonitorForWindow(nsWindow* aWindow) { + LOG_SCREEN("GetMonitorForWindow() [%p]", aWindow); + + static auto s_gdk_display_get_monitor_at_window = + (GdkMonitor * (*)(GdkDisplay*, GdkWindow*)) + dlsym(RTLD_DEFAULT, "gdk_display_get_monitor_at_window"); + + if (!s_gdk_display_get_monitor_at_window) { + LOG_SCREEN(" failed, missing Gtk helpers"); + return -1; + } + + GdkWindow* gdkWindow = gtk_widget_get_window(aWindow->GetGtkWidget()); + if (!gdkWindow) { + LOG_SCREEN(" failed, can't get GdkWindow"); + return -1; + } + + GdkMonitor* monitor = + s_gdk_display_get_monitor_at_window(gdk_display_get_default(), gdkWindow); + if (!monitor) { + LOG_SCREEN(" failed, can't get monitor for GdkWindow"); + return -1; + } + + GdkRectangle workArea; + if (!GdkMonitorGetWorkarea(monitor, &workArea)) { + LOG_SCREEN(" failed, can't get work area"); + return -1; + } + + for (unsigned int i = 0; i < mMonitors.Length(); i++) { + // Although Gtk/Mutter is very creative in reporting various screens sizes + // we can rely on Gtk work area start position to match wl_output. + if (mMonitors[i]->x == workArea.x && mMonitors[i]->y == workArea.y) { + LOG_SCREEN(" monitor %d work area [%d, %d] -> (%d x %d) scale %d", i, + mMonitors[i]->x, mMonitors[i]->y, mMonitors[i]->width, + mMonitors[i]->height, mMonitors[i]->scale); + return i; + } + } + + return -1; +} + +RefPtr<widget::Screen> ScreenGetterWayland::GetScreenForWindow( + nsWindow* aWindow) { + if (mMonitors.IsEmpty()) { + return nullptr; + } + + int monitor = GetMonitorForWindow(aWindow); + if (monitor < 0) { + return nullptr; + } + + if (mMonitors.Length() != mScreenList.Length()) { + // Gtk list of GtkScreens are out of sync with our monitor list. + // Try to refresh it now. + RefreshScreens(); + } + + MOZ_DIAGNOSTIC_ASSERT((unsigned)monitor < mScreenList.Length(), + "We're missing screen?"); + return mScreenList[monitor]; +} +#endif + +RefPtr<widget::Screen> ScreenHelperGTK::GetScreenForWindow(nsWindow* aWindow) { + return gScreenGetter->GetScreenForWindow(aWindow); +} + +gint ScreenHelperGTK::GetGTKMonitorScaleFactor(gint aMonitorNum) { + GdkScreen* screen = gdk_screen_get_default(); + return gdk_screen_get_monitor_scale_factor(screen, aMonitorNum); +} + +ScreenHelperGTK::ScreenHelperGTK() { +#ifdef MOZ_WAYLAND + // Use ScreenGetterWayland on Gnome/Mutter only. It uses additional wl_output + // to track screen size changes (which are wrongly reported by mutter) + // and causes issues on Sway (Bug 1730476). + // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/3941 + if (GdkIsWaylandDisplay() && IsGnomeDesktopEnvironment()) { + gScreenGetter = MakeUnique<ScreenGetterWayland>(); + } +#endif + if (!gScreenGetter) { + gScreenGetter = MakeUnique<ScreenGetterGtk>(); + } + gScreenGetter->Init(); +} + +ScreenHelperGTK::~ScreenHelperGTK() { gScreenGetter = nullptr; } + +} // namespace mozilla::widget diff --git a/widget/gtk/ScreenHelperGTK.h b/widget/gtk/ScreenHelperGTK.h new file mode 100644 index 0000000000..2b162903aa --- /dev/null +++ b/widget/gtk/ScreenHelperGTK.h @@ -0,0 +1,104 @@ +/* -*- 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 + +class nsWindow; +struct wl_registry; + +namespace mozilla { +namespace widget { + +class ScreenGetter { + public: + ScreenGetter() = default; + virtual ~ScreenGetter(){}; + + virtual void Init(){}; + + virtual void RefreshScreens(){}; + virtual RefPtr<widget::Screen> GetScreenForWindow(nsWindow* aWindow) { + return nullptr; + } +}; + +class ScreenGetterGtk : public ScreenGetter { + public: + ScreenGetterGtk(); + ~ScreenGetterGtk(); + + void Init(); + +#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 +}; + +class ScreenGetterWayland; +struct MonitorConfig; + +#ifdef MOZ_WAYLAND +class ScreenGetterWayland : public ScreenGetter { + public: + ScreenGetterWayland(); + ~ScreenGetterWayland(); + + void Init(); + + MonitorConfig* AddMonitorConfig(int aId); + bool RemoveMonitorConfig(int aId); + already_AddRefed<Screen> MakeScreenWayland(gint aMonitor); + + RefPtr<widget::Screen> GetScreenForWindow(nsWindow* aWindow); + + // For internal use from signal callback functions + void RefreshScreens(); + + private: + int GetMonitorForWindow(nsWindow* aWindow); + bool MonitorUsesNonIntegerScale(int aMonitor); + + private: + wl_registry* mRegistry = nullptr; + // We use UniquePtr<> here to ensure that MonitorConfig is heap-allocated + // so it's not invalidated by any change to mMonitors that could happen in the + // meantime. + AutoTArray<UniquePtr<MonitorConfig>, 4> mMonitors; + AutoTArray<RefPtr<Screen>, 4> mScreenList; +}; +#endif + +class ScreenHelperGTK final : public ScreenManager::Helper { + public: + ScreenHelperGTK(); + ~ScreenHelperGTK(); + + static gint GetGTKMonitorScaleFactor(gint aMonitorNum = 0); + static RefPtr<widget::Screen> GetScreenForWindow(nsWindow* aWindow); +}; + +} // 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..396f39b5e7 --- /dev/null +++ b/widget/gtk/TaskbarProgress.cpp @@ -0,0 +1,106 @@ +/* -*- 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) { + 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); + + 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..e3517c5437 --- /dev/null +++ b/widget/gtk/WakeLockListener.cpp @@ -0,0 +1,546 @@ +/* -*- 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> + +# include "WidgetUtilsGtk.h" + +# if defined(MOZ_X11) +# include "prlink.h" +# include <gdk/gdk.h> +# include <gdk/gdkx.h> +# include "X11UndefineNone.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 FREEDESKTOP_POWER_TARGET "org.freedesktop.PowerManagement" +# define FREEDESKTOP_POWER_OBJECT "/org/freedesktop/PowerManagement/Inhibit" +# define FREEDESKTOP_POWER_INTERFACE "org.freedesktop.PowerManagement.Inhibit" + +# 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 WakeLockDesktopEnvironment { + FreeDesktopScreensaver, + FreeDesktopPower, + 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(FreeDesktopScreensaver), + mInhibitRequest(0), + mShouldInhibit(false), + mWaitingForReply(false) { + } + + nsresult InhibitScreensaver(void); + nsresult UninhibitScreensaver(void); + + private: + bool SendInhibit(); + bool SendUninhibit(); + + bool SendFreeDesktopPowerInhibitMessage(); + bool SendFreeDesktopScreensaverInhibitMessage(); + 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; + + WakeLockDesktopEnvironment 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::SendFreeDesktopPowerInhibitMessage() { + RefPtr<DBusMessage> message = + already_AddRefed<DBusMessage>(dbus_message_new_method_call( + FREEDESKTOP_POWER_TARGET, FREEDESKTOP_POWER_OBJECT, + FREEDESKTOP_POWER_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::SendFreeDesktopScreensaverInhibitMessage() { + 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 (!GdkIsX11Display(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 (!GdkIsX11Display(gDisplay)) { + return false; + } + Display* display = GDK_DISPLAY_XDISPLAY(gDisplay); + _XSSSuspend(display, inhibit); + return true; +} + +# endif + +# if defined(MOZ_WAYLAND) + +/* static */ +bool WakeLockTopic::CheckWaylandIdleInhibitSupport() { + nsWaylandDisplay* waylandDisplay = WaylandDisplayGet(); + return waylandDisplay && waylandDisplay->GetIdleInhibitManager() != nullptr; +} + +bool WakeLockTopic::InhibitWaylandIdle() { + nsWaylandDisplay* waylandDisplay = WaylandDisplayGet(); + if (!waylandDisplay) { + return false; + } + + nsWindow* focusedWindow = nsWindow::GetFocusedWindow(); + if (!focusedWindow) { + return false; + } + + UninhibitWaylandIdle(); + + MozContainerSurfaceLock lock(focusedWindow->GetMozContainer()); + struct wl_surface* waylandSurface = lock.GetSurface(); + if (waylandSurface) { + mWaylandInhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor( + waylandDisplay->GetIdleInhibitManager(), 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 FreeDesktopScreensaver: + WAKE_LOCK_LOG("SendInhibit(): FreeDesktopScreensaver"); + sendOk = SendFreeDesktopScreensaverInhibitMessage(); + break; + case FreeDesktopPower: + WAKE_LOCK_LOG("SendInhibit(): FreeDesktopPower"); + sendOk = SendFreeDesktopPowerInhibitMessage(); + break; + case GNOME: + WAKE_LOCK_LOG("SendInhibit(): GNOME"); + sendOk = SendGNOMEInhibitMessage(); + break; +# if defined(MOZ_X11) + case XScreenSaver: + WAKE_LOCK_LOG("SendInhibit(): InhibitXScreenSaver"); + return InhibitXScreenSaver(true); +# endif +# if defined(MOZ_WAYLAND) + case WaylandIdleInhibit: + WAKE_LOCK_LOG("SendInhibit(): WaylandIdleInhibit"); + return InhibitWaylandIdle(); +# endif + case Unsupported: + return false; + } + + if (sendOk) { + mWaitingForReply = true; + } + + return sendOk; +} + +bool WakeLockTopic::SendUninhibit() { + RefPtr<DBusMessage> message; + + if (mDesktopEnvironment == FreeDesktopScreensaver) { + WAKE_LOCK_LOG("SendUninhibit(): FreeDesktopScreensaver"); + message = already_AddRefed<DBusMessage>(dbus_message_new_method_call( + FREEDESKTOP_SCREENSAVER_TARGET, FREEDESKTOP_SCREENSAVER_OBJECT, + FREEDESKTOP_SCREENSAVER_INTERFACE, "UnInhibit")); + } else if (mDesktopEnvironment == FreeDesktopPower) { + WAKE_LOCK_LOG("SendUninhibit(): FreeDesktopPower"); + message = already_AddRefed<DBusMessage>(dbus_message_new_method_call( + FREEDESKTOP_POWER_TARGET, FREEDESKTOP_POWER_OBJECT, + FREEDESKTOP_POWER_INTERFACE, "UnInhibit")); + } else if (mDesktopEnvironment == GNOME) { + WAKE_LOCK_LOG("SendUninhibit(): 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) { + WAKE_LOCK_LOG("SendUninhibit(): XScreenSaver"); + return InhibitXScreenSaver(false); + } +# endif +# if defined(MOZ_WAYLAND) + else if (mDesktopEnvironment == WaylandIdleInhibit) { + WAKE_LOCK_LOG("SendUninhibit(): Wayland"); + 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 == FreeDesktopScreensaver) { + mDesktopEnvironment = GNOME; + } else if (mDesktopEnvironment == GNOME) { + mDesktopEnvironment = FreeDesktopPower; +# if defined(MOZ_X11) + } else if (mDesktopEnvironment == FreeDesktopPower && + CheckXScreenSaverSupport()) { + mDesktopEnvironment = XScreenSaver; +# endif +# if defined(MOZ_WAYLAND) + } else if (mDesktopEnvironment == FreeDesktopPower && + 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; + } + + WAKE_LOCK_LOG("WakeLockListener %s state %s", + NS_ConvertUTF16toUTF8(topic).get(), + NS_ConvertUTF16toUTF8(state).get()); + + if (!topic.Equals(u"screen"_ns) && !topic.Equals(u"video-playing"_ns) && + !topic.Equals(u"autoscroll"_ns)) + return NS_OK; + + WakeLockTopic* const topicLock = + mTopics.GetOrInsertNew(topic, topic, mConnection); + + // Treat "locked-background" the same as "unlocked" on desktop linux. + bool shouldLock = state.EqualsLiteral("locked-foreground"); + WAKE_LOCK_LOG("shouldLock %d", 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/WaylandBuffer.cpp b/widget/gtk/WaylandBuffer.cpp new file mode 100644 index 0000000000..f3fc409362 --- /dev/null +++ b/widget/gtk/WaylandBuffer.cpp @@ -0,0 +1,224 @@ +/* -*- 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 "WaylandBuffer.h" + +#include <sys/mman.h> +#include <fcntl.h> +#include <errno.h> + +#include "gfx2DGlue.h" +#include "gfxPlatform.h" +#include "mozilla/WidgetUtilsGtk.h" +#include "mozilla/gfx/Tools.h" +#include "nsGtkUtils.h" +#include "nsPrintfCString.h" +#include "prenv.h" // For PR_GetEnv + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "mozilla/ScopeExit.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetWaylandLog; +# define LOGWAYLAND(...) \ + MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOGWAYLAND(...) +#endif /* MOZ_LOGGING */ + +using namespace mozilla::gl; + +namespace mozilla::widget { + +#define BUFFER_BPP 4 +gfx::SurfaceFormat WaylandBuffer::mFormat = gfx::SurfaceFormat::B8G8R8A8; + +#ifdef MOZ_LOGGING +int WaylandBufferSHM::mDumpSerial = + PR_GetEnv("MOZ_WAYLAND_DUMP_WL_BUFFERS") ? 1 : 0; +char* WaylandBufferSHM::mDumpDir = PR_GetEnv("MOZ_WAYLAND_DUMP_DIR"); +#endif + +/* static */ +RefPtr<WaylandShmPool> WaylandShmPool::Create(nsWaylandDisplay* aWaylandDisplay, + int aSize) { + if (!aWaylandDisplay->GetShm()) { + NS_WARNING("WaylandShmPool: Missing Wayland shm interface!"); + return nullptr; + } + + RefPtr<WaylandShmPool> shmPool = new WaylandShmPool(); + + shmPool->mShm = MakeUnique<base::SharedMemory>(); + if (!shmPool->mShm->Create(aSize)) { + NS_WARNING("WaylandShmPool: Unable to allocate shared memory!"); + return nullptr; + } + + shmPool->mSize = aSize; + shmPool->mShmPool = wl_shm_create_pool( + aWaylandDisplay->GetShm(), shmPool->mShm->CloneHandle().get(), aSize); + if (!shmPool->mShmPool) { + NS_WARNING("WaylandShmPool: Unable to allocate shared memory pool!"); + return nullptr; + } + + return shmPool; +} + +void* WaylandShmPool::GetImageData() { + if (mImageData) { + return mImageData; + } + if (!mShm->Map(mSize)) { + NS_WARNING("WaylandShmPool: Failed to map Shm!"); + return nullptr; + } + mImageData = mShm->memory(); + return mImageData; +} + +WaylandShmPool::~WaylandShmPool() { + MozClearPointer(mShmPool, wl_shm_pool_destroy); +} + +static const struct wl_buffer_listener sBufferListenerWaylandBuffer = { + WaylandBuffer::BufferReleaseCallbackHandler}; + +WaylandBuffer::WaylandBuffer(const LayoutDeviceIntSize& aSize) : mSize(aSize) {} + +void WaylandBuffer::AttachAndCommit(wl_surface* aSurface) { + LOGWAYLAND( + "WaylandBuffer::AttachAndCommit [%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); + } +} + +void WaylandBuffer::BufferReleaseCallbackHandler(wl_buffer* aBuffer) { + mAttached = false; + + if (mBufferReleaseFunc) { + mBufferReleaseFunc(mBufferReleaseData, aBuffer); + } +} + +void WaylandBuffer::BufferReleaseCallbackHandler(void* aData, + wl_buffer* aBuffer) { + auto* buffer = reinterpret_cast<WaylandBuffer*>(aData); + buffer->BufferReleaseCallbackHandler(aBuffer); +} + +/* static */ +RefPtr<WaylandBufferSHM> WaylandBufferSHM::Create( + const LayoutDeviceIntSize& aSize) { + RefPtr<WaylandBufferSHM> buffer = new WaylandBufferSHM(aSize); + nsWaylandDisplay* waylandDisplay = WaylandDisplayGet(); + + int size = aSize.width * aSize.height * BUFFER_BPP; + buffer->mShmPool = WaylandShmPool::Create(waylandDisplay, size); + if (!buffer->mShmPool) { + return nullptr; + } + + buffer->mWLBuffer = wl_shm_pool_create_buffer( + buffer->mShmPool->GetShmPool(), 0, aSize.width, aSize.height, + aSize.width * BUFFER_BPP, WL_SHM_FORMAT_ARGB8888); + if (!buffer->mWLBuffer) { + return nullptr; + } + + wl_buffer_add_listener(buffer->GetWlBuffer(), &sBufferListenerWaylandBuffer, + buffer.get()); + + LOGWAYLAND("WaylandBufferSHM Created [%p] WaylandDisplay [%p]\n", + buffer.get(), waylandDisplay); + + return buffer; +} + +WaylandBufferSHM::WaylandBufferSHM(const LayoutDeviceIntSize& aSize) + : WaylandBuffer(aSize) {} + +WaylandBufferSHM::~WaylandBufferSHM() { + MozClearPointer(mWLBuffer, wl_buffer_destroy); +} + +already_AddRefed<gfx::DrawTarget> WaylandBufferSHM::Lock() { + return gfxPlatform::CreateDrawTargetForData( + static_cast<unsigned char*>(mShmPool->GetImageData()), + mSize.ToUnknownSize(), BUFFER_BPP * mSize.width, GetSurfaceFormat()); +} + +void WaylandBufferSHM::Clear() { + memset(mShmPool->GetImageData(), 0, mSize.height * mSize.width * BUFFER_BPP); +} + +#ifdef MOZ_LOGGING +void WaylandBufferSHM::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, + mSize.width, mSize.height, BUFFER_BPP * mSize.width); + 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 */ +RefPtr<WaylandBufferDMABUF> WaylandBufferDMABUF::Create( + const LayoutDeviceIntSize& aSize, GLContext* aGL) { + RefPtr<WaylandBufferDMABUF> buffer = new WaylandBufferDMABUF(aSize); + + const auto flags = + static_cast<DMABufSurfaceFlags>(DMABUF_TEXTURE | DMABUF_ALPHA); + buffer->mDMABufSurface = + DMABufSurfaceRGBA::CreateDMABufSurface(aSize.width, aSize.height, flags); + if (!buffer->mDMABufSurface || !buffer->mDMABufSurface->CreateTexture(aGL)) { + return nullptr; + } + + if (!buffer->mDMABufSurface->CreateWlBuffer()) { + return nullptr; + } + + wl_buffer_add_listener(buffer->GetWlBuffer(), &sBufferListenerWaylandBuffer, + buffer.get()); + + return buffer; +} + +WaylandBufferDMABUF::WaylandBufferDMABUF(const LayoutDeviceIntSize& aSize) + : WaylandBuffer(aSize) {} + +} // namespace mozilla::widget diff --git a/widget/gtk/WaylandBuffer.h b/widget/gtk/WaylandBuffer.h new file mode 100644 index 0000000000..34bdb87ff5 --- /dev/null +++ b/widget/gtk/WaylandBuffer.h @@ -0,0 +1,140 @@ +/* -*- 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_WAYLAND_BUFFER_H +#define _MOZILLA_WIDGET_GTK_WAYLAND_BUFFER_H + +#include "DMABufSurface.h" +#include "GLContext.h" +#include "MozFramebuffer.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/Mutex.h" +#include "nsTArray.h" +#include "nsWaylandDisplay.h" +#include "base/shared_memory.h" + +namespace mozilla::widget { + +// Allocates and owns shared memory for Wayland drawing surface +class WaylandShmPool { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaylandShmPool); + + static RefPtr<WaylandShmPool> Create(nsWaylandDisplay* aWaylandDisplay, + int aSize); + + wl_shm_pool* GetShmPool() { return mShmPool; }; + void* GetImageData(); + + private: + WaylandShmPool() = default; + ~WaylandShmPool(); + + wl_shm_pool* mShmPool = nullptr; + void* mImageData = nullptr; + UniquePtr<base::SharedMemory> mShm; + int mSize = 0; +}; + +class WaylandBuffer { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaylandBuffer); + + void AttachAndCommit(wl_surface* aSurface); + virtual wl_buffer* GetWlBuffer() = 0; + virtual already_AddRefed<gfx::DrawTarget> Lock() { return nullptr; }; + virtual void* GetImageData() { return nullptr; } + virtual GLuint GetTexture() { return 0; } + virtual void DestroyGLResources(){}; + + LayoutDeviceIntSize GetSize() { return mSize; }; + bool IsMatchingSize(const LayoutDeviceIntSize& aSize) { + return aSize == mSize; + } + bool IsAttached() { return mAttached; } + static gfx::SurfaceFormat GetSurfaceFormat() { return mFormat; } + + static void BufferReleaseCallbackHandler(void* aData, wl_buffer* aBuffer); + void SetBufferReleaseFunc(void (*aBufferReleaseFunc)(void* aData, + wl_buffer* aBuffer)) { + mBufferReleaseFunc = aBufferReleaseFunc; + } + void SetBufferReleaseData(void* aBufferReleaseData) { + mBufferReleaseData = aBufferReleaseData; + } + + protected: + explicit WaylandBuffer(const LayoutDeviceIntSize& aSize); + virtual ~WaylandBuffer() = default; + + void BufferReleaseCallbackHandler(wl_buffer* aBuffer); + + LayoutDeviceIntSize mSize; + bool mAttached = false; + void (*mBufferReleaseFunc)(void* aData, wl_buffer* aBuffer) = nullptr; + void* mBufferReleaseData = nullptr; + static gfx::SurfaceFormat mFormat; +}; + +// Holds actual graphics data for wl_surface +class WaylandBufferSHM final : public WaylandBuffer { + public: + static RefPtr<WaylandBufferSHM> Create(const LayoutDeviceIntSize& aSize); + + wl_buffer* GetWlBuffer() override { return mWLBuffer; }; + already_AddRefed<gfx::DrawTarget> Lock() override; + void* GetImageData() override { return mShmPool->GetImageData(); } + + void Clear(); + size_t GetBufferAge() { return mBufferAge; }; + RefPtr<WaylandShmPool> GetShmPool() { return mShmPool; } + + void IncrementBufferAge() { mBufferAge++; }; + void ResetBufferAge() { mBufferAge = 0; }; + +#ifdef MOZ_LOGGING + void DumpToFile(const char* aHint); +#endif + + private: + explicit WaylandBufferSHM(const LayoutDeviceIntSize& aSize); + ~WaylandBufferSHM() override; + + // WaylandShmPoolMB provides actual shared memory we draw into + RefPtr<WaylandShmPool> mShmPool; + + // wl_buffer is a wayland object that encapsulates the shared memory + // and passes it to wayland compositor by wl_surface object. + wl_buffer* mWLBuffer = nullptr; + + size_t mBufferAge = 0; + +#ifdef MOZ_LOGGING + static int mDumpSerial; + static char* mDumpDir; +#endif +}; + +class WaylandBufferDMABUF final : public WaylandBuffer { + public: + static RefPtr<WaylandBufferDMABUF> Create(const LayoutDeviceIntSize& aSize, + gl::GLContext* aGL); + + wl_buffer* GetWlBuffer() override { return mDMABufSurface->GetWlBuffer(); }; + GLuint GetTexture() override { return mDMABufSurface->GetTexture(); }; + void DestroyGLResources() override { mDMABufSurface->ReleaseTextures(); }; + + private: + explicit WaylandBufferDMABUF(const LayoutDeviceIntSize& aSize); + ~WaylandBufferDMABUF() = default; + + RefPtr<DMABufSurfaceRGBA> mDMABufSurface; +}; + +} // namespace mozilla::widget + +#endif // _MOZILLA_WIDGET_GTK_WAYLAND_BUFFER_H diff --git a/widget/gtk/WaylandVsyncSource.cpp b/widget/gtk/WaylandVsyncSource.cpp new file mode 100644 index 0000000000..e5bca04837 --- /dev/null +++ b/widget/gtk/WaylandVsyncSource.cpp @@ -0,0 +1,431 @@ +/* -*- 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 "mozilla/UniquePtr.h" +# include "nsThreadUtils.h" +# include "nsISupportsImpl.h" +# include "MainThreadUtils.h" +# include "mozilla/ScopeExit.h" +# include "nsGtkUtils.h" +# include "mozilla/StaticPrefs_layout.h" +# include "mozilla/StaticPrefs_widget.h" +# include "nsWindow.h" + +# include <gdk/gdkwayland.h> + +# ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetVsync; +# undef LOG +# define LOG(str, ...) \ + MOZ_LOG(gWidgetVsync, mozilla::LogLevel::Debug, \ + ("[nsWindow %p]: " str, GetWindowForLogging(), ##__VA_ARGS__)) +# else +# define LOG(...) +# endif /* MOZ_LOGGING */ + +using namespace mozilla::widget; + +namespace mozilla { + +static void WaylandVsyncSourceCallbackHandler(void* aData, + struct wl_callback* aCallback, + uint32_t aTime) { + RefPtr context = static_cast<WaylandVsyncSource*>(aData); + context->FrameCallback(aCallback, aTime); +} + +static const struct wl_callback_listener WaylandVsyncSourceCallbackListener = { + WaylandVsyncSourceCallbackHandler}; + +static void NativeLayerRootWaylandVsyncCallback(void* aData, uint32_t aTime) { + RefPtr context = static_cast<WaylandVsyncSource*>(aData); + context->FrameCallback(nullptr, aTime); +} + +static float GetFPS(TimeDuration aVsyncRate) { + return 1000.0f / float(aVsyncRate.ToMilliseconds()); +} + +static nsTArray<WaylandVsyncSource*> gWaylandVsyncSources; + +Maybe<TimeDuration> WaylandVsyncSource::GetFastestVsyncRate() { + Maybe<TimeDuration> retVal; + for (auto* source : gWaylandVsyncSources) { + auto rate = source->GetVsyncRateIfEnabled(); + if (!rate) { + continue; + } + if (!retVal.isSome()) { + retVal.emplace(*rate); + } else if (*rate < *retVal) { + retVal.ref() = *rate; + } + } + + return retVal; +} + +WaylandVsyncSource::WaylandVsyncSource(nsWindow* aWindow) + : mMutex("WaylandVsyncSource"), + mVsyncRate(TimeDuration::FromMilliseconds(1000.0 / 60.0)), + mLastVsyncTimeStamp(TimeStamp::Now()), + mWindow(aWindow), + mIdleTimeout(1000 / StaticPrefs::layout_throttled_frame_rate()) { + MOZ_ASSERT(NS_IsMainThread()); + gWaylandVsyncSources.AppendElement(this); +} + +WaylandVsyncSource::~WaylandVsyncSource() { + gWaylandVsyncSources.RemoveElement(this); +} + +void WaylandVsyncSource::MaybeUpdateSource(MozContainer* aContainer) { + MutexAutoLock lock(mMutex); + + LOG("WaylandVsyncSource::MaybeUpdateSource fps %f", GetFPS(mVsyncRate)); + + if (aContainer == mContainer) { + LOG(" mContainer is the same, quit."); + return; + } + + mNativeLayerRoot = nullptr; + mContainer = aContainer; + + if (mMonitorEnabled) { + LOG(" monitor enabled, ask for Refresh()"); + mCallbackRequested = false; + Refresh(lock); + } +} + +void WaylandVsyncSource::MaybeUpdateSource( + const RefPtr<NativeLayerRootWayland>& aNativeLayerRoot) { + MutexAutoLock lock(mMutex); + + LOG("WaylandVsyncSource::MaybeUpdateSource aNativeLayerRoot fps %f", + GetFPS(mVsyncRate)); + + if (aNativeLayerRoot == mNativeLayerRoot) { + LOG(" mNativeLayerRoot is the same, quit."); + return; + } + + mNativeLayerRoot = aNativeLayerRoot; + mContainer = nullptr; + + if (mMonitorEnabled) { + LOG(" monitor enabled, ask for Refresh()"); + mCallbackRequested = false; + Refresh(lock); + } +} + +void WaylandVsyncSource::Refresh(const MutexAutoLock& aProofOfLock) { + mMutex.AssertCurrentThreadOwns(); + + LOG("WaylandVsyncSource::Refresh fps %f\n", GetFPS(mVsyncRate)); + + if (!(mContainer || mNativeLayerRoot) || !mMonitorEnabled || !mVsyncEnabled || + mCallbackRequested) { + // 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. + LOG(" quit mContainer %d mNativeLayerRoot %d mMonitorEnabled %d " + "mVsyncEnabled %d mCallbackRequested %d", + !!mContainer, !!mNativeLayerRoot, mMonitorEnabled, mVsyncEnabled, + !!mCallbackRequested); + return; + } + + if (mContainer) { + MozContainerSurfaceLock lock(mContainer); + struct wl_surface* surface = lock.GetSurface(); + LOG(" refresh from mContainer, wl_surface %p", surface); + if (!surface) { + LOG(" we're missing wl_surface, register Refresh() callback"); + // The surface hasn't been created yet. Try again when the surface is + // ready. + RefPtr<WaylandVsyncSource> self(this); + moz_container_wayland_add_initial_draw_callback_locked( + mContainer, [self]() -> void { + MutexAutoLock lock(self->mMutex); + self->Refresh(lock); + }); + return; + } + } + + // Vsync is enabled, but we don't have a callback configured. Set one up so + // we can get to work. + SetupFrameCallback(aProofOfLock); + const TimeStamp lastVSync = TimeStamp::Now(); + mLastVsyncTimeStamp = lastVSync; + TimeStamp outputTimestamp = mLastVsyncTimeStamp + mVsyncRate; + + { + MutexAutoUnlock unlock(mMutex); + NotifyVsync(lastVSync, outputTimestamp); + } +} + +void WaylandVsyncSource::EnableMonitor() { + LOG("WaylandVsyncSource::EnableMonitor"); + MutexAutoLock lock(mMutex); + if (mMonitorEnabled) { + return; + } + mMonitorEnabled = true; + Refresh(lock); +} + +void WaylandVsyncSource::DisableMonitor() { + LOG("WaylandVsyncSource::DisableMonitor"); + MutexAutoLock lock(mMutex); + if (!mMonitorEnabled) { + return; + } + mMonitorEnabled = false; + mCallbackRequested = false; +} + +void WaylandVsyncSource::SetupFrameCallback(const MutexAutoLock& aProofOfLock) { + mMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(!mCallbackRequested); + + LOG("WaylandVsyncSource::SetupFrameCallback"); + + if (mNativeLayerRoot) { + LOG(" use mNativeLayerRoot"); + mNativeLayerRoot->RequestFrameCallback(&NativeLayerRootWaylandVsyncCallback, + this); + } else { + MozContainerSurfaceLock lock(mContainer); + struct wl_surface* surface = lock.GetSurface(); + LOG(" use mContainer, wl_surface %p", surface); + 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. + LOG(" missing wl_surface, quit."); + return; + } + + LOG(" register frame callback"); + MozClearPointer(mCallback, wl_callback_destroy); + mCallback = wl_surface_frame(surface); + wl_callback_add_listener(mCallback, &WaylandVsyncSourceCallbackListener, + this); + wl_surface_commit(surface); + wl_display_flush(WaylandDisplayGet()->GetDisplay()); + + if (!mIdleTimerID) { + mIdleTimerID = g_timeout_add( + mIdleTimeout, + [](void* data) -> gint { + RefPtr vsync = static_cast<WaylandVsyncSource*>(data); + if (vsync->IdleCallback()) { + // We want to fire again, so don't clear mIdleTimerID + return G_SOURCE_CONTINUE; + } + // No need for g_source_remove, caller does it for us. + vsync->mIdleTimerID = 0; + return G_SOURCE_REMOVE; + }, + this); + } + } + + mCallbackRequested = true; +} + +bool WaylandVsyncSource::IdleCallback() { + LOG("WaylandVsyncSource::IdleCallback"); + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + RefPtr<nsWindow> window; + TimeStamp lastVSync; + TimeStamp outputTimestamp; + { + MutexAutoLock lock(mMutex); + + 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. + LOG(" quit, mVsyncEnabled %d mMonitorEnabled %d", mVsyncEnabled, + mMonitorEnabled); + return false; + } + + const auto now = TimeStamp::Now(); + const auto timeSinceLastVSync = now - mLastVsyncTimeStamp; + if (timeSinceLastVSync.ToMilliseconds() < mIdleTimeout) { + // We're not idle, we want to fire the timer again. + return true; + } + + LOG(" fire idle vsync"); + CalculateVsyncRate(lock, now); + mLastVsyncTimeStamp = lastVSync = now; + + outputTimestamp = mLastVsyncTimeStamp + mVsyncRate; + window = mWindow; + } + + // This could disable vsync. + window->NotifyOcclusionState(OcclusionState::OCCLUDED); + + if (window->IsDestroyed()) { + return false; + } + // Make sure to fire vsync now even if we get disabled afterwards. + // This gives an opportunity to clean up after the visibility state change. + // FIXME: Do we really need to do this? + NotifyVsync(lastVSync, outputTimestamp); + return StaticPrefs::widget_wayland_vsync_keep_firing_at_idle(); +} + +void WaylandVsyncSource::FrameCallback(wl_callback* aCallback, uint32_t aTime) { + LOG("WaylandVsyncSource::FrameCallback"); + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + { + // This might enable vsync. + RefPtr window = mWindow; + window->NotifyOcclusionState(OcclusionState::VISIBLE); + // NotifyOcclusionState can destroy us. + if (window->IsDestroyed()) { + return; + } + } + + MutexAutoLock lock(mMutex); + mCallbackRequested = false; + + // NotifyOcclusionState() can clear and create new mCallback by + // EnableVsync()/Refresh(). So don't delete newly created frame callback. + if (aCallback && aCallback == mCallback) { + MozClearPointer(mCallback, wl_callback_destroy); + } + + 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. + LOG(" quit, mVsyncEnabled %d mMonitorEnabled %d", mVsyncEnabled, + mMonitorEnabled); + return; + } + + // Configure our next frame callback. + SetupFrameCallback(lock); + + int64_t tick = BaseTimeDurationPlatformUtils::TicksFromMilliseconds(aTime); + const auto callbackTimeStamp = TimeStamp::FromSystemTime(tick); + const auto now = TimeStamp::Now(); + + // If the callback timestamp is close enough to our timestamp, use it, + // otherwise use the current time. + const TimeStamp& vsyncTimestamp = + std::abs((now - callbackTimeStamp).ToMilliseconds()) < 50.0 + ? callbackTimeStamp + : now; + + CalculateVsyncRate(lock, vsyncTimestamp); + mLastVsyncTimeStamp = vsyncTimestamp; + const TimeStamp outputTimestamp = vsyncTimestamp + mVsyncRate; + + { + MutexAutoUnlock unlock(mMutex); + NotifyVsync(vsyncTimestamp, outputTimestamp); + } +} + +TimeDuration WaylandVsyncSource::GetVsyncRate() { + MutexAutoLock lock(mMutex); + return mVsyncRate; +} + +Maybe<TimeDuration> WaylandVsyncSource::GetVsyncRateIfEnabled() { + MutexAutoLock lock(mMutex); + if (!mVsyncEnabled) { + return Nothing(); + } + return Some(mVsyncRate); +} + +void WaylandVsyncSource::CalculateVsyncRate(const MutexAutoLock& aProofOfLock, + TimeStamp aVsyncTimestamp) { + mMutex.AssertCurrentThreadOwns(); + + double duration = (aVsyncTimestamp - mLastVsyncTimeStamp).ToMilliseconds(); + double curVsyncRate = mVsyncRate.ToMilliseconds(); + + LOG("WaylandVsyncSource::CalculateVsyncRate start fps %f\n", + GetFPS(mVsyncRate)); + + 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); + } + + LOG(" new fps %f correction %f\n", GetFPS(mVsyncRate), correction); +} + +void WaylandVsyncSource::EnableVsync() { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + + LOG("WaylandVsyncSource::EnableVsync fps %f\n", GetFPS(mVsyncRate)); + if (mVsyncEnabled || mIsShutdown) { + LOG(" early quit"); + return; + } + mVsyncEnabled = true; + Refresh(lock); +} + +void WaylandVsyncSource::DisableVsync() { + MutexAutoLock lock(mMutex); + + LOG("WaylandVsyncSource::DisableVsync fps %f\n", GetFPS(mVsyncRate)); + mVsyncEnabled = false; + mCallbackRequested = false; +} + +bool WaylandVsyncSource::IsVsyncEnabled() { + MutexAutoLock lock(mMutex); + return mVsyncEnabled; +} + +void WaylandVsyncSource::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mMutex); + + LOG("WaylandVsyncSource::Shutdown fps %f\n", GetFPS(mVsyncRate)); + mContainer = nullptr; + mNativeLayerRoot = nullptr; + mIsShutdown = true; + mVsyncEnabled = false; + mCallbackRequested = false; + MozClearHandleID(mIdleTimerID, g_source_remove); + MozClearPointer(mCallback, wl_callback_destroy); +} + +} // namespace mozilla + +#endif // MOZ_WAYLAND diff --git a/widget/gtk/WaylandVsyncSource.h b/widget/gtk/WaylandVsyncSource.h new file mode 100644 index 0000000000..78fe72e445 --- /dev/null +++ b/widget/gtk/WaylandVsyncSource.h @@ -0,0 +1,99 @@ +/* -*- 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 "base/thread.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Maybe.h" +#include "mozilla/Mutex.h" +#include "mozilla/Monitor.h" +#include "mozilla/layers/NativeLayerWayland.h" +#include "MozContainer.h" +#include "nsWaylandDisplay.h" +#include "VsyncSource.h" + +namespace mozilla { + +using layers::NativeLayerRootWayland; + +/* + * 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(nsWindow* aWindow); + virtual ~WaylandVsyncSource(); + + static Maybe<TimeDuration> GetFastestVsyncRate(); + + void MaybeUpdateSource(MozContainer* aContainer); + void MaybeUpdateSource( + const RefPtr<NativeLayerRootWayland>& aNativeLayerRoot); + + void EnableMonitor(); + void DisableMonitor(); + + void FrameCallback(wl_callback* aCallback, uint32_t aTime); + // Returns whether we should keep firing. + bool IdleCallback(); + + TimeDuration GetVsyncRate() override; + + void EnableVsync() override; + + void DisableVsync() override; + + bool IsVsyncEnabled() override; + + void Shutdown() override; + + private: + Maybe<TimeDuration> GetVsyncRateIfEnabled(); + + void Refresh(const MutexAutoLock& aProofOfLock); + void SetupFrameCallback(const MutexAutoLock& aProofOfLock); + void CalculateVsyncRate(const MutexAutoLock& aProofOfLock, + TimeStamp aVsyncTimestamp); + void* GetWindowForLogging() { return mWindow; }; + + Mutex mMutex; + bool mIsShutdown MOZ_GUARDED_BY(mMutex) = false; + bool mVsyncEnabled MOZ_GUARDED_BY(mMutex) = false; + bool mMonitorEnabled MOZ_GUARDED_BY(mMutex) = false; + bool mCallbackRequested MOZ_GUARDED_BY(mMutex) = false; + MozContainer* mContainer MOZ_GUARDED_BY(mMutex) = nullptr; + RefPtr<NativeLayerRootWayland> mNativeLayerRoot MOZ_GUARDED_BY(mMutex); + TimeDuration mVsyncRate MOZ_GUARDED_BY(mMutex); + TimeStamp mLastVsyncTimeStamp MOZ_GUARDED_BY(mMutex); + wl_callback* mCallback MOZ_GUARDED_BY(mMutex) = nullptr; + + guint mIdleTimerID = 0; // Main thread only. + nsWindow* const mWindow; // Main thread only, except for logging. + const guint mIdleTimeout; +}; + +} // namespace mozilla + +#endif // _WaylandVsyncSource_h_ diff --git a/widget/gtk/WidgetStyleCache.cpp b/widget/gtk/WidgetStyleCache.cpp new file mode 100644 index 0000000000..05bfd30142 --- /dev/null +++ b/widget/gtk/WidgetStyleCache.cpp @@ -0,0 +1,1463 @@ +/* -*- 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 "mozilla/ScopeExit.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"); + +enum class CSDStyle { + Unknown, + Solid, + Normal, +}; + +static bool gHeaderBarShouldDrawContainer = false; +static bool gMaximizedHeaderBarShouldDrawContainer = false; +static CSDStyle gCSDStyle = CSDStyle::Unknown; +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* CreateMenuPopupWidget() { + GtkWidget* widget = gtk_menu_new(); + GtkStyleContext* style = gtk_widget_get_style_context(widget); + gtk_style_context_add_class(style, GTK_STYLE_CLASS_POPUP); + 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* 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* 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; +} + +static bool HasBackground(GtkStyleContext* aStyle) { + GdkRGBA gdkColor; + gtk_style_context_get_background_color(aStyle, GTK_STATE_FLAG_NORMAL, + &gdkColor); + if (gdkColor.alpha != 0.0) { + return true; + } + + GValue value = G_VALUE_INIT; + gtk_style_context_get_property(aStyle, "background-image", + GTK_STATE_FLAG_NORMAL, &value); + auto cleanup = mozilla::MakeScopeExit([&] { g_value_unset(&value); }); + return g_value_get_boxed(&value); +} + +static void CreateHeaderBarWidget(WidgetNodeType aAppearance) { + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + GtkStyleContext* windowStyle = gtk_widget_get_style_context(window); + + // Headerbar has to be placed to window with csd or solid-csd style + // to properly draw the decorated. + gtk_style_context_add_class(windowStyle, + IsSolidCSDStyleUsed() ? "solid-csd" : "csd"); + + GtkWidget* fixed = gtk_fixed_new(); + GtkStyleContext* fixedStyle = gtk_widget_get_style_context(fixed); + gtk_style_context_add_class(fixedStyle, "titlebar"); + + GtkWidget* headerBar = gtk_header_bar_new(); + + // Emulate what create_titlebar() at gtkwindow.c does. + GtkStyleContext* headerBarStyle = gtk_widget_get_style_context(headerBar); + gtk_style_context_add_class(headerBarStyle, "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(headerBarStyle, "default-decoration"); + + sWidgetStorage[aAppearance] = headerBar; + if (aAppearance == MOZ_GTK_HEADER_BAR_MAXIMIZED) { + MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED], + "Window widget is already created!"); + MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED], + "Fixed widget is already created!"); + + gtk_style_context_add_class(windowStyle, "maximized"); + + sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED] = window; + sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED] = fixed; + } else { + MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW], + "Window widget is already created!"); + MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED], + "Fixed widget is already created!"); + sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW] = window; + sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED] = fixed; + } + + gtk_container_add(GTK_CONTAINER(window), fixed); + gtk_container_add(GTK_CONTAINER(fixed), headerBar); + + gtk_style_context_invalidate(headerBarStyle); + gtk_style_context_invalidate(fixedStyle); + + // Some themes like Elementary's style the container of the headerbar rather + // than the header bar itself. + bool& shouldDrawContainer = aAppearance == MOZ_GTK_HEADER_BAR + ? gHeaderBarShouldDrawContainer + : gMaximizedHeaderBarShouldDrawContainer; + shouldDrawContainer = [&] { + const bool headerBarHasBackground = HasBackground(headerBarStyle); + if (headerBarHasBackground && GetBorderRadius(headerBarStyle)) { + return false; + } + if (HasBackground(fixedStyle) && + (GetBorderRadius(fixedStyle) || !headerBarHasBackground)) { + return true; + } + return false; + }(); +} + +#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 */ + for (int scale = 1; scale < ICON_SCALE_VARIANTS + 1; scale++) { + GtkIconInfo* gtkIconInfo = gtk_icon_theme_lookup_icon_for_scale( + 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 = + gdk_cairo_surface_create_from_pixbuf(iconPixbuf, scale, nullptr); + g_object_unref(iconPixbuf); + + nsPrintfCString surfaceName("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; + } + + nsPrintfCString surfaceName("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], + "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; +} + +bool IsSolidCSDStyleUsed() { + if (gCSDStyle == CSDStyle::Unknown) { + bool solid; + { + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_titlebar(GTK_WINDOW(window), gtk_header_bar_new()); + gtk_widget_realize(window); + GtkStyleContext* windowStyle = gtk_widget_get_style_context(window); + solid = gtk_style_context_has_class(windowStyle, "solid-csd"); + gtk_widget_destroy(window); + } + gCSDStyle = solid ? CSDStyle::Solid : CSDStyle::Normal; + } + return gCSDStyle == CSDStyle::Solid; +} + +static void CreateHeaderBarButtons() { + GtkWidget* headerBar = sWidgetStorage[MOZ_GTK_HEADER_BAR]; + MOZ_ASSERT(headerBar, "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(headerBar), 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_MAXIMIZED), + 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_VERTICAL: + return CreateScrollbarWidget(aAppearance, GTK_ORIENTATION_VERTICAL); + case MOZ_GTK_MENUPOPUP: + return CreateMenuPopupWidget(); + 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_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_HEADERBAR_FIXED: + case MOZ_GTK_HEADERBAR_FIXED_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_MENUITEM: + style = CreateStyleForWidget(gtk_menu_item_new(), 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_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_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_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_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_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_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_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_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() { + for (int i = 0; i < MOZ_GTK_WIDGET_NODE_COUNT; i++) { + if (sStyleStorage[i]) g_object_unref(sStyleStorage[i]); + } + mozilla::PodArrayZero(sStyleStorage); + + gCSDStyle = CSDStyle::Unknown; + + /* 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); + } +} + +bool HeaderBarShouldDrawContainer(WidgetNodeType aNodeType) { + MOZ_ASSERT(aNodeType == MOZ_GTK_HEADER_BAR || + aNodeType == MOZ_GTK_HEADER_BAR_MAXIMIZED); + mozilla::Unused << GetWidget(aNodeType); + return aNodeType == MOZ_GTK_HEADER_BAR + ? gHeaderBarShouldDrawContainer + : gMaximizedHeaderBarShouldDrawContainer; +} + +gint GetBorderRadius(GtkStyleContext* aStyle) { + GValue value = G_VALUE_INIT; + // NOTE(emilio): In an ideal world, we'd query the two longhands + // (border-top-left-radius and border-top-right-radius) separately. However, + // that doesn't work (GTK rejects the query with: + // + // Style property "border-top-left-radius" is not gettable + // + // However! Getting border-radius does work, and it does return the + // border-top-left-radius as a gint: + // + // https://docs.gtk.org/gtk3/const.STYLE_PROPERTY_BORDER_RADIUS.html + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-20/gtk/gtkcssshorthandpropertyimpl.c#L961-977 + // + // So we abuse this fact, and make the assumption here that the + // border-top-{left,right}-radius are the same, and roll with it. + gtk_style_context_get_property(aStyle, "border-radius", GTK_STATE_FLAG_NORMAL, + &value); + gint result = 0; + auto type = G_VALUE_TYPE(&value); + if (type == G_TYPE_INT) { + result = g_value_get_int(&value); + } else { + NS_WARNING(nsPrintfCString("Unknown value type %lu for border-radius", type) + .get()); + } + g_value_unset(&value); + return result; +} diff --git a/widget/gtk/WidgetStyleCache.h b/widget/gtk/WidgetStyleCache.h new file mode 100644 index 0000000000..f38b75fae6 --- /dev/null +++ b/widget/gtk/WidgetStyleCache.h @@ -0,0 +1,63 @@ +/* -*- 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(); + +bool IsSolidCSDStyleUsed(); + +void StyleContextSetScale(GtkStyleContext* style, gint aScaleFactor); + +gint GetBorderRadius(GtkStyleContext* aStyle); + +bool HeaderBarShouldDrawContainer(WidgetNodeType); + +#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..d9d688587b --- /dev/null +++ b/widget/gtk/WidgetUtilsGtk.cpp @@ -0,0 +1,496 @@ +/* -*- 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 "MainThreadUtils.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/UniquePtr.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsWindow.h" +#include "nsIGfxInfo.h" +#include "mozilla/Components.h" +#include "nsCOMPtr.h" +#include "nsIProperties.h" +#include "nsIFile.h" +#include "nsXULAppAPI.h" +#include "nsXPCOMCID.h" +#include "nsDirectoryServiceDefs.h" +#include "nsString.h" +#include "nsGtkKeyUtils.h" +#include "nsGtkUtils.h" + +#include <gtk/gtk.h> +#include <dlfcn.h> +#include <glib.h> + +#undef LOGW +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +extern mozilla::LazyLogModule gWidgetLog; +# define LOGW(...) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOGW(...) +#endif /* MOZ_LOGGING */ + +namespace mozilla::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(); +} + +// We avoid linking gdk_*_display_get_type directly in order to avoid a runtime +// dependency on GTK built with both backends. Other X11- and Wayland-specific +// functions get stubbed out by libmozgtk and crash when called, but those +// should only be called when the matching backend is already in use. + +bool GdkIsWaylandDisplay(GdkDisplay* display) { + static auto sGdkWaylandDisplayGetType = + (GType(*)())dlsym(RTLD_DEFAULT, "gdk_wayland_display_get_type"); + return sGdkWaylandDisplayGetType && + G_TYPE_CHECK_INSTANCE_TYPE(display, sGdkWaylandDisplayGetType()); +} + +bool GdkIsX11Display(GdkDisplay* display) { + static auto sGdkX11DisplayGetType = + (GType(*)())dlsym(RTLD_DEFAULT, "gdk_x11_display_get_type"); + return sGdkX11DisplayGetType && + G_TYPE_CHECK_INSTANCE_TYPE(display, sGdkX11DisplayGetType()); +} + +bool IsXWaylandProtocol() { + static bool isXwayland = [] { + return !GdkIsWaylandDisplay() && !!getenv("WAYLAND_DISPLAY"); + }(); + return isXwayland; +} + +bool GdkIsWaylandDisplay() { + static bool isWaylandDisplay = gdk_display_get_default() && + GdkIsWaylandDisplay(gdk_display_get_default()); + return isWaylandDisplay; +} + +bool GdkIsX11Display() { + static bool isX11Display = gdk_display_get_default() + ? GdkIsX11Display(gdk_display_get_default()) + : false; + return isX11Display; +} + +GdkDevice* GdkGetPointer() { + GdkDisplay* display = gdk_display_get_default(); + GdkDeviceManager* deviceManager = gdk_display_get_device_manager(display); + return gdk_device_manager_get_client_pointer(deviceManager); +} + +static GdkEvent* sLastMousePressEvent = nullptr; +GdkEvent* GetLastMousePressEvent() { return sLastMousePressEvent; } + +void SetLastMousePressEvent(GdkEvent* aEvent) { + if (sLastMousePressEvent) { + GUniquePtr<GdkEvent> event(sLastMousePressEvent); + sLastMousePressEvent = nullptr; + } + if (!aEvent) { + return; + } + GUniquePtr<GdkEvent> event(gdk_event_copy(aEvent)); + sLastMousePressEvent = event.release(); +} + +bool IsRunningUnderSnap() { return !!GetSnapInstanceName(); } + +bool IsRunningUnderFlatpak() { + // https://gitlab.gnome.org/GNOME/gtk/-/blob/4300a5c609306ce77cbc8a3580c19201dccd8d13/gdk/gdk.c#L472 + static bool sRunning = [] { + return g_file_test("/.flatpak-info", G_FILE_TEST_EXISTS); + }(); + return sRunning; +} + +bool IsPackagedAppFileExists() { + static bool sRunning = [] { + nsresult rv; + nsCString path; + nsCOMPtr<nsIFile> file; + nsCOMPtr<nsIProperties> directoryService; + + directoryService = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(directoryService, FALSE); + + rv = directoryService->Get(NS_GRE_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, FALSE); + + rv = file->AppendNative("is-packaged-app"_ns); + NS_ENSURE_SUCCESS(rv, FALSE); + + rv = file->GetNativePath(path); + NS_ENSURE_SUCCESS(rv, FALSE); + + return g_file_test(path.get(), G_FILE_TEST_EXISTS); + }(); + return sRunning; +} + +const char* GetSnapInstanceName() { + static const char* sInstanceName = []() -> const char* { + const char* snapName = g_getenv("SNAP_NAME"); + if (!snapName) { + return nullptr; + } + if (g_strcmp0(snapName, MOZ_APP_NAME)) { + return nullptr; + } + // Intentionally leaked, as keeping a pointer to the environment forever + // is a bit suspicious. + if (const char* instanceName = g_getenv("SNAP_INSTANCE_NAME")) { + return g_strdup(instanceName); + } + // Instance name didn't exist for snapd <= 2.35: + return g_strdup(snapName); + }(); + return sInstanceName; +} + +bool ShouldUsePortal(PortalKind aPortalKind) { + static bool sPortalEnv = [] { + if (IsRunningUnderFlatpakOrSnap()) { + return true; + } + const char* portalEnvString = g_getenv("GTK_USE_PORTAL"); + return portalEnvString && atoi(portalEnvString) != 0; + }(); + + bool autoBehavior = sPortalEnv; + const int32_t pref = [&] { + switch (aPortalKind) { + case PortalKind::FilePicker: + return StaticPrefs::widget_use_xdg_desktop_portal_file_picker(); + case PortalKind::MimeHandler: + // Mime portal breaks default browser handling, see bug 1516290. + autoBehavior = IsRunningUnderFlatpakOrSnap(); + return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler(); + case PortalKind::Settings: + autoBehavior = true; + return StaticPrefs::widget_use_xdg_desktop_portal_settings(); + case PortalKind::Location: + return StaticPrefs::widget_use_xdg_desktop_portal_location(); + case PortalKind::OpenUri: + return StaticPrefs::widget_use_xdg_desktop_portal_open_uri(); + } + return 2; + }(); + + switch (pref) { + case 0: + return false; + case 1: + return true; + default: + return autoBehavior; + } +} + +nsTArray<nsCString> ParseTextURIList(const nsACString& aData) { + UniquePtr<char[]> data(ToNewCString(aData)); + gchar** uris = g_uri_list_extract_uris(data.get()); + + nsTArray<nsCString> result; + for (size_t i = 0; i < g_strv_length(uris); i++) { + result.AppendElement(nsCString(uris[i])); + } + + g_strfreev(uris); + return result; +} + +#ifdef MOZ_WAYLAND +static gboolean token_failed(gpointer aData); + +class XDGTokenRequest { + public: + void SetTokenID(const char* aTokenID) { + LOGW("RequestWaylandFocusPromise() SetTokenID %s", aTokenID); + mTransferPromise->Resolve(aTokenID, __func__); + } + void Cancel() { + LOGW("RequestWaylandFocusPromise() canceled"); + mTransferPromise->Reject(false, __func__); + mActivationTimeoutID = 0; + } + + XDGTokenRequest(xdg_activation_token_v1* aXdgToken, + RefPtr<FocusRequestPromise::Private> aTransferPromise) + : mXdgToken(aXdgToken), mTransferPromise(std::move(aTransferPromise)) { + mActivationTimeoutID = + g_timeout_add(sActivationTimeout, token_failed, this); + } + ~XDGTokenRequest() { + MozClearPointer(mXdgToken, xdg_activation_token_v1_destroy); + if (mActivationTimeoutID) { + g_source_remove(mActivationTimeoutID); + } + } + + private: + xdg_activation_token_v1* mXdgToken; + RefPtr<FocusRequestPromise::Private> mTransferPromise; + guint mActivationTimeoutID; + // Reject FocusRequestPromise if we don't get XDG token in 0.5 sec. + static constexpr int sActivationTimeout = 500; +}; + +// Failed to get token in time +static gboolean token_failed(gpointer data) { + UniquePtr<XDGTokenRequest> request(static_cast<XDGTokenRequest*>(data)); + request->Cancel(); + return false; +} + +// We've got activation token from Wayland compositor so it's time to use it. +static void token_done(gpointer data, struct xdg_activation_token_v1* provider, + const char* tokenID) { + UniquePtr<XDGTokenRequest> request(static_cast<XDGTokenRequest*>(data)); + request->SetTokenID(tokenID); +} + +static const struct xdg_activation_token_v1_listener token_listener = { + token_done, +}; +#endif + +RefPtr<FocusRequestPromise> RequestWaylandFocusPromise() { +#ifdef MOZ_WAYLAND + if (!GdkIsWaylandDisplay() || !KeymapWrapper::GetSeat()) { + LOGW("RequestWaylandFocusPromise() failed."); + return nullptr; + } + + RefPtr<nsWindow> sourceWindow = nsWindow::GetFocusedWindow(); + if (!sourceWindow || sourceWindow->IsDestroyed()) { + LOGW("RequestWaylandFocusPromise() missing source window"); + return nullptr; + } + + xdg_activation_v1* xdg_activation = WaylandDisplayGet()->GetXdgActivation(); + if (!xdg_activation) { + LOGW("RequestWaylandFocusPromise() missing xdg_activation"); + return nullptr; + } + + wl_surface* focusSurface; + uint32_t focusSerial; + KeymapWrapper::GetFocusInfo(&focusSurface, &focusSerial); + if (!focusSurface) { + LOGW("RequestWaylandFocusPromise() missing focusSurface"); + return nullptr; + } + + GdkWindow* gdkWindow = gtk_widget_get_window(sourceWindow->GetGtkWidget()); + if (!gdkWindow) { + return nullptr; + } + wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWindow); + if (focusSurface != surface) { + LOGW("RequestWaylandFocusPromise() missing wl_surface"); + return nullptr; + } + + RefPtr<FocusRequestPromise::Private> transferPromise = + new FocusRequestPromise::Private(__func__); + + xdg_activation_token_v1* aXdgToken = + xdg_activation_v1_get_activation_token(xdg_activation); + xdg_activation_token_v1_add_listener( + aXdgToken, &token_listener, + new XDGTokenRequest(aXdgToken, transferPromise)); + xdg_activation_token_v1_set_serial(aXdgToken, focusSerial, + KeymapWrapper::GetSeat()); + xdg_activation_token_v1_set_surface(aXdgToken, focusSurface); + xdg_activation_token_v1_commit(aXdgToken); + + LOGW("RequestWaylandFocusPromise() XDG Token sent"); + + return transferPromise.forget(); +#else // !defined(MOZ_WAYLAND) + return nullptr; +#endif +} + +// https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html +static nsCString GetWindowManagerName() { + if (!GdkIsX11Display()) { + return {}; + } + +#ifdef MOZ_X11 + Display* xdisplay = gdk_x11_get_default_xdisplay(); + Window root_win = + GDK_WINDOW_XID(gdk_screen_get_root_window(gdk_screen_get_default())); + + 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 {}; + } + 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 {}; + } + + Window wmWindow = reinterpret_cast<Window*>(prop_return)[0]; + if (!wmWindow) { + return {}; + } + + 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 {}; + } + 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 {}; + } + + return nsCString(reinterpret_cast<const char*>(prop_return)); +#else + return {}; +#endif +} + +// Getting a reliable identifier is quite tricky. We try to use the standard +// XDG_CURRENT_DESKTOP environment first, _NET_WM_NAME later, and a set of +// legacy / non-standard environment variables otherwise. +// +// Documentation for some of those can be found in: +// +// https://wiki.archlinux.org/title/Environment_variables#Examples +// https://wiki.archlinux.org/title/Xdg-utils#Environment_variables +const nsCString& GetDesktopEnvironmentIdentifier() { + MOZ_ASSERT(NS_IsMainThread()); + static const nsDependentCString sIdentifier = [] { + nsCString ident = [] { + auto Env = [](const char* aKey) -> const char* { + const char* v = getenv(aKey); + return v && *v ? v : nullptr; + }; + if (const char* currentDesktop = Env("XDG_CURRENT_DESKTOP")) { + return nsCString(currentDesktop); + } + if (auto wm = GetWindowManagerName(); !wm.IsEmpty()) { + return wm; + } + if (const char* sessionDesktop = Env("XDG_SESSION_DESKTOP")) { + // This is not really standardized in freedesktop.org, but it is + // documented here, and should be set in systemd systems. + // https://www.freedesktop.org/software/systemd/man/pam_systemd.html#%24XDG_SESSION_DESKTOP + return nsCString(sessionDesktop); + } + // We try first the DE-specific variables, then SESSION_DESKTOP, to match + // the documented order in: + // https://wiki.archlinux.org/title/Xdg-utils#Environment_variables + if (getenv("GNOME_DESKTOP_SESSION_ID")) { + return nsCString("gnome"_ns); + } + if (getenv("KDE_FULL_SESSION")) { + return nsCString("kde"_ns); + } + if (getenv("MATE_DESKTOP_SESSION_ID")) { + return nsCString("mate"_ns); + } + if (getenv("LXQT_SESSION_CONFIG")) { + return nsCString("lxqt"_ns); + } + if (const char* desktopSession = Env("DESKTOP_SESSION")) { + // Try the legacy DESKTOP_SESSION as a last resort. + return nsCString(desktopSession); + } + return nsCString(); + }(); + ToLowerCase(ident); + // Intentionally put into a ToNewCString copy, rather than just making a + // static nsCString to avoid leakchecking errors, since we really want to + // leak this string. + return nsDependentCString(ToNewCString(ident), ident.Length()); + }(); + return sIdentifier; +} + +bool IsGnomeDesktopEnvironment() { + static bool sIsGnome = + !!FindInReadable("gnome"_ns, GetDesktopEnvironmentIdentifier()); + return sIsGnome; +} + +bool IsKdeDesktopEnvironment() { + static bool sIsKde = GetDesktopEnvironmentIdentifier().EqualsLiteral("kde"); + return sIsKde; +} + +} // namespace mozilla::widget diff --git a/widget/gtk/WidgetUtilsGtk.h b/widget/gtk/WidgetUtilsGtk.h new file mode 100644 index 0000000000..dd49e96c50 --- /dev/null +++ b/widget/gtk/WidgetUtilsGtk.h @@ -0,0 +1,80 @@ +/* -*- 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 "nsString.h" +#include "nsTArray.h" +#include "mozilla/MozPromise.h" + +#include <stdint.h> + +typedef struct _GdkDisplay GdkDisplay; +typedef struct _GdkDevice GdkDevice; +typedef union _GdkEvent GdkEvent; +class nsWindow; + +namespace mozilla::widget { + +class WidgetUtilsGTK { + public: + /* See WidgetUtils::IsTouchDeviceSupportPresent(). */ + static int32_t IsTouchDeviceSupportPresent(); +}; + +bool IsMainWindowTransparent(); + +bool GdkIsWaylandDisplay(GdkDisplay* display); +bool GdkIsX11Display(GdkDisplay* display); + +bool GdkIsWaylandDisplay(); +bool GdkIsX11Display(); + +bool IsXWaylandProtocol(); + +GdkDevice* GdkGetPointer(); + +// Sets / returns the last mouse press event we processed. +void SetLastMousePressEvent(GdkEvent*); +GdkEvent* GetLastMousePressEvent(); + +// Return the snap's instance name, or null when not running as a snap. +const char* GetSnapInstanceName(); +bool IsRunningUnderSnap(); +bool IsRunningUnderFlatpak(); +bool IsPackagedAppFileExists(); +inline bool IsRunningUnderFlatpakOrSnap() { + return IsRunningUnderFlatpak() || IsRunningUnderSnap(); +} + +enum class PortalKind { + FilePicker, + MimeHandler, + Settings, + Location, + OpenUri, +}; +bool ShouldUsePortal(PortalKind); + +// Tries to get a descriptive identifier for the desktop environment that the +// program is running under. Always normalized to lowercase. +// See the implementation for the different environment variables and desktop +// information we try to use. +// +// If we can't find a reasonable environment, the empty string is returned. +const nsCString& GetDesktopEnvironmentIdentifier(); +bool IsGnomeDesktopEnvironment(); +bool IsKdeDesktopEnvironment(); + +// Parse text/uri-list +nsTArray<nsCString> ParseTextURIList(const nsACString& data); + +using FocusRequestPromise = MozPromise<nsCString, bool, false>; +RefPtr<FocusRequestPromise> RequestWaylandFocusPromise(); + +} // namespace mozilla::widget + +#endif // WidgetUtilsGtk_h__ diff --git a/widget/gtk/WindowSurface.h b/widget/gtk/WindowSurface.h new file mode 100644 index 0000000000..858ddde59e --- /dev/null +++ b/widget/gtk/WindowSurface.h @@ -0,0 +1,41 @@ +/* -*- 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_WINDOW_SURFACE_H +#define _MOZILLA_WIDGET_WINDOW_SURFACE_H + +#include "mozilla/gfx/2D.h" +#include "Units.h" + +namespace mozilla { +namespace widget { + +// A class for drawing to double-buffered windows. +class WindowSurface { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowSurface); + + // Locks a region of the window for drawing, returning a draw target + // capturing the bounds of the provided region. + // Implementations must permit invocation from any thread. + virtual already_AddRefed<gfx::DrawTarget> Lock( + const LayoutDeviceIntRegion& aRegion) = 0; + + // Swaps the provided invalid region from the back buffer to the window. + // Implementations must permit invocation from any thread. + virtual void Commit(const LayoutDeviceIntRegion& aInvalidRegion) = 0; + + // Whether the window surface represents a fallback method. + virtual bool IsFallback() const { return false; } + + protected: + virtual ~WindowSurface() = default; +}; + +} // namespace widget +} // namespace mozilla + +#endif // _MOZILLA_WIDGET_WINDOW_SURFACE_H diff --git a/widget/gtk/WindowSurfaceProvider.cpp b/widget/gtk/WindowSurfaceProvider.cpp new file mode 100644 index 0000000000..faf147a80a --- /dev/null +++ b/widget/gtk/WindowSurfaceProvider.cpp @@ -0,0 +1,202 @@ +/* -*- 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 "GtkCompositorWidget.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/layers/LayersTypes.h" +#include "nsWindow.h" + +#ifdef MOZ_WAYLAND +# include "mozilla/StaticPrefs_widget.h" +# include "WindowSurfaceWaylandMultiBuffer.h" +#endif +#ifdef MOZ_X11 +# include "mozilla/X11Util.h" +# include "WindowSurfaceX11Image.h" +# include "WindowSurfaceX11SHM.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 */ + +namespace mozilla { +namespace widget { + +using namespace mozilla::layers; + +WindowSurfaceProvider::WindowSurfaceProvider() + : mWindowSurface(nullptr), + mMutex("WindowSurfaceProvider"), + mWindowSurfaceValid(false) +#ifdef MOZ_X11 + , + mIsShaped(false), + mXDepth(0), + mXWindow(0), + mXVisual(nullptr) +#endif +{ +} + +#ifdef MOZ_WAYLAND +void WindowSurfaceProvider::Initialize(RefPtr<nsWindow> aWidget) { + mWindowSurfaceValid = false; + mWidget = std::move(aWidget); +} +void WindowSurfaceProvider::Initialize(GtkCompositorWidget* aCompositorWidget) { + mWindowSurfaceValid = false; + mCompositorWidget = aCompositorWidget; + mWidget = static_cast<nsWindow*>(aCompositorWidget->RealWidget()); +} +#endif +#ifdef MOZ_X11 +void WindowSurfaceProvider::Initialize(Window aWindow, Visual* aVisual, + int aDepth, bool aIsShaped) { + mWindowSurfaceValid = false; + mXWindow = aWindow; + mXVisual = aVisual; + mXDepth = aDepth; + mIsShaped = aIsShaped; +} +#endif + +void WindowSurfaceProvider::CleanupResources() { + MutexAutoLock lock(mMutex); + mWindowSurfaceValid = false; +#ifdef MOZ_WAYLAND + mWidget = nullptr; +#endif +#ifdef MOZ_X11 + mXWindow = 0; + mXVisual = 0; + mXDepth = 0; + mIsShaped = false; +#endif +} + +RefPtr<WindowSurface> WindowSurfaceProvider::CreateWindowSurface() { +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + // We're called too early or we're unmapped. + if (!mWidget) { + return nullptr; + } + return MakeRefPtr<WindowSurfaceWaylandMB>(mWidget, mCompositorWidget); + } +#endif +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + // We're called too early or we're unmapped. + if (!mXWindow) { + return nullptr; + } + // Blit to the window with the following priority: + // 1. MIT-SHM + // 2. XPutImage +# ifdef MOZ_HAVE_SHMIMAGE + if (!mIsShaped && nsShmImage::UseShm()) { + LOG(("Drawing to Window 0x%lx will use MIT-SHM\n", mXWindow)); + return MakeRefPtr<WindowSurfaceX11SHM>(DefaultXDisplay(), mXWindow, + mXVisual, mXDepth); + } +# endif // MOZ_HAVE_SHMIMAGE + + LOG(("Drawing to Window 0x%lx will use XPutImage\n", mXWindow)); + return MakeRefPtr<WindowSurfaceX11Image>(DefaultXDisplay(), mXWindow, + mXVisual, mXDepth, mIsShaped); + } +#endif + MOZ_RELEASE_ASSERT(false); +} + +already_AddRefed<gfx::DrawTarget> +WindowSurfaceProvider::StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) { + if (aInvalidRegion.IsEmpty()) { + return nullptr; + } + + MutexAutoLock lock(mMutex); + + if (!mWindowSurfaceValid) { + mWindowSurface = nullptr; + mWindowSurfaceValid = true; + } + + if (!mWindowSurface) { + mWindowSurface = CreateWindowSurface(); + if (!mWindowSurface) { + return nullptr; + } + } + + *aBufferMode = BufferMode::BUFFER_NONE; + RefPtr<gfx::DrawTarget> dt = mWindowSurface->Lock(aInvalidRegion); +#ifdef MOZ_X11 + if (!dt && GdkIsX11Display() && !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 = MakeRefPtr<WindowSurfaceX11Image>( + DefaultXDisplay(), mXWindow, mXVisual, mXDepth, mIsShaped); + dt = mWindowSurface->Lock(aInvalidRegion); + } +#endif + return dt.forget(); +} + +void WindowSurfaceProvider::EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + MutexAutoLock lock(mMutex); + // Commit to mWindowSurface only if we have a valid one. + if (!mWindowSurface || !mWindowSurfaceValid) { + return; + } +#if defined(MOZ_WAYLAND) + if (GdkIsWaylandDisplay()) { + // We're called too early or we're unmapped. + // Don't draw anything. + if (!mWidget || !mWidget->IsMapped()) { + return; + } + if (moz_container_wayland_is_commiting_to_parent( + mWidget->GetMozContainer())) { + // If we're drawing directly to wl_surface owned by Gtk we need to use it + // in main thread to sync with Gtk access to it. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "WindowSurfaceProvider::EndRemoteDrawingInRegion", + [widget = RefPtr{mWidget}, this, aInvalidRegion]() { + if (!widget->IsMapped()) { + return; + } + MutexAutoLock lock(mMutex); + // Commit to mWindowSurface only when we have a valid one. + if (mWindowSurface && mWindowSurfaceValid) { + mWindowSurface->Commit(aInvalidRegion); + } + })); + return; + } + } +#endif + 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..353fa82f70 --- /dev/null +++ b/widget/gtk/WindowSurfaceProvider.h @@ -0,0 +1,101 @@ +/* -*- 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 <gdk/gdk.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 "mozilla/ScopeExit.h" + +#ifdef MOZ_X11 +# include <X11/Xlib.h> // for Window, Display, Visual, etc. +# include "X11UndefineNone.h" +#endif + +class nsWindow; + +namespace mozilla { +namespace widget { + +class GtkCompositorWidget; + +/* + * 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(); + ~WindowSurfaceProvider() = default; + + /** + * 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. + */ +#ifdef MOZ_WAYLAND + void Initialize(RefPtr<nsWindow> aWidget); + void Initialize(GtkCompositorWidget* aCompositorWidget); +#endif +#ifdef MOZ_X11 + void Initialize(Window aWindow, Visual* aVisual, int aDepth, bool aIsShaped); +#endif + + /** + * Releases any surfaces created by this provider. + * This is used by GtkCompositorWidget to get rid + * of resources. + */ + void CleanupResources(); + + already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode); + void EndRemoteDrawingInRegion(gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion); + + private: + RefPtr<WindowSurface> CreateWindowSurface(); + void CleanupWindowSurface(); + + RefPtr<WindowSurface> mWindowSurface; + + /* While CleanupResources() can be called from Main thread when nsWindow is + * destroyed/hidden, StartRemoteDrawingInRegion()/EndRemoteDrawingInRegion() + * is called from Compositor thread during rendering. + * + * As nsWindow CleanupResources() call comes from Gtk/X11 we can't synchronize + * that with WebRender so we use lock to synchronize the access. + */ + mozilla::Mutex mMutex MOZ_UNANNOTATED; + // WindowSurface needs to be re-created as underlying window was changed. + mozilla::Atomic<bool> mWindowSurfaceValid; +#ifdef MOZ_WAYLAND + RefPtr<nsWindow> mWidget; + // WindowSurfaceProvider is owned by GtkCompositorWidget so we don't need + // to reference it. + GtkCompositorWidget* mCompositorWidget = nullptr; +#endif +#ifdef MOZ_X11 + bool mIsShaped; + int mXDepth; + Window mXWindow; + Visual* mXVisual; +#endif +}; + +} // namespace widget +} // namespace mozilla + +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H diff --git a/widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp b/widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp new file mode 100644 index 0000000000..31091f4b98 --- /dev/null +++ b/widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp @@ -0,0 +1,417 @@ +/* -*- 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 "WindowSurfaceWaylandMultiBuffer.h" + +#include <errno.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <prenv.h> + +#include "gfx2DGlue.h" +#include "gfxPlatform.h" +#include "MozContainer.h" +#include "GtkCompositorWidget.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "mozilla/gfx/Tools.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/WidgetUtils.h" + +#undef LOG +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "Units.h" +extern mozilla::LazyLogModule gWidgetWaylandLog; +# define LOGWAYLAND(...) \ + MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOGWAYLAND(...) +#endif /* MOZ_LOGGING */ + +namespace mozilla::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 | + | | | ------------------ + | | ----------------------- | + | | | WaylandBufferSHM | | + | | | | | + | | | ------------------- | | + | | | | WaylandShmPool | | | + | | | ------------------- | | + | | ----------------------- | + | | | + | | ----------------------- | + | | | WaylandBufferSHM | | + | | | | | + | | | ------------------- | | + | | | | WaylandShmPool | | | + | | | ------------------- | | + | | ----------------------- | + | --------------------------------- + | + | + --------------------------------- ------------------ + | WindowSurfaceWayland |<------>| nsWindow | + | | ------------------ + | ----------------------- | + | | WaylandBufferSHM | | + | | | | + | | ------------------- | | + | | | WaylandShmPool | | | + | | ------------------- | | + | ----------------------- | + | | + | ----------------------- | + | | WaylandBufferSHM | | + | | | | + | | ------------------- | | + | | | 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 WaylandBufferSHM) +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 available or discard the +WindowImageSurface cache when whole screen is invalidated. + +WaylandBufferSHM + +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. + +WaylandBufferSHM 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 WaylandBufferSHM. +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 WaylandBufferSHM/WindowSurfaceWayland +(wl_buffer/wl_surface). +*/ + +using gfx::DataSourceSurface; + +#define BACK_BUFFER_NUM 3 + +WindowSurfaceWaylandMB::WindowSurfaceWaylandMB( + RefPtr<nsWindow> aWindow, GtkCompositorWidget* aCompositorWidget) + : mSurfaceLock("WindowSurfaceWayland lock"), + mWindow(std::move(aWindow)), + mCompositorWidget(aCompositorWidget), + mFrameInProcess(false), + mCallbackRequested(false) {} + +bool WindowSurfaceWaylandMB::MaybeUpdateWindowSize() { + // We want to get window size from compositor widget as it matches window + // size used by parent RenderCompositorSWGL rendrer. + // For main thread rendering mCompositorWidget is not available so get + // window size directly from nsWindow. + LayoutDeviceIntSize newWindowSize = mCompositorWidget + ? mCompositorWidget->GetClientSize() + : mWindow->GetClientSize(); + if (mWindowSize != newWindowSize) { + mWindowSize = newWindowSize; + return true; + } + return false; +} + +already_AddRefed<DrawTarget> WindowSurfaceWaylandMB::Lock( + const LayoutDeviceIntRegion& aInvalidRegion) { + MutexAutoLock lock(mSurfaceLock); + +#ifdef MOZ_LOGGING + gfx::IntRect lockRect = aInvalidRegion.GetBounds().ToUnknownRect(); + LOGWAYLAND("WindowSurfaceWaylandMB::Lock [%p] [%d,%d] -> [%d x %d] rects %d", + (void*)mWindow.get(), lockRect.x, lockRect.y, lockRect.width, + lockRect.height, aInvalidRegion.GetNumRects()); +#endif + + if (mWindow->GetWindowType() == WindowType::Invisible) { + return nullptr; + } + mFrameInProcess = true; + + CollectPendingSurfaces(lock); + + if (MaybeUpdateWindowSize()) { + LOGWAYLAND(" new window size [%d x %d]", mWindowSize.width, + mWindowSize.height); + if (mInProgressBuffer) { + ReturnBufferToPool(lock, mInProgressBuffer); + mInProgressBuffer = nullptr; + } + if (mFrontBuffer) { + ReturnBufferToPool(lock, mFrontBuffer); + mFrontBuffer = nullptr; + } + mAvailableBuffers.Clear(); + } + + if (!mInProgressBuffer) { + if (mFrontBuffer && !mFrontBuffer->IsAttached()) { + mInProgressBuffer = mFrontBuffer; + } else { + mInProgressBuffer = ObtainBufferFromPool(lock, mWindowSize); + if (!mInProgressBuffer) { + return nullptr; + } + if (mFrontBuffer) { + HandlePartialUpdate(lock, aInvalidRegion); + ReturnBufferToPool(lock, mFrontBuffer); + } + } + mFrontBuffer = nullptr; + mFrontBufferInvalidRegion.SetEmpty(); + } + + RefPtr<DrawTarget> dt = mInProgressBuffer->Lock(); + return dt.forget(); +} + +void WindowSurfaceWaylandMB::HandlePartialUpdate( + const MutexAutoLock& aProofOfLock, + const LayoutDeviceIntRegion& aInvalidRegion) { + LayoutDeviceIntRegion copyRegion; + if (mInProgressBuffer->GetBufferAge() == 2) { + copyRegion.Sub(mFrontBufferInvalidRegion, aInvalidRegion); + } else { + LayoutDeviceIntSize frontBufferSize = mFrontBuffer->GetSize(); + copyRegion = LayoutDeviceIntRegion(LayoutDeviceIntRect( + 0, 0, frontBufferSize.width, frontBufferSize.height)); + copyRegion.SubOut(aInvalidRegion); + } + + if (!copyRegion.IsEmpty()) { + RefPtr<DataSourceSurface> dataSourceSurface = + mozilla::gfx::CreateDataSourceSurfaceFromData( + mFrontBuffer->GetSize().ToUnknownSize(), + mFrontBuffer->GetSurfaceFormat(), + (const uint8_t*)mFrontBuffer->GetShmPool()->GetImageData(), + mFrontBuffer->GetSize().width * + BytesPerPixel(mFrontBuffer->GetSurfaceFormat())); + RefPtr<DrawTarget> dt = mInProgressBuffer->Lock(); + + for (auto iter = copyRegion.RectIter(); !iter.Done(); iter.Next()) { + LayoutDeviceIntRect r = iter.Get(); + dt->CopySurface(dataSourceSurface, r.ToUnknownRect(), + gfx::IntPoint(r.x, r.y)); + } + } +} + +void WindowSurfaceWaylandMB::Commit( + const LayoutDeviceIntRegion& aInvalidRegion) { + MutexAutoLock lock(mSurfaceLock); + Commit(lock, aInvalidRegion); +} + +void WindowSurfaceWaylandMB::Commit( + const MutexAutoLock& aProofOfLock, + const LayoutDeviceIntRegion& aInvalidRegion) { +#ifdef MOZ_LOGGING + gfx::IntRect invalidRect = aInvalidRegion.GetBounds().ToUnknownRect(); + LOGWAYLAND( + "WindowSurfaceWaylandMB::Commit [%p] damage rect [%d, %d] -> [%d x %d] " + "Window [%d x %d]\n", + (void*)mWindow.get(), invalidRect.x, invalidRect.y, invalidRect.width, + invalidRect.height, mWindowSize.width, mWindowSize.height); +#endif + + if (!mInProgressBuffer) { + // invisible window + return; + } + mFrameInProcess = false; + + MozContainer* container = mWindow->GetMozContainer(); + MozContainerSurfaceLock MozContainerLock(container); + struct wl_surface* waylandSurface = MozContainerLock.GetSurface(); + if (!waylandSurface) { + LOGWAYLAND( + "WindowSurfaceWaylandMB::Commit [%p] frame queued: can't lock " + "wl_surface\n", + (void*)mWindow.get()); + if (!mCallbackRequested) { + RefPtr<WindowSurfaceWaylandMB> self(this); + moz_container_wayland_add_initial_draw_callback_locked( + container, [self, aInvalidRegion]() -> void { + MutexAutoLock lock(self->mSurfaceLock); + if (!self->mFrameInProcess) { + self->Commit(lock, aInvalidRegion); + } + self->mCallbackRequested = false; + }); + mCallbackRequested = true; + } + return; + } + + if (moz_container_wayland_is_commiting_to_parent(container)) { + // When committing to parent surface we must use wl_surface_damage(). + // A parent surface is created as v.3 object which does not support + // wl_surface_damage_buffer(). + wl_surface_damage(waylandSurface, 0, 0, INT32_MAX, INT32_MAX); + } else { + for (auto iter = aInvalidRegion.RectIter(); !iter.Done(); iter.Next()) { + LayoutDeviceIntRect r = iter.Get(); + wl_surface_damage_buffer(waylandSurface, r.x, r.y, r.width, r.height); + } + } + + // aProofOfLock is a kind of substitution of MozContainerSurfaceLock. + // MozContainer is locked but MozContainerSurfaceLock doen't convert to + // MutexAutoLock& so we use aProofOfLock here. + moz_container_wayland_set_scale_factor_locked(aProofOfLock, container); + + // It's possible that scale factor changed between Lock() and Commit() + // but window size is the same. + // Don't attach such buffer as it may have incorrect size, + // we'll paint new content soon. + if (moz_container_wayland_size_matches_scale_factor_locked( + aProofOfLock, container, mWindowSize.width, mWindowSize.height)) { + mInProgressBuffer->AttachAndCommit(waylandSurface); + } + + mInProgressBuffer->ResetBufferAge(); + mFrontBuffer = mInProgressBuffer; + mFrontBufferInvalidRegion = aInvalidRegion; + mInProgressBuffer = nullptr; + + EnforcePoolSizeLimit(aProofOfLock); + IncrementBufferAge(aProofOfLock); + + if (wl_display_flush(WaylandDisplayGet()->GetDisplay()) == -1) { + LOGWAYLAND("WindowSurfaceWaylandMB::Commit [%p] flush failed\n", + (void*)mWindow.get()); + } +} + +RefPtr<WaylandBufferSHM> WindowSurfaceWaylandMB::ObtainBufferFromPool( + const MutexAutoLock& aProofOfLock, const LayoutDeviceIntSize& aSize) { + if (!mAvailableBuffers.IsEmpty()) { + RefPtr<WaylandBufferSHM> buffer = mAvailableBuffers.PopLastElement(); + mInUseBuffers.AppendElement(buffer); + return buffer; + } + + RefPtr<WaylandBufferSHM> buffer = WaylandBufferSHM::Create(aSize); + if (buffer) { + mInUseBuffers.AppendElement(buffer); + } + + return buffer; +} + +void WindowSurfaceWaylandMB::ReturnBufferToPool( + const MutexAutoLock& aProofOfLock, + const RefPtr<WaylandBufferSHM>& aBuffer) { + if (aBuffer->IsAttached()) { + mPendingBuffers.AppendElement(aBuffer); + } else if (aBuffer->IsMatchingSize(mWindowSize)) { + mAvailableBuffers.AppendElement(aBuffer); + } + mInUseBuffers.RemoveElement(aBuffer); +} + +void WindowSurfaceWaylandMB::EnforcePoolSizeLimit( + const MutexAutoLock& aProofOfLock) { + // Enforce the pool size limit, removing least-recently-used entries as + // necessary. + while (mAvailableBuffers.Length() > BACK_BUFFER_NUM) { + mAvailableBuffers.RemoveElementAt(0); + } + + NS_WARNING_ASSERTION(mPendingBuffers.Length() < BACK_BUFFER_NUM, + "Are we leaking pending buffers?"); + NS_WARNING_ASSERTION(mInUseBuffers.Length() < BACK_BUFFER_NUM, + "Are we leaking in-use buffers?"); +} + +void WindowSurfaceWaylandMB::CollectPendingSurfaces( + const MutexAutoLock& aProofOfLock) { + mPendingBuffers.RemoveElementsBy([&](auto& buffer) { + if (!buffer->IsAttached()) { + if (buffer->IsMatchingSize(mWindowSize)) { + mAvailableBuffers.AppendElement(std::move(buffer)); + } + return true; + } + return false; + }); +} + +void WindowSurfaceWaylandMB::IncrementBufferAge( + const MutexAutoLock& aProofOfLock) { + for (const RefPtr<WaylandBufferSHM>& buffer : mInUseBuffers) { + buffer->IncrementBufferAge(); + } + for (const RefPtr<WaylandBufferSHM>& buffer : mPendingBuffers) { + buffer->IncrementBufferAge(); + } + for (const RefPtr<WaylandBufferSHM>& buffer : mAvailableBuffers) { + buffer->IncrementBufferAge(); + } +} + +} // namespace mozilla::widget diff --git a/widget/gtk/WindowSurfaceWaylandMultiBuffer.h b/widget/gtk/WindowSurfaceWaylandMultiBuffer.h new file mode 100644 index 0000000000..e14a626de0 --- /dev/null +++ b/widget/gtk/WindowSurfaceWaylandMultiBuffer.h @@ -0,0 +1,84 @@ +/* -*- 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_MULTI_BUFFER_H +#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_MULTI_BUFFER_H + +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/Mutex.h" +#include "nsTArray.h" +#include "nsWaylandDisplay.h" +#include "nsWindow.h" +#include "WaylandBuffer.h" +#include "WindowSurface.h" + +namespace mozilla::widget { + +using gfx::DrawTarget; + +// WindowSurfaceWaylandMB is an abstraction for wl_surface +// and related management +class WindowSurfaceWaylandMB : public WindowSurface { + public: + WindowSurfaceWaylandMB(RefPtr<nsWindow> aWindow, + GtkCompositorWidget* aCompositorWidget); + ~WindowSurfaceWaylandMB() = default; + + // 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<DrawTarget> Lock( + const LayoutDeviceIntRegion& aInvalidRegion) override; + void Commit(const LayoutDeviceIntRegion& aInvalidRegion) final; + + private: + void Commit(const MutexAutoLock& aProofOfLock, + const LayoutDeviceIntRegion& aInvalidRegion); + RefPtr<WaylandBufferSHM> ObtainBufferFromPool( + const MutexAutoLock& aProofOfLock, const LayoutDeviceIntSize& aSize); + void ReturnBufferToPool(const MutexAutoLock& aProofOfLock, + const RefPtr<WaylandBufferSHM>& aBuffer); + void EnforcePoolSizeLimit(const MutexAutoLock& aProofOfLock); + void CollectPendingSurfaces(const MutexAutoLock& aProofOfLock); + void HandlePartialUpdate(const MutexAutoLock& aProofOfLock, + const LayoutDeviceIntRegion& aInvalidRegion); + void IncrementBufferAge(const MutexAutoLock& aProofOfLock); + // Return true if window size was updated. + bool MaybeUpdateWindowSize(); + + mozilla::Mutex mSurfaceLock MOZ_UNANNOTATED; + + RefPtr<nsWindow> mWindow; + // WindowSurfaceWaylandMB is owned by GtkCompositorWidget so we can't + // reference it. + GtkCompositorWidget* mCompositorWidget; + LayoutDeviceIntSize mWindowSize; + + RefPtr<WaylandBufferSHM> mInProgressBuffer; + RefPtr<WaylandBufferSHM> mFrontBuffer; + LayoutDeviceIntRegion mFrontBufferInvalidRegion; + + // buffer pool + nsTArray<RefPtr<WaylandBufferSHM>> mInUseBuffers; + nsTArray<RefPtr<WaylandBufferSHM>> mPendingBuffers; + nsTArray<RefPtr<WaylandBufferSHM>> mAvailableBuffers; + + // delayed commits + bool mFrameInProcess; + bool mCallbackRequested; +}; + +} // namespace mozilla::widget + +#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_MULTI_BUFFER_H diff --git a/widget/gtk/WindowSurfaceX11.cpp b/widget/gtk/WindowSurfaceX11.cpp new file mode 100644 index 0000000000..36b238a98b --- /dev/null +++ b/widget/gtk/WindowSurfaceX11.cpp @@ -0,0 +1,47 @@ +/* -*- 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" + +namespace mozilla::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 mozilla::widget diff --git a/widget/gtk/WindowSurfaceX11.h b/widget/gtk/WindowSurfaceX11.h new file mode 100644 index 0000000000..dda17bf0d0 --- /dev/null +++ b/widget/gtk/WindowSurfaceX11.h @@ -0,0 +1,39 @@ +/* -*- 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> +# include "X11UndefineNone.h" + +namespace mozilla::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 mozilla::widget + +#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..f83b64336c --- /dev/null +++ b/widget/gtk/WindowSurfaceX11Image.cpp @@ -0,0 +1,254 @@ +/* -*- 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; + } +} + +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)) { + backend = gfx::BackendType::SKIA; + } + 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(); + if (bounds.IsEmpty()) { + return; + } + + if (mIsShaped) { + ApplyTransparencyBitmap(); + } + + uint32_t numRects = aInvalidRegion.GetNumRects(); + if (numRects == 1) { + dt->CopySurface(surf, bounds, bounds.TopLeft()); + } else { + 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()); + + dt->DrawSurface(surf, gfx::Rect(bounds), gfx::Rect(bounds), + DrawSurfaceOptions(), + DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + + 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/WindowSurfaceX11SHM.cpp b/widget/gtk/WindowSurfaceX11SHM.cpp new file mode 100644 index 0000000000..889881d22d --- /dev/null +++ b/widget/gtk/WindowSurfaceX11SHM.cpp @@ -0,0 +1,27 @@ +/* -*- 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 "WindowSurfaceX11SHM.h" + +namespace mozilla::widget { + +WindowSurfaceX11SHM::WindowSurfaceX11SHM(Display* aDisplay, Drawable aWindow, + Visual* aVisual, unsigned int aDepth) { + mFrontImage = new nsShmImage(aDisplay, aWindow, aVisual, aDepth); + mBackImage = new nsShmImage(aDisplay, aWindow, aVisual, aDepth); +} + +already_AddRefed<gfx::DrawTarget> WindowSurfaceX11SHM::Lock( + const LayoutDeviceIntRegion& aRegion) { + mBackImage.swap(mFrontImage); + return mBackImage->CreateDrawTarget(aRegion); +} + +void WindowSurfaceX11SHM::Commit(const LayoutDeviceIntRegion& aInvalidRegion) { + mBackImage->Put(aInvalidRegion); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/WindowSurfaceX11SHM.h b/widget/gtk/WindowSurfaceX11SHM.h new file mode 100644 index 0000000000..5d12137f7b --- /dev/null +++ b/widget/gtk/WindowSurfaceX11SHM.h @@ -0,0 +1,36 @@ +/* -*- 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_WINDOW_SURFACE_X11_SHM_H +#define _MOZILLA_WIDGET_WINDOW_SURFACE_X11_SHM_H + +#ifdef MOZ_X11 + +# include "mozilla/widget/WindowSurface.h" +# include "nsShmImage.h" + +namespace mozilla { +namespace widget { + +class WindowSurfaceX11SHM : public WindowSurface { + public: + WindowSurfaceX11SHM(Display* aDisplay, Drawable aWindow, Visual* aVisual, + unsigned int aDepth); + + already_AddRefed<gfx::DrawTarget> Lock( + const LayoutDeviceIntRegion& aRegion) override; + void Commit(const LayoutDeviceIntRegion& aInvalidRegion) override; + + private: + RefPtr<nsShmImage> mFrontImage; + RefPtr<nsShmImage> mBackImage; +}; + +} // namespace widget +} // namespace mozilla + +#endif // MOZ_X11 +#endif // _MOZILLA_WIDGET_WINDOW_SURFACE_X11_SHM_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..7b0718f3cb --- /dev/null +++ b/widget/gtk/compat/gdk/gdkx.h @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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)); +} + +#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..a2759a3c45 --- /dev/null +++ b/widget/gtk/components.conf @@ -0,0 +1,151 @@ +# -*- 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_SOCKET_AND_UTILITY_PROCESS, + }, + { + 'cid': '{e9537f8f-c07e-4435-8ab3-83f1ad6e3bbf}', + 'contract_ids': ['@mozilla.org/gfx/parent/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': '{4364de1a-798e-419c-a6f5-ca28866b6d5f}', + 'contract_ids': ['@mozilla.org/parent/colorpicker;1'], + 'type': 'nsColorPicker', + 'headers': ['/widget/gtk/nsColorPicker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{1940fed5-7d02-4122-8acf-7abaac698983}', + 'contract_ids': ['@mozilla.org/parent/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': '{e711c28b-c1f1-4b87-8448-e1e0da0a7b7d}', + 'contract_ids': ['@mozilla.org/parent/sound;1'], + 'singleton': True, + 'type': 'nsISound', + 'constructor': 'nsSound::GetInstance', + 'headers': ['/widget/gtk/nsSound.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + '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, + }, + { + 'cid': '{f55f5d31-dbb7-4d0d-9f6f-a4f4cd8e8ef1}', + 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'], + 'interfaces': ['nsIClipboard'], + 'type': 'nsIClipboard', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}', + 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'], + 'type': 'nsClipboardHelper', + 'headers': ['/widget/nsClipboardHelper.h'], + }, + { + 'cid': '{0ba77e04-2adb-422f-af01-5a57b8013100}', + 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'], + 'singleton': True, + 'type': 'nsDragService', + 'headers': ['/widget/gtk/nsDragService.h'], + 'constructor': 'nsDragService::GetInstance', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'name': 'GfxInfo', + 'cid': '{d755a760-9f27-11df-0800-200c9a664242}', + 'contract_ids': ['@mozilla.org/gfx/info;1'], + 'type': 'mozilla::widget::GfxInfo', + 'headers': ['/widget/gtk/GfxInfo.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'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}', + 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'], + 'type': 'nsPrintDialogServiceGTK', + 'headers': ['/widget/gtk/nsPrintDialogGTK.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + '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'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + ] 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..b9e927174c --- /dev/null +++ b/widget/gtk/gtk3drawing.cpp @@ -0,0 +1,2452 @@ +/* -*- 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 "WidgetUtilsGtk.h" + +#include <math.h> +#include <dlfcn.h> + +static gboolean checkbox_check_state; +static gboolean notebook_has_tab_gap; + +static ToggleGTKMetrics sCheckboxMetrics; +static ToggleGTKMetrics sRadioMetrics; +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+=(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 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; + } + + sCheckboxMetrics.initialized = false; + sRadioMetrics.initialized = false; + sToolbarMetrics.initialized = false; + sToplevelWindowDecorationSize.initialized = false; + sPopupWindowDecorationSize.initialized = false; + + /* This will destroy all of our widgets */ + ResetWidgetCache(); +} + +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)) { + 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 equally 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)); + + 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}; + } else if (button.EqualsLiteral("minimize")) { + aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE}; + } else if (button.EqualsLiteral("maximize")) { + aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE}; + } + if (activeButtons == aButtonLayout.Length()) { + return activeButtons; + } + } + } + return activeButtons; +} + +static void EnsureToolbarMetrics() { + if (sToolbarMetrics.initialized) { + return; + } + // 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(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) { + if (!widget) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + + 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->image_scale); + gtk_style_context_set_state(style, state_flags); + + if (state->isDefault && relief == GTK_RELIEF_NORMAL && !state->focused && + !(state_flags & GTK_STATE_FLAG_PRELIGHT)) { + /* 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) { + GdkRectangle rect = *aRect; + // We need to inset our calculated margin because it also + // contains titlebar button spacing. + const ToolbarButtonGTKMetrics* metrics = GetToolbarButtonMetrics( + aIconWidgetType == MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE + ? MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE + : aIconWidgetType); + Inset(&rect, metrics->buttonMargin); + + GtkWidget* buttonWidget = GetWidget(aIconWidgetType); + if (!buttonWidget) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + moz_gtk_button_paint(cr, &rect, state, relief, buttonWidget, direction); + + GtkWidget* iconWidget = + gtk_bin_get_child(GTK_BIN(GetWidget(aIconWidgetType))); + if (!iconWidget) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + cairo_surface_t* surface = + GetWidgetIconSurface(iconWidget, state->image_scale); + + if (surface) { + GtkStyleContext* style = gtk_widget_get_style_context(buttonWidget); + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + + gtk_style_context_save(style); + StyleContextSetScale(style, state->image_scale); + gtk_style_context_set_state(style, state_flags); + + /* This is available since Gtk+ 3.10 as well as GtkHeaderBar */ + gtk_render_icon_surface(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->image_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; +} + +/** + * 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 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 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_inner_spin_paint(cairo_t* cr, GdkRectangle* rect, + GtkWidgetState* state, + GtkTextDirection direction) { + GtkStyleContext* style = + GetStyleContext(MOZ_GTK_SPINBUTTON, state->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_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) { + GdkRectangle rect = *aRect; + 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->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_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); + if (!comboBoxButton || !comboBoxArrow) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + + /* 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->image_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->image_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; + + GtkWidget* widget = GetWidget(MOZ_GTK_BUTTON_ARROW); + if (!widget) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + calculate_arrow_rect(widget, rect, &arrow_rect, direction); + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + GtkStyleContext* style = GetStyleContext( + MOZ_GTK_BUTTON_ARROW, state->image_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); + if (!comboBoxEntry) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + + 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->image_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; + } + + GtkWidget* arrow = GetWidget(MOZ_GTK_COMBOBOX_ENTRY_ARROW); + if (!arrow) { + return MOZ_GTK_UNKNOWN_WIDGET; + } + calculate_arrow_rect(arrow, &arrow_rect, &real_arrow_rect, direction); + + style = GetStyleContext(MOZ_GTK_COMBOBOX_ENTRY_ARROW, state->image_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->image_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->image_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->image_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->image_scale); + gtk_style_context_get_style(style, "wide-separators", &wide_separators, + "separator-width", &separator_width, NULL); + + style = + GetStyleContext(MOZ_GTK_TOOLBAR_SEPARATOR, state->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_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->image_scale, + direction, GetStateFlagsFromGtkWidgetState(state)); + gtk_render_arrow(style, cr, arrow_angle, x, y, arrow_size); + } + 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->image_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; +} + +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->image_scale, + GTK_TEXT_DIR_NONE, state_flags); + + // Some themes like Elementary's style the container of the headerbar rather + // than the header bar itself. + if (HeaderBarShouldDrawContainer(widgetType)) { + auto containerType = widgetType == MOZ_GTK_HEADER_BAR + ? MOZ_GTK_HEADERBAR_FIXED + : MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED; + style = GetStyleContext(containerType, state->image_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. + // + // We also don't need to draw this on Wayland, since the compositor takes care + // of it. + if (widgetType == MOZ_GTK_HEADER_BAR && + !mozilla::widget::GdkIsWaylandDisplay()) { + GtkStyleContext* windowStyle = + GetStyleContext(MOZ_GTK_HEADERBAR_WINDOW, state->image_scale); + const bool solidDecorations = + gtk_style_context_has_class(windowStyle, "solid-csd"); + GtkStyleContext* decorationStyle = + GetStyleContext(solidDecorations ? MOZ_GTK_WINDOW_DECORATION_SOLID + : MOZ_GTK_WINDOW_DECORATION, + state->image_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; +} + +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); + if (w) { + 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_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_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_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_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; + } + if (widget) { + GtkRequisition requisition; + gtk_widget_get_preferred_size(widget, NULL, &requisition); + moz_gtk_sanity_preferred_size(&requisition); + + *width = requisition.width; + *height = requisition.height; + } else { + *width = 0; + *height = 0; + } +} + +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; +} + +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; +} + +const ToggleGTKMetrics* GetToggleMetrics(WidgetNodeType aWidgetType) { + ToggleGTKMetrics* metrics; + + switch (aWidgetType) { + case MOZ_GTK_RADIOBUTTON: + metrics = &sRadioMetrics; + break; + case MOZ_GTK_CHECKBUTTON: + metrics = &sCheckboxMetrics; + 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 = 0; + 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 = 0, indicator_spacing = 0; + 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; +} + +/* + * 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_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->image_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->image_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_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_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_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..8a6b731e11 --- /dev/null +++ b/widget/gtk/gtkdrawing.h @@ -0,0 +1,545 @@ +/* -*- 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 image_scale; /* image 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; + 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; + +/** 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, + /* 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 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 popup menus. */ + MOZ_GTK_MENUITEM, + /* 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, + /* Used only as a container for MOZ_GTK_HEADER_BAR. */ + MOZ_GTK_HEADERBAR_FIXED, + /* Used only as a container for MOZ_GTK_HEADER_BAR_MAXIMIZED. */ + MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED, + /* Window container for all widgets */ + MOZ_GTK_WINDOW_CONTAINER, + /* 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; +}; + +/*** 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); + +/** + * 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 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 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/moz.build b/widget/gtk/moz.build new file mode 100644 index 0000000000..f4a1ababbe --- /dev/null +++ b/widget/gtk/moz.build @@ -0,0 +1,181 @@ +# -*- 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["COMPILE_ENVIRONMENT"]: + DIRS += ["mozgtk"] + +if CONFIG["MOZ_WAYLAND"]: + DIRS += ["wayland", "mozwayland", "vaapitest"] + +EXPORTS += [ + "MozContainer.h", + "nsGTKToolkit.h", + "nsGtkUtils.h", + "nsImageToPixbuf.h", +] + +EXPORTS.mozilla += [ + "GfxInfo.h", + "GfxInfoUtils.h", + "GRefPtr.h", + "GUniquePtr.h", + "WidgetUtilsGtk.h", +] + +EXPORTS.mozilla.widget += [ + "WindowSurface.h", + "WindowSurfaceProvider.h", +] + +UNIFIED_SOURCES += [ + "AsyncGtkClipboardRequest.cpp", + "GfxInfo.cpp", + "gtk3drawing.cpp", + "GtkCompositorWidget.cpp", + "IMContextWrapper.cpp", + "MozContainer.cpp", + "MPRISServiceHandler.cpp", + "NativeKeyBindings.cpp", + "NativeMenuGtk.cpp", + "NativeMenuSupport.cpp", + "nsApplicationChooser.cpp", + "nsAppShell.cpp", + "nsBidiKeyboard.cpp", + "nsClipboard.cpp", + "nsColorPicker.cpp", + "nsDragService.cpp", + "nsFilePicker.cpp", + "nsGtkKeyUtils.cpp", + "nsImageToPixbuf.cpp", + "nsLookAndFeel.cpp", + "nsSound.cpp", + "nsToolkit.cpp", + "nsWidgetFactory.cpp", + "ScreenHelperGTK.cpp", + "TaskbarProgress.cpp", + "WakeLockListener.cpp", + "WidgetStyleCache.cpp", + "WidgetTraceEvent.cpp", + "WidgetUtilsGtk.cpp", + "WindowSurfaceProvider.cpp", +] + +SOURCES += [ + "MediaKeysEventSourceFactory.cpp", + "nsNativeThemeGTK.cpp", # conflicts with X11 headers + "nsWindow.cpp", # conflicts with X11 headers + "WaylandVsyncSource.cpp", # conflicts with X11 headers +] + +if CONFIG["MOZ_WAYLAND"]: + UNIFIED_SOURCES += [ + "DMABufLibWrapper.cpp", + "DMABufSurface.cpp", + "MozContainerWayland.cpp", + "nsClipboardWayland.cpp", + "nsWaylandDisplay.cpp", + "WaylandBuffer.cpp", + "WindowSurfaceWaylandMultiBuffer.cpp", + ] + EXPORTS.mozilla.widget += [ + "DMABufLibWrapper.h", + "DMABufSurface.h", + "MozContainerWayland.h", + "nsWaylandDisplay.h", + "WaylandBuffer.h", + ] + +if CONFIG["MOZ_X11"] or CONFIG["MOZ_WAYLAND"]: + UNIFIED_SOURCES += [ + "CompositorWidgetChild.cpp", + "CompositorWidgetParent.cpp", + "InProcessGtkCompositorWidget.cpp", + "nsUserIdleServiceGTK.cpp", + ] + EXPORTS.mozilla.widget += [ + "CompositorWidgetChild.h", + "CompositorWidgetParent.h", + "GtkCompositorWidget.h", + "InProcessGtkCompositorWidget.h", + ] + +if CONFIG["MOZ_X11"]: + UNIFIED_SOURCES += [ + "nsClipboardX11.cpp", + "nsShmImage.cpp", + "WindowSurfaceX11.cpp", + "WindowSurfaceX11Image.cpp", + "WindowSurfaceX11SHM.cpp", + ] + +if CONFIG["NS_PRINTING"]: + UNIFIED_SOURCES += [ + "nsDeviceContextSpecG.cpp", + "nsPrintDialogGTK.cpp", + "nsPrintSettingsGTK.cpp", + "nsPrintSettingsServiceGTK.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"] or CONFIG["MOZ_WAYLAND"]: + LOCAL_INCLUDES += [ + "/widget/x11", + ] + +DEFINES["CAIRO_GFX"] = True + +DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] + +CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] +CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +if CONFIG["MOZ_WAYLAND"]: + CFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"] + +if CONFIG["MOZ_ENABLE_DBUS"]: + EXPORTS.mozilla.widget += [ + "AsyncDBus.h", + ] + UNIFIED_SOURCES += [ + "AsyncDBus.cpp", + ] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + +CXXFLAGS += ["-Werror=switch"] diff --git a/widget/gtk/mozgtk/moz.build b/widget/gtk/mozgtk/moz.build new file mode 100644 index 0000000000..d5e78d0032 --- /dev/null +++ b/widget/gtk/mozgtk/moz.build @@ -0,0 +1,37 @@ +# -*- 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/. + +SharedLibrary("mozgtk") + +SOURCES += [ + "mozgtk.c", +] + +CFLAGS += CONFIG["MOZ_X11_CFLAGS"] +CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +# 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/mozgtk.c b/widget/gtk/mozgtk/mozgtk.c new file mode 100644 index 0000000000..d95746fc0b --- /dev/null +++ b/widget/gtk/mozgtk/mozgtk.c @@ -0,0 +1,30 @@ +/* -*- 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 <gdk/gdk.h> + +// Dummy call to gtk3 library to prevent the linker from removing +// the gtk3 dependency with --as-needed. +// see toolkit/library/moz.build for details. +MOZ_EXPORT void mozgtk_linker_holder() { gdk_display_get_default(); } + +#ifdef MOZ_X11 +# 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. +// +// This is also used to force libxul to depend on the mozgtk library. If we +// ever can remove this workaround for system Cairo, we'll need something +// to replace it for that purpose. +MOZ_EXPORT Bool XShmQueryExtension(Display* aDisplay) { return False; } +#endif diff --git a/widget/gtk/mozwayland/moz.build b/widget/gtk/mozwayland/moz.build new file mode 100644 index 0000000000..0df3f0f685 --- /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["MOZ_GTK3_CFLAGS"] diff --git a/widget/gtk/mozwayland/mozwayland.c b/widget/gtk/mozwayland/mozwayland.c new file mode 100644 index 0000000000..ad8e949ce2 --- /dev/null +++ b/widget/gtk/mozwayland/mozwayland.c @@ -0,0 +1,212 @@ +/* -*- 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_pointer_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_egl_window_get_attached_size( + struct wl_egl_window* egl_window, int* width, int* height) {} + +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) {} + +MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_flags( + struct wl_proxy* proxy, uint32_t opcode, + const struct wl_interface* interface, uint32_t version, uint32_t flags, + ...) { + return NULL; +} 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..bea837605f --- /dev/null +++ b/widget/gtk/nsAppShell.cpp @@ -0,0 +1,424 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <gdk/gdk.h> +#include "nsAppShell.h" +#include "nsBaseAppShell.h" +#include "nsWindow.h" +#include "mozilla/Logging.h" +#include "prenv.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/Hal.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerThreadSleep.h" +#include "mozilla/Unused.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/WidgetUtils.h" +#include "nsIPowerManagerService.h" +#ifdef MOZ_ENABLE_DBUS +# include <dbus/dbus-glib-lowlevel.h> +# include <gio/gio.h> +# include "WakeLockListener.h" +# include "nsIObserverService.h" +#endif +#include "gfxPlatform.h" +#include "nsAppRunner.h" +#include "mozilla/XREAppData.h" +#include "ScreenHelperGTK.h" +#include "HeadlessScreenHelper.h" +#include "mozilla/widget/ScreenManager.h" +#ifdef MOZ_WAYLAND +# include "nsWaylandDisplay.h" +#endif + +using namespace mozilla; +using mozilla::widget::HeadlessScreenHelper; +using mozilla::widget::ScreenHelperGTK; +using mozilla::widget::ScreenManager; + +#define NOTIFY_TOKEN 0xFA + +LazyLogModule gWidgetLog("Widget"); +LazyLogModule gWidgetDragLog("WidgetDrag"); +LazyLogModule gWidgetWaylandLog("WidgetWayland"); +LazyLogModule gWidgetPopupLog("WidgetPopup"); +LazyLogModule gWidgetVsync("WidgetVsync"); +LazyLogModule gDmabufLog("Dmabuf"); +LazyLogModule gClipboardLog("WidgetClipboard"); + +static GPollFunc sPollFunc; + +// Wrapper function to disable hang monitoring while waiting in poll(). +static gint PollWrapper(GPollFD* aUfds, guint aNfsd, gint aTimeout) { + if (aTimeout == 0) { + // When the timeout is 0, there is no wait, so no point in notifying + // the BackgroundHangMonitor and the profiler. + return (*sPollFunc)(aUfds, aNfsd, aTimeout); + } + + mozilla::BackgroundHangMonitor().NotifyWait(); + gint result; + { + gint timeout = aTimeout; + gint64 begin = 0; + if (aTimeout != -1) { + begin = g_get_monotonic_time(); + } + + AUTO_PROFILER_LABEL("PollWrapper", IDLE); + AUTO_PROFILER_THREAD_SLEEP; + do { + result = (*sPollFunc)(aUfds, aNfsd, timeout); + + // The result will be -1 with the EINTR error if the poll was interrupted + // by a signal, typically the signal sent by the profiler to sample the + // process. We are only done waiting if we are not in that case. + if (result != -1 || errno != EINTR) { + break; + } + + if (aTimeout != -1) { + // Adjust the timeout to account for the time already spent waiting. + gint elapsedSinceBegin = (g_get_monotonic_time() - begin) / 1000; + if (elapsedSinceBegin < aTimeout) { + timeout = aTimeout - elapsedSinceBegin; + } else { + // poll returns 0 to indicate the call timed out before any fd + // became ready. + result = 0; + break; + } + } + } while (true); + } + 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() { +#ifdef MOZ_ENABLE_DBUS + StopDBusListening(); +#endif + mozilla::hal::Shutdown(); + + if (mTag) g_source_remove(mTag); + if (mPipeFDs[0]) close(mPipeFDs[0]); + if (mPipeFDs[1]) close(mPipeFDs[1]); +} + +#ifdef MOZ_ENABLE_DBUS +static void SessionSleepCallback(DBusGProxy* aProxy, gboolean aSuspend, + gpointer data) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + return; + } + + if (aSuspend) { + // Post sleep_notification + observerService->NotifyObservers(nullptr, NS_WIDGET_SLEEP_OBSERVER_TOPIC, + nullptr); + } else { + // Post wake_notification + observerService->NotifyObservers(nullptr, NS_WIDGET_WAKE_OBSERVER_TOPIC, + nullptr); + } +} + +static void TimedatePropertiesChangedCallback(DBusGProxy* aProxy, + char* interface, + GHashTable* aChanged, + char** aInvalidated, + gpointer aData) { + // dbus signals are not fine-grained enough for us to only react to timezone + // changes, so we need to ensure that timezone is one of the properties + // changed. + if (g_hash_table_contains(aChanged, "Timezone")) { + nsBaseAppShell::OnSystemTimezoneChange(); + } +} + +static DBusHandlerResult ConnectionSignalFilter(DBusConnection* aConnection, + DBusMessage* aMessage, + void* aData) { + if (dbus_message_is_signal(aMessage, DBUS_INTERFACE_LOCAL, "Disconnected")) { + auto* appShell = static_cast<nsAppShell*>(aData); + appShell->StopDBusListening(); + // We do not return DBUS_HANDLER_RESULT_HANDLED here because the connection + // might be shared and some other filters might want to do something. + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +// Based on +// https://github.com/lcp/NetworkManager/blob/240f47c892b4e935a3e92fc09eb15163d1fa28d8/src/nm-sleep-monitor-systemd.c +// Use login1 to signal sleep and wake notifications. +void nsAppShell::StartDBusListening() { + GUniquePtr<GError> error; + mDBusConnection = dbus_g_bus_get(DBUS_BUS_SYSTEM, getter_Transfers(error)); + if (!mDBusConnection) { + NS_WARNING(nsPrintfCString("gds: Failed to open connection to bus %s\n", + error->message) + .get()); + return; + } + + DBusConnection* dbusConnection = + dbus_g_connection_get_connection(mDBusConnection); + + // Make sure we do not exit the entire program if DBus connection gets + // lost. + dbus_connection_set_exit_on_disconnect(dbusConnection, false); + + // Listening to signals the DBus connection is going to get so we will + // know when it is lost and we will be able to disconnect cleanly. + dbus_connection_add_filter(dbusConnection, ConnectionSignalFilter, this, + nullptr); + + mLogin1Proxy = dbus_g_proxy_new_for_name( + mDBusConnection, "org.freedesktop.login1", "/org/freedesktop/login1", + "org.freedesktop.login1.Manager"); + + if (!mLogin1Proxy) { + NS_WARNING("gds: error creating login dbus proxy\n"); + return; + } + + dbus_g_proxy_add_signal(mLogin1Proxy, "PrepareForSleep", G_TYPE_BOOLEAN, + G_TYPE_INVALID); + dbus_g_proxy_connect_signal(mLogin1Proxy, "PrepareForSleep", + G_CALLBACK(SessionSleepCallback), this, nullptr); + + mTimedate1Proxy = dbus_g_proxy_new_for_name( + mDBusConnection, "org.freedesktop.timedate1", + "/org/freedesktop/timedate1", "org.freedesktop.DBus.Properties"); + + if (!mTimedate1Proxy) { + NS_WARNING("gds: error creating timedate dbus proxy\n"); + return; + } + + dbus_g_proxy_add_signal( + mTimedate1Proxy, "PropertiesChanged", G_TYPE_STRING, + dbus_g_type_get_map("GHashTable", G_TYPE_STRING, G_TYPE_VALUE), + G_TYPE_STRV, G_TYPE_INVALID); + dbus_g_proxy_connect_signal(mTimedate1Proxy, "PropertiesChanged", + G_CALLBACK(TimedatePropertiesChangedCallback), + this, nullptr); +} + +void nsAppShell::StopDBusListening() { + // If mDBusConnection isn't initialized, that means we are not really + // listening. + if (!mDBusConnection) { + return; + } + dbus_connection_remove_filter( + dbus_g_connection_get_connection(mDBusConnection), ConnectionSignalFilter, + this); + + if (mLogin1Proxy) { + dbus_g_proxy_disconnect_signal(mLogin1Proxy, "PrepareForSleep", + G_CALLBACK(SessionSleepCallback), this); + g_object_unref(mLogin1Proxy); + mLogin1Proxy = nullptr; + } + + if (mTimedate1Proxy) { + dbus_g_proxy_disconnect_signal( + mTimedate1Proxy, "PropertiesChanged", + G_CALLBACK(TimedatePropertiesChangedCallback), this); + g_object_unref(mTimedate1Proxy); + mTimedate1Proxy = nullptr; + } + + dbus_g_connection_unref(mDBusConnection); + mDBusConnection = nullptr; +} + +#endif + +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!"); + } + + StartDBusListening(); + } +#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. + if (gAppData) { + gdk_set_program_class(gAppData->remotingName); + } + } + } + + 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"); + } + + // 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") && strcmp(name, "webp") && strcmp(name, "avif")) { + 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) { + if (mSuspendNativeCount) { + return false; + } + bool didProcessEvent = g_main_context_iteration(nullptr, mayWait); + return didProcessEvent; +} diff --git a/widget/gtk/nsAppShell.h b/widget/gtk/nsAppShell.h new file mode 100644 index 0000000000..20f718d4fa --- /dev/null +++ b/widget/gtk/nsAppShell.h @@ -0,0 +1,46 @@ +/* -*- 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" +#ifdef MOZ_ENABLE_DBUS +# include <dbus/dbus-glib.h> +#endif + +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; +#ifdef MOZ_ENABLE_DBUS + void StartDBusListening(); + void StopDBusListening(); +#endif + + private: + virtual ~nsAppShell(); + + static gboolean EventProcessorCallback(GIOChannel* source, + GIOCondition condition, gpointer data); + int mPipeFDs[2]; + unsigned mTag; + +#ifdef MOZ_ENABLE_DBUS + DBusGConnection* mDBusConnection = nullptr; + DBusGProxy* mLogin1Proxy = nullptr; + DBusGProxy* mTimedate1Proxy = nullptr; +#endif +}; + +#endif /* nsAppShell_h__ */ diff --git a/widget/gtk/nsApplicationChooser.cpp b/widget/gtk/nsApplicationChooser.cpp new file mode 100644 index 0000000000..6752c52caf --- /dev/null +++ b/widget/gtk/nsApplicationChooser.cpp @@ -0,0 +1,132 @@ +/* -*- 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 "nsIFile.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..7b0e599e69 --- /dev/null +++ b/widget/gtk/nsClipboard.cpp @@ -0,0 +1,1372 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "nsArrayUtils.h" +#include "nsClipboard.h" +#if defined(MOZ_X11) +# include "nsClipboardX11.h" +#endif +#if defined(MOZ_WAYLAND) +# include "nsClipboardWayland.h" +#endif +#include "nsGtkUtils.h" +#include "nsIURI.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#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 "nsIFileURL.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/RefPtr.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/TimeStamp.h" +#include "GRefPtr.h" +#include "WidgetUtilsGtk.h" + +#include "imgIContainer.h" + +#include <gtk/gtk.h> +#if defined(MOZ_X11) +# include <gtk/gtkx.h> +#endif + +#include "mozilla/Encoding.h" + +using namespace mozilla; + +// Idle timeout for receiving selection and property notify events (microsec) +// Right now it's set to 1 sec. +const int kClipboardTimeout = 1000000; + +// Defines how many event loop iterations will be done without sleep. +// We ususally get data in first 2-3 iterations unless some large object +// (an image for instance) is transferred through clipboard. +const int kClipboardFastIterationNum = 3; + +// 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">)"; + +static const char kURIListMime[] = "text/uri-list"; + +ClipboardTargets nsRetrievalContext::sClipboardTargets; +ClipboardTargets nsRetrievalContext::sPrimaryTargets; + +// 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 GetHTMLCharset(Span<const char> aData, nsCString& str); + +static void SetTransferableData(nsITransferable* aTransferable, + const nsACString& aFlavor, + const char* aClipboardData, + uint32_t aClipboardDataLength) { + LOGCLIP("SetTransferableData MIME %s\n", PromiseFlatCString(aFlavor).get()); + nsCOMPtr<nsISupports> wrapper; + nsPrimitiveHelpers::CreatePrimitiveForData( + aFlavor, aClipboardData, aClipboardDataLength, getter_AddRefs(wrapper)); + aTransferable->SetTransferData(PromiseFlatCString(aFlavor).get(), wrapper); +} + +ClipboardTargets ClipboardTargets::Clone() { + ClipboardTargets ret; + ret.mCount = mCount; + if (mCount) { + ret.mTargets.reset( + reinterpret_cast<GdkAtom*>(g_malloc(sizeof(GdkAtom) * mCount))); + memcpy(ret.mTargets.get(), mTargets.get(), sizeof(GdkAtom) * mCount); + } + return ret; +} + +void ClipboardTargets::Set(ClipboardTargets aTargets) { + mCount = aTargets.mCount; + mTargets = std::move(aTargets.mTargets); +} + +void ClipboardData::SetData(Span<const uint8_t> aData) { + mData = nullptr; + mLength = aData.Length(); + if (mLength) { + mData.reset(reinterpret_cast<char*>(g_malloc(sizeof(char) * mLength))); + memcpy(mData.get(), aData.data(), sizeof(char) * mLength); + } +} + +void ClipboardData::SetText(Span<const char> aData) { + mData = nullptr; + mLength = aData.Length(); + if (mLength) { + mData.reset( + reinterpret_cast<char*>(g_malloc(sizeof(char) * (mLength + 1)))); + memcpy(mData.get(), aData.data(), sizeof(char) * mLength); + mData.get()[mLength] = '\0'; + } +} + +void ClipboardData::SetTargets(ClipboardTargets aTargets) { + mLength = aTargets.mCount; + mData.reset(reinterpret_cast<char*>(aTargets.mTargets.release())); +} + +ClipboardTargets ClipboardData::ExtractTargets() { + GUniquePtr<GdkAtom> targets(reinterpret_cast<GdkAtom*>(mData.release())); + uint32_t length = std::exchange(mLength, 0); + return ClipboardTargets{std::move(targets), length}; +} + +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; + + return -1; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF +} + +void nsRetrievalContext::ClearCachedTargetsClipboard(GtkClipboard* aClipboard, + GdkEvent* aEvent, + gpointer data) { + LOGCLIP("nsRetrievalContext::ClearCachedTargetsClipboard()"); + sClipboardTargets.Clear(); +} + +void nsRetrievalContext::ClearCachedTargetsPrimary(GtkClipboard* aClipboard, + GdkEvent* aEvent, + gpointer data) { + LOGCLIP("nsRetrievalContext::ClearCachedTargetsPrimary()"); + sPrimaryTargets.Clear(); +} + +ClipboardTargets nsRetrievalContext::GetTargets(int32_t aWhichClipboard) { + LOGCLIP("nsRetrievalContext::GetTargets(%s)\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + ClipboardTargets& storedTargets = + (aWhichClipboard == nsClipboard::kSelectionClipboard) ? sPrimaryTargets + : sClipboardTargets; + if (!storedTargets) { + LOGCLIP(" getting targets from system"); + storedTargets.Set(GetTargetsImpl(aWhichClipboard)); + } else { + LOGCLIP(" using cached targets"); + } + return storedTargets.Clone(); +} + +nsRetrievalContext::nsRetrievalContext() { + g_signal_connect(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), "owner-change", + G_CALLBACK(ClearCachedTargetsClipboard), this); + g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY), "owner-change", + G_CALLBACK(ClearCachedTargetsPrimary), this); +} + +nsRetrievalContext::~nsRetrievalContext() { + g_signal_handlers_disconnect_by_func( + gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), + FuncToGpointer(ClearCachedTargetsClipboard), this); + g_signal_handlers_disconnect_by_func( + gtk_clipboard_get(GDK_SELECTION_PRIMARY), + FuncToGpointer(ClearCachedTargetsPrimary), this); + sClipboardTargets.Clear(); + sPrimaryTargets.Clear(); +} + +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_INHERITED(nsClipboard, ClipboardSetDataHelper, nsIObserver) + +nsresult nsClipboard::Init(void) { +#if defined(MOZ_X11) + if (widget::GdkIsX11Display()) { + mContext = new nsRetrievalContextX11(); + } +#endif +#if defined(MOZ_WAYLAND) + if (widget::GdkIsWaylandDisplay()) { + mContext = new nsRetrievalContextWayland(); + } +#endif + + 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()", []() { + LOGCLIP("nsClipboard storing clipboard content\n"); + gtk_clipboard_store(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)); + })); +} + +NS_IMETHODIMP +nsClipboard::SetNativeClipboardData(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]; + LOGCLIP(" processing target %s\n", flavorStr.get()); + + // Special case text/plain since we can handle all of the string types. + if (flavorStr.EqualsLiteral(kTextMime)) { + LOGCLIP(" adding 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(" adding IMAGE targets\n"); + gtk_target_list_add_image_targets(list, 0, TRUE); + imagesAdded = true; + } + continue; + } + + if (flavorStr.EqualsLiteral(kFileMime)) { + LOGCLIP(" adding text/uri-list target\n"); + GdkAtom atom = gdk_atom_intern(kURIListMime, FALSE); + gtk_target_list_add(list, atom, 0, 0); + continue; + } + + // Add this to our list of valid targets + LOGCLIP(" adding OTHER target %s\n", flavorStr.get()); + 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 = 0; + GtkTargetEntry* gtkTargets = + gtk_target_table_new_from_list(list, &numTargets); + if (!gtkTargets || numTargets == 0) { + LOGCLIP( + " gtk_target_table_new_from_list() failed or empty list of " + "targets!\n"); + // Clear references to the any old data and let GTK know that it is no + // longer available. + EmptyClipboard(aWhichClipboard); + return NS_ERROR_FAILURE; + } + + ClearCachedTargets(aWhichClipboard); + + // Set getcallback and request to store data after an application exit + if (gtk_clipboard_set_with_data(gtkClipboard, gtkTargets, numTargets, + clipboard_get_cb, clipboard_clear_cb, this)) { + // 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"); + EmptyClipboard(aWhichClipboard); + rv = NS_ERROR_FAILURE; + } + + gtk_target_table_free(gtkTargets, numTargets); + gtk_target_list_unref(list); + + return rv; +} + +static bool IsMIMEAtFlavourList(const nsTArray<nsCString>& aFlavourList, + const char* aMime) { + for (const auto& flavorStr : aFlavourList) { + if (flavorStr.Equals(aMime)) { + return true; + } + } + return false; +} + +// When clipboard contains only images, X11/Gtk tries to convert them +// to text when we request text instead of just fail to provide the data. +// So if clipboard contains images only remove text MIME offer. +bool nsClipboard::FilterImportedFlavors(int32_t aWhichClipboard, + nsTArray<nsCString>& aFlavors) { + LOGCLIP("nsClipboard::FilterImportedFlavors"); + + auto targets = mContext->GetTargets(aWhichClipboard); + if (!targets) { + LOGCLIP(" X11: no targes at clipboard (null), quit.\n"); + return true; + } + + for (const auto& atom : targets.AsSpan()) { + GUniquePtr<gchar> atom_name(gdk_atom_name(atom)); + if (!atom_name) { + continue; + } + // Filter out system MIME types. + if (strcmp(atom_name.get(), "TARGETS") == 0 || + strcmp(atom_name.get(), "TIMESTAMP") == 0 || + strcmp(atom_name.get(), "SAVE_TARGETS") == 0 || + strcmp(atom_name.get(), "MULTIPLE") == 0) { + continue; + } + // Filter out types which can't be converted to text. + if (strncmp(atom_name.get(), "image/", 6) == 0 || + strncmp(atom_name.get(), "application/", 12) == 0 || + strncmp(atom_name.get(), "audio/", 6) == 0 || + strncmp(atom_name.get(), "video/", 6) == 0) { + continue; + } + // We have some other MIME type on clipboard which can be hopefully + // converted to text without any problem. + LOGCLIP(" X11: text types in clipboard, no need to filter them.\n"); + return true; + } + + // So make sure we offer only types we have at clipboard. + nsTArray<nsCString> clipboardFlavors; + for (const auto& atom : targets.AsSpan()) { + GUniquePtr<gchar> atom_name(gdk_atom_name(atom)); + if (!atom_name) { + continue; + } + if (IsMIMEAtFlavourList(aFlavors, atom_name.get())) { + clipboardFlavors.AppendElement(nsCString(atom_name.get())); + } + } + aFlavors.SwapElements(clipboardFlavors); +#ifdef MOZ_LOGGING + LOGCLIP(" X11: Flavors which match clipboard content:\n"); + for (uint32_t i = 0; i < aFlavors.Length(); i++) { + LOGCLIP(" %s\n", aFlavors[i].get()); + } +#endif + return true; +} + +static nsresult GetTransferableFlavors(nsITransferable* aTransferable, + nsTArray<nsCString>& aFlavors) { + if (!aTransferable) { + return NS_ERROR_FAILURE; + } + // Get a list of flavors this transferable can import + nsresult rv = aTransferable->FlavorsTransferableCanImport(aFlavors); + if (NS_FAILED(rv)) { + LOGCLIP(" FlavorsTransferableCanImport falied!\n"); + return rv; + } +#ifdef MOZ_LOGGING + LOGCLIP(" Flavors which can be imported:"); + for (const auto& flavor : aFlavors) { + LOGCLIP(" %s", flavor.get()); + } +#endif + return NS_OK; +} + +static bool TransferableSetFile(nsITransferable* aTransferable, + const nsACString& aURIList) { + nsresult rv; + nsTArray<nsCString> uris = mozilla::widget::ParseTextURIList(aURIList); + if (!uris.IsEmpty()) { + nsCOMPtr<nsIURI> fileURI; + NS_NewURI(getter_AddRefs(fileURI), uris[0]); + if (nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv)) { + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + aTransferable->SetTransferData(kFileMime, file); + LOGCLIP(" successfully set file to clipboard\n"); + return true; + } + } + } + return false; +} + +static bool TransferableSetHTML(nsITransferable* aTransferable, + Span<const char> aData) { + nsLiteralCString mimeType(kHTMLMime); + + // Convert text/html into our text format + nsAutoCString charset; + if (!GetHTMLCharset(aData, 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"); + } + + LOGCLIP("TransferableSetHTML: HTML detected charset %s", charset.get()); + // app which use "text/html" to copy&paste + // get the decoder + auto encoding = Encoding::ForLabelNoReplacement(charset); + if (!encoding) { + LOGCLIP("TransferableSetHTML: get unicode decoder error (charset: %s)", + charset.get()); + return false; + } + + // According to spec html UTF-16BE/LE should be switched to UTF-8 + // https://html.spec.whatwg.org/#determining-the-character-encoding:utf-16-encoding-2 + if (encoding == UTF_16LE_ENCODING || encoding == UTF_16BE_ENCODING) { + encoding = UTF_8_ENCODING; + } + + // Remove kHTMLMarkupPrefix again, it won't necessarily cause any + // issues, but might confuse other users. + const size_t prefixLen = ArrayLength(kHTMLMarkupPrefix) - 1; + if (aData.Length() >= prefixLen && nsDependentCSubstring(aData.To(prefixLen)) + .EqualsLiteral(kHTMLMarkupPrefix)) { + aData = aData.From(prefixLen); + } + + nsAutoString unicodeData; + auto [rv, enc] = encoding->Decode(AsBytes(aData), unicodeData); +#if MOZ_LOGGING + if (enc != UTF_8_ENCODING && + MOZ_LOG_TEST(gClipboardLog, mozilla::LogLevel::Debug)) { + nsCString decoderName; + enc->Name(decoderName); + LOGCLIP("TransferableSetHTML: expected UTF-8 decoder but got %s", + decoderName.get()); + } +#endif + if (NS_FAILED(rv)) { + LOGCLIP("TransferableSetHTML: failed to decode HTML"); + return false; + } + SetTransferableData(aTransferable, mimeType, + (const char*)unicodeData.BeginReading(), + unicodeData.Length() * sizeof(char16_t)); + return true; +} + +NS_IMETHODIMP +nsClipboard::GetData(nsITransferable* aTransferable, int32_t aWhichClipboard) { + LOGCLIP("nsClipboard::GetData (%s)\n", + aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard"); + + // TODO: Ensure we don't re-enter here. + if (!mContext) { + return NS_ERROR_FAILURE; + } + + nsTArray<nsCString> flavors; + nsresult rv = GetTransferableFlavors(aTransferable, flavors); + NS_ENSURE_SUCCESS(rv, rv); + + // Filter out MIME types on X11 to prevent unwanted conversions, + // see Bug 1611407 + if (widget::GdkIsX11Display() && + !FilterImportedFlavors(aWhichClipboard, flavors)) { + LOGCLIP(" Missing suitable clipboard data, quit."); + return NS_OK; + } + + 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()); + + auto clipboardData = + mContext->GetClipboardData(flavorStr.get(), aWhichClipboard); + if (!clipboardData) { + LOGCLIP(" %s type is missing\n", flavorStr.get()); + continue; + } + + nsCOMPtr<nsIInputStream> byteStream; + NS_NewByteInputStream(getter_AddRefs(byteStream), clipboardData.AsSpan(), + NS_ASSIGNMENT_COPY); + aTransferable->SetTransferData(flavorStr.get(), byteStream); + LOGCLIP(" got %s MIME data\n", flavorStr.get()); + return NS_OK; + } + + // Special case text/plain since we can convert any + // string into text/plain + if (flavorStr.EqualsLiteral(kTextMime)) { + LOGCLIP(" Getting text %s MIME clipboard data\n", flavorStr.get()); + + auto clipboardData = mContext->GetClipboardText(aWhichClipboard); + if (!clipboardData) { + LOGCLIP(" failed to get text data\n"); + // If the type was text/plain and we couldn't get + // text off the clipboard, run the next loop + // iteration. + continue; + } + + // Convert utf-8 into our text format. + NS_ConvertUTF8toUTF16 ucs2string(clipboardData.get()); + SetTransferableData(aTransferable, flavorStr, + (const char*)ucs2string.BeginReading(), + ucs2string.Length() * 2); + + LOGCLIP(" got text data, length %zd\n", ucs2string.Length()); + return NS_OK; + } + + if (flavorStr.EqualsLiteral(kFileMime)) { + LOGCLIP(" Getting %s file clipboard data\n", flavorStr.get()); + + auto clipboardData = + mContext->GetClipboardData(kURIListMime, aWhichClipboard); + if (!clipboardData) { + LOGCLIP(" text/uri-list type is missing\n"); + continue; + } + + nsDependentCSubstring fileName(clipboardData.AsSpan()); + if (!TransferableSetFile(aTransferable, fileName)) { + continue; + } + return NS_OK; + } + + LOGCLIP(" Getting %s MIME clipboard data\n", flavorStr.get()); + + auto clipboardData = + mContext->GetClipboardData(flavorStr.get(), aWhichClipboard); + +#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)) { + if (!TransferableSetHTML(aTransferable, clipboardData.AsSpan())) { + continue; + } + } else { + auto span = clipboardData.AsSpan(); + SetTransferableData(aTransferable, flavorStr, span.data(), + span.Length()); + } + return NS_OK; + } + } + + LOGCLIP(" failed to get clipboard content.\n"); + return NS_OK; +} + +enum DataType { + DATATYPE_IMAGE, + DATATYPE_FILE, + DATATYPE_HTML, + DATATYPE_RAW, +}; + +struct DataPromiseHandler { + RefPtr<nsITransferable> mTransferable; + RefPtr<GenericPromise::Private> mDataPromise; + nsCString mMimeType; + DataType mDataType; + + explicit DataPromiseHandler(RefPtr<nsITransferable> aTransferable, + RefPtr<GenericPromise::Private> aDataPromise, + const char* aMimeType, + DataType aDataType = DATATYPE_RAW) + : mTransferable(std::move(aTransferable)), + mDataPromise(std::move(aDataPromise)), + mMimeType(aMimeType), + mDataType(aDataType) { + MOZ_COUNT_CTOR(DataPromiseHandler); + LOGCLIP("DataPromiseHandler created [%p] MIME %s type %d", this, + mMimeType.get(), mDataType); + } + ~DataPromiseHandler() { + LOGCLIP("DataPromiseHandler deleted [%p]", this); + MOZ_COUNT_DTOR(DataPromiseHandler); + } +}; + +static RefPtr<GenericPromise> AsyncGetTextImpl(nsITransferable* aTransferable, + int32_t aWhichClipboard) { + LOGCLIP("AsyncGetText() type '%s'", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + + RefPtr<GenericPromise::Private> dataPromise = + new GenericPromise::Private(__func__); + + gtk_clipboard_request_text( + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)), + [](GtkClipboard* aClipboard, const gchar* aText, gpointer aData) -> void { + UniquePtr<DataPromiseHandler> ref( + static_cast<DataPromiseHandler*>(aData)); + LOGCLIP("AsyncGetText async handler of [%p]", aData); + + size_t dataLength = aText ? strlen(aText) : 0; + if (dataLength <= 0) { + ref->mDataPromise->Resolve(false, __func__); + LOGCLIP(" quit, text is not available"); + return; + } + + // Convert utf-8 into our unicode format. + NS_ConvertUTF8toUTF16 utf16string(aText, dataLength); + nsLiteralCString flavor(kTextMime); + SetTransferableData(ref->mTransferable, flavor, + (const char*)utf16string.BeginReading(), + utf16string.Length() * 2); + LOGCLIP(" text is set, length = %d", (int)dataLength); + ref->mDataPromise->Resolve(true, __func__); + }, + new DataPromiseHandler(aTransferable, dataPromise, kTextMime)); + + return dataPromise; +} + +static RefPtr<GenericPromise> AsyncGetDataImpl(nsITransferable* aTransferable, + int32_t aWhichClipboard, + const char* aMimeType, + DataType aDataType) { + LOGCLIP("AsyncGetText() type '%s'", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + + RefPtr<GenericPromise::Private> dataPromise = + new GenericPromise::Private(__func__); + + const char* gtkMIMEType = nullptr; + switch (aDataType) { + case DATATYPE_FILE: + // Don't ask Gtk for application/x-moz-file + gtkMIMEType = kURIListMime; + break; + case DATATYPE_IMAGE: + case DATATYPE_HTML: + case DATATYPE_RAW: + gtkMIMEType = aMimeType; + break; + } + + gtk_clipboard_request_contents( + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)), + gdk_atom_intern(gtkMIMEType, FALSE), + [](GtkClipboard* aClipboard, GtkSelectionData* aSelection, + gpointer aData) -> void { + UniquePtr<DataPromiseHandler> ref( + static_cast<DataPromiseHandler*>(aData)); + LOGCLIP("AsyncGetData async handler [%p] MIME %s type %d", aData, + ref->mMimeType.get(), ref->mDataType); + + int dataLength = gtk_selection_data_get_length(aSelection); + if (dataLength <= 0) { + ref->mDataPromise->Resolve(false, __func__); + return; + } + const char* data = (const char*)gtk_selection_data_get_data(aSelection); + if (!data) { + ref->mDataPromise->Resolve(false, __func__); + return; + } + switch (ref->mDataType) { + case DATATYPE_IMAGE: { + LOGCLIP(" set image clipboard data"); + nsCOMPtr<nsIInputStream> byteStream; + NS_NewByteInputStream(getter_AddRefs(byteStream), + Span(data, dataLength), NS_ASSIGNMENT_COPY); + ref->mTransferable->SetTransferData(ref->mMimeType.get(), + byteStream); + break; + } + case DATATYPE_FILE: { + LOGCLIP(" set file clipboard data"); + nsDependentCSubstring file(data, dataLength); + TransferableSetFile(ref->mTransferable, file); + break; + } + case DATATYPE_HTML: { + LOGCLIP(" html clipboard data"); + Span dataSpan(data, dataLength); + TransferableSetHTML(ref->mTransferable, dataSpan); + break; + } + case DATATYPE_RAW: { + LOGCLIP(" raw clipboard data %s", ref->mMimeType.get()); + SetTransferableData(ref->mTransferable, ref->mMimeType, data, + dataLength); + break; + } + } + ref->mDataPromise->Resolve(true, __func__); + }, + new DataPromiseHandler(aTransferable, dataPromise, aMimeType, aDataType)); + return dataPromise; +} + +static RefPtr<GenericPromise> AsyncGetDataFlavor(nsITransferable* aTransferable, + int32_t aWhichClipboard, + nsCString& aFlavorStr) { + if (aFlavorStr.EqualsLiteral(kJPEGImageMime) || + aFlavorStr.EqualsLiteral(kJPGImageMime) || + aFlavorStr.EqualsLiteral(kPNGImageMime) || + aFlavorStr.EqualsLiteral(kGIFImageMime)) { + // Emulate support for image/jpg + if (aFlavorStr.EqualsLiteral(kJPGImageMime)) { + aFlavorStr.Assign(kJPEGImageMime); + } + LOGCLIP(" Getting image %s MIME clipboard data", aFlavorStr.get()); + return AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(), + DATATYPE_IMAGE); + } + // Special case text/plain since we can convert any + // string into text/plain + if (aFlavorStr.EqualsLiteral(kTextMime)) { + LOGCLIP(" Getting unicode clipboard data"); + return AsyncGetTextImpl(aTransferable, aWhichClipboard); + } + if (aFlavorStr.EqualsLiteral(kFileMime)) { + LOGCLIP(" Getting file clipboard data\n"); + return AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(), + DATATYPE_FILE); + } + if (aFlavorStr.EqualsLiteral(kHTMLMime)) { + LOGCLIP(" Getting HTML clipboard data"); + return AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(), + DATATYPE_HTML); + } + LOGCLIP(" Getting raw %s MIME clipboard data\n", aFlavorStr.get()); + return AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(), + DATATYPE_RAW); +} + +RefPtr<GenericPromise> nsClipboard::AsyncGetData(nsITransferable* aTransferable, + int32_t aWhichClipboard) { + LOGCLIP("nsClipboard::AsyncGetData (%s)", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + nsTArray<nsCString> importedFlavors; + nsresult rv = GetTransferableFlavors(aTransferable, importedFlavors); + NS_ENSURE_SUCCESS(rv, GenericPromise::CreateAndReject(rv, __func__)); + + auto flavorsNum = importedFlavors.Length(); + if (!flavorsNum) { + return GenericPromise::CreateAndResolve(false, __func__); + } +#ifdef MOZ_LOGGING + if (flavorsNum > 1) { + LOGCLIP(" Only first MIME type (%s) will be imported from clipboard!", + importedFlavors[0].get()); + } +#endif + + // Filter out MIME types on X11 to prevent unwanted conversions, + // see Bug 1611407 + if (widget::GdkIsX11Display()) { + return AsyncHasDataMatchingFlavors(importedFlavors, aWhichClipboard) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolve */ + [transferable = RefPtr{aTransferable}, + aWhichClipboard](nsTArray<nsCString> clipboardFlavors) { + if (!clipboardFlavors.Length()) { + LOGCLIP(" no flavors in clipboard, quit."); + return GenericPromise::CreateAndResolve(false, __func__); + } + return AsyncGetDataFlavor(transferable, aWhichClipboard, + clipboardFlavors[0]); + }, + /* reject */ + [](nsresult rv) { + LOGCLIP(" failed to get flavors from clipboard, quit."); + return GenericPromise::CreateAndReject(rv, __func__); + }); + } + + // Read clipboard directly on Wayland + return AsyncGetDataFlavor(aTransferable, aWhichClipboard, importedFlavors[0]); +} + +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); + } + } + ClearCachedTargets(aWhichClipboard); + 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; + } +} + +static bool FlavorMatchesTarget(const nsACString& aFlavor, GdkAtom aTarget) { + GUniquePtr<gchar> atom_name(gdk_atom_name(aTarget)); + if (!atom_name) { + return false; + } + if (aFlavor.Equals(atom_name.get())) { + LOGCLIP(" has %s\n", atom_name.get()); + return true; + } + // X clipboard supports image/jpeg, but we want to emulate support + // for image/jpg as well + if (aFlavor.EqualsLiteral(kJPGImageMime) && + !strcmp(atom_name.get(), kJPEGImageMime)) { + LOGCLIP(" has image/jpg\n"); + return true; + } + // application/x-moz-file should be treated like text/uri-list + if (aFlavor.EqualsLiteral(kFileMime) && + !strcmp(atom_name.get(), kURIListMime)) { + LOGCLIP(" has text/uri-list treating as application/x-moz-file"); + return true; + } + return false; +} + +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; + + if (!mContext) { + return NS_ERROR_FAILURE; + } + + auto targets = mContext->GetTargets(aWhichClipboard); + if (!targets) { + LOGCLIP(" no targes at clipboard (null)\n"); + return NS_OK; + } + +#ifdef MOZ_LOGGING + if (LOGCLIP_ENABLED()) { + LOGCLIP(" Asking for content:\n"); + for (auto& flavor : aFlavorList) { + LOGCLIP(" MIME %s\n", flavor.get()); + } + LOGCLIP(" Clipboard content (target nums %zu):\n", + targets.AsSpan().Length()); + for (const auto& target : targets.AsSpan()) { + GUniquePtr<gchar> atom_name(gdk_atom_name(target)); + if (!atom_name) { + LOGCLIP(" failed to get MIME\n"); + continue; + } + LOGCLIP(" MIME %s\n", atom_name.get()); + } + } +#endif + + // Walk through the provided types and try to match it to a + // provided type. + for (auto& flavor : aFlavorList) { + // We special case text/plain here. + if (flavor.EqualsLiteral(kTextMime) && + gtk_targets_include_text(targets.AsSpan().data(), + targets.AsSpan().Length())) { + *_retval = true; + LOGCLIP(" has kTextMime\n"); + return NS_OK; + } + for (const auto& target : targets.AsSpan()) { + if (FlavorMatchesTarget(flavor, target)) { + *_retval = true; + return NS_OK; + } + } + } + + LOGCLIP(" no targes at clipboard (bad match)\n"); + return NS_OK; +} + +struct TragetPromiseHandler { + TragetPromiseHandler(const nsTArray<nsCString>& aAcceptedFlavorList, + RefPtr<DataFlavorsPromise::Private> aTargetsPromise) + : mAcceptedFlavorList(aAcceptedFlavorList.Clone()), + mTargetsPromise(aTargetsPromise) { + LOGCLIP("TragetPromiseHandler(%p) created", this); + } + ~TragetPromiseHandler() { LOGCLIP("TragetPromiseHandler(%p) deleted", this); } + nsTArray<nsCString> mAcceptedFlavorList; + RefPtr<DataFlavorsPromise::Private> mTargetsPromise; +}; + +RefPtr<DataFlavorsPromise> nsClipboard::AsyncHasDataMatchingFlavors( + const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) { + LOGCLIP("nsClipboard::AsyncHasDataMatchingFlavors() type %s", + aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard"); + + RefPtr<DataFlavorsPromise::Private> flavorPromise = + new DataFlavorsPromise::Private(__func__); + gtk_clipboard_request_contents( + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)), + gdk_atom_intern("TARGETS", FALSE), + [](GtkClipboard* aClipboard, GtkSelectionData* aSelection, + gpointer aData) -> void { + LOGCLIP("gtk_clipboard_request_contents async handler (%p)", aData); + UniquePtr<TragetPromiseHandler> handler( + static_cast<TragetPromiseHandler*>(aData)); + + GdkAtom* targets = nullptr; + gint targetsNum = 0; + if (gtk_selection_data_get_length(aSelection) > 0) { + gtk_selection_data_get_targets(aSelection, &targets, &targetsNum); + } + nsTArray<nsCString> results; + if (targetsNum) { + for (auto& flavor : handler->mAcceptedFlavorList) { + LOGCLIP(" looking for %s", flavor.get()); + if (flavor.EqualsLiteral(kTextMime) && + gtk_targets_include_text(targets, targetsNum)) { + results.AppendElement(flavor); + LOGCLIP(" has kTextMime\n"); + continue; + } + for (int i = 0; i < targetsNum; i++) { + if (FlavorMatchesTarget(flavor, targets[i])) { + results.AppendElement(flavor); + } + } + } + } + handler->mTargetsPromise->Resolve(std::move(results), __func__); + }, + new TragetPromiseHandler(aFlavorList, flavorPromise)); + + return flavorPromise.forget(); +} + +NS_IMETHODIMP +nsClipboard::IsClipboardTypeSupported(int32_t aWhichClipboard, bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = kGlobalClipboard == aWhichClipboard || + kSelectionClipboard == aWhichClipboard; + 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/plain 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", + GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get()); + + // Check to see if the selection data is some text type. + if (gtk_targets_include_text(&selectionTarget, 1)) { + LOGCLIP(" providing text/plain data\n"); + // Try to convert our internal type into a text string. Get + // the transferable for this clipboard and try to get the + // text/plain type for it. + rv = trans->GetTransferData("text/plain", getter_AddRefs(item)); + if (NS_FAILED(rv) || !item) { + LOGCLIP(" GetTransferData() failed to get text/plain!\n"); + return; + } + + nsCOMPtr<nsISupportsString> wideString; + wideString = do_QueryInterface(item); + if (!wideString) return; + + nsAutoString ucs2string; + wideString->GetData(ucs2string); + NS_ConvertUTF16toUTF8 utf8string(ucs2string); + + LOGCLIP(" sent %zd 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; + } + + RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(image); + if (!pixbuf) { + LOGCLIP(" nsImageToPixbuf::ImageToPixbuf() failed!\n"); + return; + } + + LOGCLIP(" Setting pixbuf image data as %s\n", + GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get()); + gtk_selection_data_set_pixbuf(aSelectionData, 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 %zd bytes of %s data\n", html.Length(), + GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get()); + gtk_selection_data_set(aSelectionData, selectionTarget, 8, + (const guchar*)html.get(), html.Length()); + return; + } + + // We put kFileMime onto the clipboard as kURIListMime. + if (selectionTarget == gdk_atom_intern(kURIListMime, FALSE)) { + LOGCLIP(" providing %s data\n", kURIListMime); + rv = trans->GetTransferData(kFileMime, getter_AddRefs(item)); + if (NS_FAILED(rv) || !item) { + LOGCLIP(" failed to get %s data by GetTransferData()!\n", kFileMime); + return; + } + + nsCOMPtr<nsIFile> file = do_QueryInterface(item); + if (!file) { + LOGCLIP(" failed to get nsIFile interface!"); + return; + } + + nsCOMPtr<nsIURI> fileURI; + rv = NS_NewFileURI(getter_AddRefs(fileURI), file); + if (NS_FAILED(rv)) { + LOGCLIP(" failed to get fileURI\n"); + return; + } + + nsAutoCString uri; + if (NS_FAILED(fileURI->GetSpec(uri))) { + LOGCLIP(" failed to get fileURI spec\n"); + return; + } + + LOGCLIP(" Setting %zd bytes of data\n", uri.Length()); + gtk_selection_data_set(aSelectionData, selectionTarget, 8, + (const guchar*)uri.get(), uri.Length()); + return; + } + + LOGCLIP(" Try if we have anything at GetTransferData() for %s\n", + GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get()); + + // Try to match up the selection data target to something our + // transferable provides. + GUniquePtr<gchar> target_name(gdk_atom_name(selectionTarget)); + if (!target_name) { + LOGCLIP(" Failed to get target name!\n"); + return; + } + + rv = trans->GetTransferData(target_name.get(), getter_AddRefs(item)); + // nothing found? + if (NS_FAILED(rv) || !item) { + LOGCLIP(" Failed to get anything from GetTransferData()!\n"); + return; + } + + void* primitive_data = nullptr; + uint32_t dataLen = 0; + nsPrimitiveHelpers::CreateDataFromPrimitive( + nsDependentCString(target_name.get()), item, &primitive_data, &dataLen); + if (!primitive_data) { + LOGCLIP(" Failed to get primitive data!\n"); + return; + } + + LOGCLIP(" Setting %s as a primitive data type, %d bytes\n", + target_name.get(), dataLen); + gtk_selection_data_set(aSelectionData, selectionTarget, + 8, /* 8 bits in a unit */ + (const guchar*)primitive_data, dataLen); + free(primitive_data); +} + +void nsClipboard::ClearCachedTargets(int32_t aWhichClipboard) { + if (aWhichClipboard == kSelectionClipboard) { + nsRetrievalContext::ClearCachedTargetsPrimary(nullptr, nullptr, nullptr); + } else { + nsRetrievalContext::ClearCachedTargetsClipboard(nullptr, nullptr, nullptr); + } +} + +void nsClipboard::SelectionClearEvent(GtkClipboard* aGtkClipboard) { + int32_t whichClipboard = GetGeckoClipboardType(aGtkClipboard); + if (whichClipboard < 0) { + return; + } + LOGCLIP("nsClipboard::SelectionClearEvent (%s)\n", + whichClipboard == kSelectionClipboard ? "primary" : "clipboard"); + ClearCachedTargets(whichClipboard); + 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); +} + +/* + * This function extracts the encoding label from the subset of HTML internal + * encoding declaration syntax that uses the old long form with double quotes + * and without spaces around the equals sign between the "content" attribute + * name and the attribute value. + * + * This was added for the sake of an ancient version of StarOffice + * in the pre-UTF-8 era in bug 123389. It is unclear if supporting + * non-UTF-8 encodings is still necessary and if this function + * still needs to exist. + * + * As of December 2022, both Gecko and LibreOffice emit an UTF-8 + * declaration that this function successfully extracts "UTF-8" from, + * but that's also the default that we fall back on if this function + * fails to extract a label. + */ +bool GetHTMLCharset(Span<const char> aData, nsCString& aFoundCharset) { + // Assume ASCII first to find "charset" info + const nsDependentCSubstring htmlStr(aData); + 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) { + aFoundCharset = Substring(valueStart, valueEnd); + ToUpperCase(aFoundCharset); + return true; + } + return false; +} diff --git a/widget/gtk/nsClipboard.h b/widget/gtk/nsClipboard.h new file mode 100644 index 0000000000..95ddcbd2ca --- /dev/null +++ b/widget/gtk/nsClipboard.h @@ -0,0 +1,174 @@ +/* -*- 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 __nsClipboard_h_ +#define __nsClipboard_h_ + +#include "mozilla/UniquePtr.h" +#include "mozilla/Span.h" +#include "nsBaseClipboard.h" +#include "nsIClipboard.h" +#include "nsIObserver.h" +#include "nsCOMPtr.h" +#include "GUniquePtr.h" +#include <gtk/gtk.h> + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gClipboardLog; +# define LOGCLIP(...) \ + MOZ_LOG(gClipboardLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define LOGCLIP_ENABLED() \ + MOZ_LOG_TEST(gClipboardLog, mozilla::LogLevel::Debug) +#else +# define LOGCLIP(...) +# define LOGCLIP_ENABLED() false +#endif /* MOZ_LOGGING */ + +class ClipboardTargets { + friend class ClipboardData; + + mozilla::GUniquePtr<GdkAtom> mTargets; + uint32_t mCount = 0; + + public: + ClipboardTargets() = default; + ClipboardTargets(mozilla::GUniquePtr<GdkAtom> aTargets, uint32_t aCount) + : mTargets(std::move(aTargets)), mCount(aCount) {} + + void Set(ClipboardTargets); + ClipboardTargets Clone(); + void Clear() { + mTargets = nullptr; + mCount = 0; + }; + + mozilla::Span<GdkAtom> AsSpan() const { return {mTargets.get(), mCount}; } + explicit operator bool() const { return bool(mTargets); } +}; + +class ClipboardData { + mozilla::GUniquePtr<char> mData; + uint32_t mLength = 0; + + public: + ClipboardData() = default; + + void SetData(mozilla::Span<const uint8_t>); + void SetText(mozilla::Span<const char>); + void SetTargets(ClipboardTargets); + + ClipboardTargets ExtractTargets(); + mozilla::GUniquePtr<char> ExtractText() { + mLength = 0; + return std::move(mData); + } + + mozilla::Span<char> AsSpan() const { return {mData.get(), mLength}; } + explicit operator bool() const { return bool(mData); } +}; + +enum class ClipboardDataType { Data, Text, Targets }; + +class nsRetrievalContext { + public: + // We intentionally use unsafe thread refcount as clipboard is used in + // main thread only. + NS_INLINE_DECL_REFCOUNTING(nsRetrievalContext) + + // Get actual clipboard content (GetClipboardData/GetClipboardText). + virtual ClipboardData GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard) = 0; + virtual mozilla::GUniquePtr<char> GetClipboardText( + int32_t aWhichClipboard) = 0; + + // Get data mime types which can be obtained from clipboard. + ClipboardTargets GetTargets(int32_t aWhichClipboard); + + // Clipboard/Primary selection owner changed. Clear internal cached data. + static void ClearCachedTargetsClipboard(GtkClipboard* aClipboard, + GdkEvent* aEvent, gpointer data); + static void ClearCachedTargetsPrimary(GtkClipboard* aClipboard, + GdkEvent* aEvent, gpointer data); + + nsRetrievalContext(); + + protected: + virtual ClipboardTargets GetTargetsImpl(int32_t aWhichClipboard) = 0; + virtual ~nsRetrievalContext(); + + static ClipboardTargets sClipboardTargets; + static ClipboardTargets sPrimaryTargets; +}; + +class nsClipboard : public ClipboardSetDataHelper, public nsIObserver { + public: + nsClipboard(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIOBSERVER + + // nsIClipboard + NS_IMETHOD GetData(nsITransferable* aTransferable, + int32_t aWhichClipboard) override; + NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override; + NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList, + int32_t aWhichClipboard, + bool* _retval) override; + NS_IMETHOD IsClipboardTypeSupported(int32_t aWhichClipboard, + bool* _retval) override; + RefPtr<mozilla::GenericPromise> AsyncGetData( + nsITransferable* aTransferable, int32_t aWhichClipboard) override; + RefPtr<DataFlavorsPromise> AsyncHasDataMatchingFlavors( + const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override; + + // 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); + + protected: + // Implement the native clipboard behavior. + NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable, + nsIClipboardOwner* aOwner, + int32_t aWhichClipboard) override; + + private: + virtual ~nsClipboard(); + + // Get our hands on the correct transferable, given a specific + // clipboard + nsITransferable* GetTransferable(int32_t aWhichClipboard); + + void ClearTransferable(int32_t aWhichClipboard); + void ClearCachedTargets(int32_t aWhichClipboard); + + bool FilterImportedFlavors(int32_t aWhichClipboard, + nsTArray<nsCString>& aFlavors); + + // 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; + RefPtr<nsRetrievalContext> mContext; +}; + +extern const int kClipboardTimeout; +extern const int kClipboardFastIterationNum; + +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..d179f242dd --- /dev/null +++ b/widget/gtk/nsClipboardWayland.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "nsClipboardWayland.h" + +#include "AsyncGtkClipboardRequest.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/ScopeExit.h" +#include "prtime.h" + +#include <gtk/gtk.h> +#include <stdlib.h> +#include <string.h> + +using namespace mozilla; + +nsRetrievalContextWayland::nsRetrievalContextWayland() = default; + +ClipboardTargets nsRetrievalContextWayland::GetTargetsImpl( + int32_t aWhichClipboard) { + LOGCLIP("nsRetrievalContextWayland::GetTargetsImpl()\n"); + + return WaitForClipboardData(ClipboardDataType::Targets, aWhichClipboard) + .ExtractTargets(); +} + +ClipboardData nsRetrievalContextWayland::GetClipboardData( + const char* aMimeType, int32_t aWhichClipboard) { + LOGCLIP("nsRetrievalContextWayland::GetClipboardData() mime %s\n", aMimeType); + + return WaitForClipboardData(ClipboardDataType::Data, aWhichClipboard, + aMimeType); +} + +GUniquePtr<char> nsRetrievalContextWayland::GetClipboardText( + int32_t aWhichClipboard) { + GdkAtom selection = GetSelectionAtom(aWhichClipboard); + + LOGCLIP("nsRetrievalContextWayland::GetClipboardText(), clipboard %s\n", + (selection == GDK_SELECTION_PRIMARY) ? "Primary" : "Selection"); + + return WaitForClipboardData(ClipboardDataType::Text, aWhichClipboard) + .ExtractText(); +} + +ClipboardData nsRetrievalContextWayland::WaitForClipboardData( + ClipboardDataType aDataType, int32_t aWhichClipboard, + const char* aMimeType) { + LOGCLIP("nsRetrievalContextWayland::WaitForClipboardData, MIME %s\n", + aMimeType); + + AsyncGtkClipboardRequest request(aDataType, aWhichClipboard, aMimeType); + int iteration = 1; + + PRTime entryTime = PR_Now(); + while (!request.HasCompleted()) { + if (iteration++ > kClipboardFastIterationNum) { + /* sleep for 10 ms/iteration */ + PR_Sleep(PR_MillisecondsToInterval(10)); + if (PR_Now() - entryTime > kClipboardTimeout) { + LOGCLIP(" failed to get async clipboard data in time limit\n"); + break; + } + } + LOGCLIP("doing iteration %d msec %ld ...\n", (iteration - 1), + (long)((PR_Now() - entryTime) / 1000)); + gtk_main_iteration(); + } + + return request.TakeResult(); +} diff --git a/widget/gtk/nsClipboardWayland.h b/widget/gtk/nsClipboardWayland.h new file mode 100644 index 0000000000..cd8dda0772 --- /dev/null +++ b/widget/gtk/nsClipboardWayland.h @@ -0,0 +1,28 @@ +/* -*- 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 "mozilla/Mutex.h" +#include "nsClipboard.h" + +class nsRetrievalContextWayland final : public nsRetrievalContext { + public: + nsRetrievalContextWayland(); + + ClipboardData GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard) override; + mozilla::GUniquePtr<char> GetClipboardText(int32_t aWhichClipboard) override; + ClipboardTargets GetTargetsImpl(int32_t aWhichClipboard) override; + + private: + ClipboardData WaitForClipboardData(ClipboardDataType, int32_t aWhichClipboard, + const char* aMimeType = nullptr); +}; + +#endif /* __nsClipboardWayland_h_ */ diff --git a/widget/gtk/nsClipboardX11.cpp b/widget/gtk/nsClipboardX11.cpp new file mode 100644 index 0000000000..fc82e0dd8d --- /dev/null +++ b/widget/gtk/nsClipboardX11.cpp @@ -0,0 +1,169 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "AsyncGtkClipboardRequest.h" +#include "nsClipboardX11.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/WidgetUtilsGtk.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; + +nsRetrievalContextX11::nsRetrievalContextX11() = default; + +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; +} + +ClipboardData nsRetrievalContextX11::WaitForClipboardData( + ClipboardDataType aDataType, int32_t aWhichClipboard, + const char* aMimeType) { + AsyncGtkClipboardRequest request(aDataType, aWhichClipboard, aMimeType); + if (request.HasCompleted()) { + // the request completed synchronously + return request.TakeResult(); + } + + GdkDisplay* gdkDisplay = gdk_display_get_default(); + // gdk_display_get_default() returns null on headless + if (widget::GdkIsX11Display(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 (request.HasCompleted()) { + return request.TakeResult(); + } + } + + 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)); + } + + LOGCLIP("exceeded clipboard timeout"); + return {}; +} + +ClipboardTargets nsRetrievalContextX11::GetTargetsImpl( + int32_t aWhichClipboard) { + LOGCLIP("nsRetrievalContextX11::GetTargetsImpl(%s)\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + return WaitForClipboardData(ClipboardDataType::Targets, aWhichClipboard) + .ExtractTargets(); +} + +ClipboardData nsRetrievalContextX11::GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard) { + LOGCLIP("nsRetrievalContextX11::GetClipboardData(%s) MIME %s\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard", + aMimeType); + + return WaitForClipboardData(ClipboardDataType::Data, aWhichClipboard, + aMimeType); +} + +GUniquePtr<char> nsRetrievalContextX11::GetClipboardText( + int32_t aWhichClipboard) { + LOGCLIP("nsRetrievalContextX11::GetClipboardText(%s)\n", + aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + + return WaitForClipboardData(ClipboardDataType::Text, aWhichClipboard) + .ExtractText(); +} diff --git a/widget/gtk/nsClipboardX11.h b/widget/gtk/nsClipboardX11.h new file mode 100644 index 0000000000..c143222d3a --- /dev/null +++ b/widget/gtk/nsClipboardX11.h @@ -0,0 +1,32 @@ +/* -*- 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> + +#include "mozilla/Maybe.h" +#include "nsClipboard.h" + +class nsRetrievalContextX11 : public nsRetrievalContext { + public: + ClipboardData GetClipboardData(const char* aMimeType, + int32_t aWhichClipboard) override; + mozilla::GUniquePtr<char> GetClipboardText(int32_t aWhichClipboard) override; + ClipboardTargets GetTargetsImpl(int32_t aWhichClipboard) override; + + nsRetrievalContextX11(); + + private: + // Spins X event loop until timing out or being completed. + ClipboardData WaitForClipboardData(ClipboardDataType aDataType, + int32_t aWhichClipboard, + const char* aMimeType = nullptr); +}; + +#endif /* __nsClipboardX11_h_ */ diff --git a/widget/gtk/nsColorPicker.cpp b/widget/gtk/nsColorPicker.cpp new file mode 100644 index 0000000000..4719710d9e --- /dev/null +++ b/widget/gtk/nsColorPicker.cpp @@ -0,0 +1,253 @@ +/* -*- 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 "mozilla/Maybe.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "nsColor.h" +#include "nsColorPicker.h" +#include "nsGtkUtils.h" +#include "nsIWidget.h" +#include "WidgetUtils.h" +#include "nsPIDOMWindow.h" + +using mozilla::dom::HTMLInputElement; + +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, + const nsTArray<nsString>& aDefaultColors) { + auto* parent = nsPIDOMWindowOuter::From(aParent); + mParentWidget = mozilla::widget::WidgetUtils::DOMWindowToWidget(parent); + mTitle = title; + mInitialColor = initialColor; + mDefaultColors.Assign(aDefaultColors); + + return NS_OK; +} + +NS_IMETHODIMP nsColorPicker::Open( + nsIColorPickerShownCallback* aColorPickerShownCallback) { + auto maybeColor = HTMLInputElement::ParseSimpleColor(mInitialColor); + if (maybeColor.isNothing()) { + return NS_ERROR_FAILURE; + } + nscolor color = maybeColor.value(); + + 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.get(), 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); + + // Setting the default colors will put them into "Custom" colors list. + for (const nsString& defaultColor : mDefaultColors) { + if (auto color = HTMLInputElement::ParseSimpleColor(defaultColor)) { + GdkRGBA color_rgba = convertToRgbaColor(*color); + gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(color_chooser), &color_rgba); + } + } + + // The initial color needs to be set last. + 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..d693175717 --- /dev/null +++ b/widget/gtk/nsColorPicker.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsColorPicker_h__ +#define nsColorPicker_h__ + +#include <gtk/gtk.h> + +#include "nsCOMPtr.h" +#include "nsIColorPicker.h" +#include "nsString.h" + +// Enable Gtk3 system color picker. +#define ACTIVATE_GTK3_COLOR_PICKER 1 + +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; + nsTArray<nsString> mDefaultColors; +}; + +#endif // nsColorPicker_h__ diff --git a/widget/gtk/nsDeviceContextSpecG.cpp b/widget/gtk/nsDeviceContextSpecG.cpp new file mode 100644 index 0000000000..3dfa816408 --- /dev/null +++ b/widget/gtk/nsDeviceContextSpecG.cpp @@ -0,0 +1,422 @@ +/* -*- 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/PrintPromise.h" +#include "mozilla/gfx/PrintTargetPDF.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/WidgetUtilsGtk.h" + +#include "prenv.h" /* for PR_GetEnv */ + +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "nsPrintfCString.h" +#include "nsQueryObject.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 <dlfcn.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.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::PrintEndDocumentPromise; +using mozilla::gfx::PrintTarget; +using mozilla::gfx::PrintTargetPDF; + +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; + + // We shouldn't be attempting to get a surface if we've already got a spool + // file. + MOZ_ASSERT(!mSpoolFile); + + auto stream = [&]() -> nsCOMPtr<nsIOutputStream> { + if (mPrintSettings->GetOutputDestination() == + nsIPrintSettings::kOutputDestinationStream) { + nsCOMPtr<nsIOutputStream> out; + mPrintSettings->GetOutputStream(getter_AddRefs(out)); + return out; + } + // 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); + if (NS_FAILED(NS_NewNativeLocalFile(nsDependentCString(buf), false, + getter_AddRefs(mSpoolFile)))) { + 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"); + if (NS_FAILED(stream->Init(mSpoolFile, -1, -1, 0))) { + return nullptr; + } + return stream; + }(); + + return PrintTargetPDF::CreateOrNull(stream, IntSize::Ceil(width, height)); +} + +#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 + +// https://developer.gnome.org/gtk3/stable/GtkPaperSize.html#gtk-paper-size-new-from-ipp +static GUniquePtr<GtkPaperSize> GtkPaperSizeFromIpp(const gchar* aIppName, + gdouble aWidth, + gdouble aHeight) { + static auto sPtr = (GtkPaperSize * (*)(const gchar*, gdouble, gdouble)) + dlsym(RTLD_DEFAULT, "gtk_paper_size_new_from_ipp"); + if (gtk_check_version(3, 16, 0)) { + return nullptr; + } + return GUniquePtr<GtkPaperSize>(sPtr(aIppName, aWidth, aHeight)); +} + +static bool PaperSizeAlmostEquals(GtkPaperSize* aSize, + GtkPaperSize* aOtherSize) { + const double kEpsilon = 1.0; // millimetres + // GTK stores sizes internally in millimetres so just use that. + if (fabs(gtk_paper_size_get_height(aSize, GTK_UNIT_MM) - + gtk_paper_size_get_height(aOtherSize, GTK_UNIT_MM)) > kEpsilon) { + return false; + } + if (fabs(gtk_paper_size_get_width(aSize, GTK_UNIT_MM) - + gtk_paper_size_get_width(aOtherSize, GTK_UNIT_MM)) > kEpsilon) { + return false; + } + return true; +} + +// Prefer the ppd name because some printers don't deal well even with standard +// ipp names. +static GUniquePtr<GtkPaperSize> PpdSizeFromIppName(const gchar* aIppName) { + static constexpr struct { + const char* mCups; + const char* mGtk; + } kMap[] = { + {CUPS_MEDIA_A3, GTK_PAPER_NAME_A3}, + {CUPS_MEDIA_A4, GTK_PAPER_NAME_A4}, + {CUPS_MEDIA_A5, GTK_PAPER_NAME_A5}, + {CUPS_MEDIA_LETTER, GTK_PAPER_NAME_LETTER}, + {CUPS_MEDIA_LEGAL, GTK_PAPER_NAME_LEGAL}, + // Other gtk sizes with no standard CUPS constant: _EXECUTIVE and _B5 + }; + + for (const auto& entry : kMap) { + if (!strcmp(entry.mCups, aIppName)) { + return GUniquePtr<GtkPaperSize>(gtk_paper_size_new(entry.mGtk)); + } + } + + return nullptr; +} + +// 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. +// +// We prefer ppd to ipp to custom sizes. +// +// See bug 414314, bug 1691798, and bug 1717292 for more info. +static GUniquePtr<GtkPaperSize> GetStandardGtkPaperSize( + GtkPaperSize* aGeckoPaperSize) { + // We should get an ipp name from cups, try to get a ppd from that first. + const gchar* geckoName = gtk_paper_size_get_name(aGeckoPaperSize); + if (auto ppd = PpdSizeFromIppName(geckoName)) { + return ppd; + } + + // We try gtk_paper_size_new_from_ipp next, because even though + // gtk_paper_size_new tries to deal with ipp, it has some rounding issues that + // the ipp equivalent doesn't have, see + // https://gitlab.gnome.org/GNOME/gtk/-/issues/3685. + if (auto ipp = GtkPaperSizeFromIpp( + geckoName, gtk_paper_size_get_width(aGeckoPaperSize, GTK_UNIT_POINTS), + gtk_paper_size_get_height(aGeckoPaperSize, GTK_UNIT_POINTS))) { + if (!gtk_paper_size_is_custom(ipp.get())) { + if (auto ppd = PpdSizeFromIppName(gtk_paper_size_get_name(ipp.get()))) { + return ppd; + } + return ipp; + } + } + + GUniquePtr<GtkPaperSize> size(gtk_paper_size_new(geckoName)); + // gtk_paper_size_is_equal compares just paper names. The name in Gecko + // might come from CUPS, which is an ipp size, and gets normalized by gtk. + // So check also for the same actual paper size. + if (gtk_paper_size_is_equal(size.get(), aGeckoPaperSize) || + PaperSizeAlmostEquals(aGeckoPaperSize, size.get())) { + return size; + } + + // Not the same after all, so use our custom paper sizes instead. + return nullptr; +} + +/** ------------------------------------------------------- + * Initialize the nsDeviceContextSpecGTK + * @update dc 2/15/98 + * @update syd 3/2/99 + */ +NS_IMETHODIMP nsDeviceContextSpecGTK::Init(nsIPrintSettings* aPS, + bool aIsPrintPreview) { + RefPtr<nsPrintSettingsGTK> settings = do_QueryObject(aPS); + if (!settings) { + return NS_ERROR_NO_INTERFACE; + } + mPrintSettings = aPS; + + mGtkPrintSettings = settings->GetGtkPrintSettings(); + mGtkPageSetup = settings->GetGtkPageSetup(); + + GtkPaperSize* geckoPaperSize = gtk_page_setup_get_paper_size(mGtkPageSetup); + GUniquePtr<GtkPaperSize> gtkPaperSize = + GetStandardGtkPaperSize(geckoPaperSize); + + 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 = + gtkPaperSize ? gtkPaperSize.get() : geckoPaperSize; + gtk_print_settings_set_paper_size(mGtkPrintSettings, properPaperSize); + gtk_page_setup_set_paper_size_and_default_margins(mGtkPageSetup, + properPaperSize); + 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; + + if (spec->mHasEnumerationFoundAMatch) { + // We're already done, but we're letting the enumeration run its course, + // to avoid a GTK bug. + return FALSE; + } + + // 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)) { + nsPrintSettingsGTK::From(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)); + + // We're already done, but we need to let the enumeration run its course, + // to avoid a GTK bug. So we record that we've found a match and + // then return FALSE. + // TODO: If/when we can be sure that GTK handles this OK, we could + // return TRUE to avoid some needless enumeration. + spec->mHasEnumerationFoundAMatch = true; + return FALSE; + } + } + + // We haven't found it yet - keep searching... + return FALSE; +} + +void nsDeviceContextSpecGTK::StartPrintJob() { + GtkPrintJob* job = gtk_print_job_new( + mTitle.get(), nsPrintSettingsGTK::From(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() { + mHasEnumerationFoundAMatch = false; + 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; +} + +RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecGTK::EndDocument() { + switch (mPrintSettings->GetOutputDestination()) { + case nsIPrintSettings::kOutputDestinationPrinter: { + // 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 (nsPrintSettingsGTK::From(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)); + } + break; + } + case nsIPrintSettings::kOutputDestinationFile: { + // 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)); + if (NS_FAILED(rv)) { + return PrintEndDocumentPromise::CreateAndReject(rv, __func__); + } + + return nsIDeviceContextSpec::EndDocumentAsync( + __func__, + [destFile = std::move(destFile), + spoolFile = std::move(mSpoolFile)]() -> nsresult { + nsAutoString destLeafName; + auto rv = destFile->GetLeafName(destLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> destDir; + rv = destFile->GetParent(getter_AddRefs(destDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = spoolFile->MoveTo(destDir, destLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + // 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)); + return NS_OK; + }); + + break; + } + case nsIPrintSettings::kOutputDestinationStream: + // Nothing to do, handled in MakePrintTarget. + MOZ_ASSERT(!mSpoolFile); + break; + } + return PrintEndDocumentPromise::CreateAndResolve(true, __func__); +} diff --git a/widget/gtk/nsDeviceContextSpecG.h b/widget/gtk/nsDeviceContextSpecG.h new file mode 100644 index 0000000000..f06b32ae47 --- /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 "mozilla/gfx/PrintPromise.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(nsIPrintSettings* aPS, bool aIsPrintPreview) override; + NS_IMETHOD BeginDocument(const nsAString& aTitle, + const nsAString& aPrintToFileName, + int32_t aStartPage, int32_t aEndPage) override; + RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override; + NS_IMETHOD BeginPage() override { return NS_OK; } + NS_IMETHOD EndPage() override { return NS_OK; } + + protected: + virtual ~nsDeviceContextSpecGTK(); + GtkPrintSettings* mGtkPrintSettings; + GtkPageSetup* mGtkPageSetup; + + nsCString mSpoolName; + nsCOMPtr<nsIFile> mSpoolFile; + nsCString mTitle; + // Helper for EnumeratePrinters / PrinterEnumerator: + bool mHasEnumerationFoundAMatch = false; + + 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..a650799506 --- /dev/null +++ b/widget/gtk/nsDragService.cpp @@ -0,0 +1,2658 @@ +/* -*- 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 "nsCRT.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Services.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/WidgetUtilsGtk.h" +#include "GRefPtr.h" +#include "nsAppShell.h" + +#ifdef MOZ_X11 +# include "gfxXlibSurface.h" +#endif +#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" +#include "nsStringStream.h" +#include "nsDirectoryService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsEscape.h" +#include "nsString.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +// The maximum time to wait for a "drag_received" arrived in microseconds. +#define NS_DND_TIMEOUT 1000000 + +// The maximum time to wait before temporary files resulting +// from drag'n'drop events will be removed in miliseconds. +// It's set to 5 min as the file has to be present some time after drop +// event to give target application time to get the data. +// (A target application can throw a dialog to ask user what to do with +// the data and will access the tmp file after user action.) +#define NS_DND_TMP_CLEANUP_TIMEOUT (1000 * 60 * 5) + +#define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1" + +// This sets how opaque the drag image is +#define DRAG_IMAGE_ALPHA_LEVEL 0.5 + +#ifdef MOZ_LOGGING +extern mozilla::LazyLogModule gWidgetDragLog; +# define LOGDRAGSERVICE(str, ...) \ + MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, \ + ("[Depth %d]: " str, GetLoopDepth(), ##__VA_ARGS__)) +# define LOGDRAGSERVICESTATIC(str, ...) \ + MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__)) +#else +# define LOGDRAGSERVICE(...) +#endif + +// Helper class to block native events processing. +class MOZ_STACK_CLASS AutoSuspendNativeEvents { + public: + AutoSuspendNativeEvents() { + mAppShell = do_GetService(NS_APPSHELL_CID); + mAppShell->SuspendNative(); + } + ~AutoSuspendNativeEvents() { mAppShell->ResumeNative(); } + + private: + nsCOMPtr<nsIAppShell> mAppShell; +}; + +// 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 GUniquePtr<GdkEvent> TakeMotionEvent() { + GUniquePtr<GdkEvent> event(sMotionEvent); + sMotionEvent = nullptr; + return event; +} +static void SetMotionEvent(GUniquePtr<GdkEvent> aEvent) { + TakeMotionEvent(); + sMotionEvent = aEvent.release(); +} + +static GtkWidget* sGrabWidget; + +static constexpr nsLiteralString kDisallowedExportedSchemes[] = { + u"about"_ns, u"blob"_ns, u"chrome"_ns, u"imap"_ns, + u"javascript"_ns, u"mailbox"_ns, u"moz-anno"_ns, u"news"_ns, + u"page-icon"_ns, u"resource"_ns, u"view-source"_ns, u"moz-extension"_ns, +}; + +// _NETSCAPE_URL is similar to text/uri-list type. +// Format is UTF8: URL + "\n" + title. +// While text/uri-list tells target application to fetch, copy and store data +// from URL, _NETSCAPE_URL suggest to create a link to the target. +// Also _NETSCAPE_URL points to only one item while text/uri-list can point to +// multiple ones. +static const char gMozUrlType[] = "_NETSCAPE_URL"; +static const char gMimeListType[] = "application/x-moz-internal-item-list"; +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"; + +// See https://docs.gtk.org/gtk3/enum.DragResult.html +static const char kGtkDragResults[][100]{ + "GTK_DRAG_RESULT_SUCCESS", "GTK_DRAG_RESULT_NO_TARGET", + "GTK_DRAG_RESULT_USER_CANCELLED", "GTK_DRAG_RESULT_TIMEOUT_EXPIRED", + "GTK_DRAG_RESULT_GRAB_BROKEN", "GTK_DRAG_RESULT_ERROR"}; + +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), + mScheduledTaskIsRunning(false), + mCachedDragContext() { + // 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 + mCanDrop = false; + mTargetDragDataReceived = false; + mTargetDragData = 0; + mTargetDragDataLen = 0; + mTempFileTimerID = 0; + mEventLoopDepth = 0; + LOGDRAGSERVICE("nsDragService::nsDragService"); +} + +nsDragService::~nsDragService() { + LOGDRAGSERVICE("nsDragService::~nsDragService"); + if (mTaskSource) g_source_remove(mTaskSource); + if (mTempFileTimerID) { + g_source_remove(mTempFileTimerID); + RemoveTempFiles(); + } +} + +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")) { + LOGDRAGSERVICE("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; + + GUniquePtr<GdkEvent> event = TakeMotionEvent(); + // 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.get()); + } + + // 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) { + SetMotionEvent(GUniquePtr<GdkEvent>(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(); + if (!widget) return nullptr; + + GtkWidget* gtkWidget = static_cast<nsWindow*>(widget.get())->GetGtkWidget(); + 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) { + LOGDRAGSERVICE("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; + + LOGDRAGSERVICE("nsDragService::InvokeDragSessionImpl"); + + // 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); + + GdkEvent* existingEvent = widget::GetLastMousePressEvent(); + GdkEvent fakeEvent; + if (!existingEvent) { + // 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. + memset(&fakeEvent, 0, sizeof(GdkEvent)); + fakeEvent.type = GDK_BUTTON_PRESS; + fakeEvent.button.window = gtk_widget_get_window(mHiddenWidget); + fakeEvent.button.time = nsWindow::GetLastUserInputTime(); + fakeEvent.button.device = widget::GdkGetPointer(); + } + + // 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)); + + // start our drag. + GdkDragContext* context = gtk_drag_begin_with_coordinates( + mHiddenWidget, sourceList, action, 1, + existingEvent ? existingEvent : &fakeEvent, -1, -1); + + if (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol()) { + GdkDevice* device = gdk_drag_context_get_device(context); + GdkWindow* gdkWindow = + gdk_device_get_window_at_position(device, nullptr, nullptr); + mSourceWindow = nsWindow::GetWindow(gdkWindow); + if (mSourceWindow) { + mSourceWindow->SetDragSource(context); + } + } + + LOGDRAGSERVICE(" GdkDragContext [%p] nsWindow [%p]", context, + mSourceWindow.get()); + + 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() { + LOGDRAGSERVICE("nsDragService::StartDragSession"); + mTempFileUrls.Clear(); + return nsBaseDragService::StartDragSession(); +} + +bool nsDragService::RemoveTempFiles() { + LOGDRAGSERVICE("nsDragService::RemoveTempFiles"); + + // We can not delete the temporary files immediately after the + // drag has finished, because the target application might have not + // copied the temporary file yet. The Qt toolkit does not provide a + // way to mark a drop as finished in an asynchronous way, so most + // Qt based applications do send the dnd_finished signal before they + // have actually accessed the data from the temporary file. + // (https://bugreports.qt.io/browse/QTBUG-5441) + // + // To work also with these applications we collect all temporary + // files in mTemporaryFiles array and remove them here in the timer event. + auto files = std::move(mTemporaryFiles); + for (nsIFile* file : files) { +#ifdef MOZ_LOGGING + if (MOZ_LOG_TEST(gWidgetDragLog, LogLevel::Debug)) { + nsAutoCString path; + if (NS_SUCCEEDED(file->GetNativePath(path))) { + LOGDRAGSERVICE(" removing %s", path.get()); + } + } +#endif + file->Remove(/* recursive = */ true); + } + MOZ_ASSERT(mTemporaryFiles.IsEmpty()); + mTempFileTimerID = 0; + // Return false to remove the timer added by g_timeout_add_full(). + return false; +} + +gboolean nsDragService::TaskRemoveTempFiles(gpointer data) { + RefPtr<nsDragService> dragService = static_cast<nsDragService*>(data); + return dragService->RemoveTempFiles(); +} + +NS_IMETHODIMP +nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) { + LOGDRAGSERVICE("nsDragService::EndDragSession(%p) %d", + mTargetDragContext.get(), 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) { + TakeMotionEvent(); + } + } + + // unset our drag action + SetDragAction(DRAGDROP_ACTION_NONE); + + // start timer to remove temporary files + if (mTemporaryFiles.Count() > 0 && !mTempFileTimerID) { + LOGDRAGSERVICE(" queue removing of temporary files"); + // |this| won't be used after nsDragService delete because the timer is + // removed in the nsDragService destructor. + mTempFileTimerID = + g_timeout_add(NS_DND_TMP_CLEANUP_TIMEOUT, TaskRemoveTempFiles, this); + mTempFileUrls.Clear(); + } + + // We're done with the drag context. + if (mSourceWindow) { + mSourceWindow->SetDragSource(nullptr); + mSourceWindow = nullptr; + } + mTargetDragContextForRemote = nullptr; + mTargetWindow = nullptr; + mPendingWindow = nullptr; + mCachedDragContext = 0; + + return nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers); +} + +// nsIDragSession +NS_IMETHODIMP +nsDragService::SetCanDrop(bool aCanDrop) { + LOGDRAGSERVICE("nsDragService::SetCanDrop %d", aCanDrop); + mCanDrop = aCanDrop; + return NS_OK; +} + +NS_IMETHODIMP +nsDragService::GetCanDrop(bool* aCanDrop) { + LOGDRAGSERVICE("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); + } +} + +// Spins event loop, called from JS. +// Can lead to another round of drag_motion events. +NS_IMETHODIMP +nsDragService::GetNumDropItems(uint32_t* aNumItems) { + LOGDRAGSERVICE("nsDragService::GetNumDropItems"); + + if (!mTargetWidget) { + LOGDRAGSERVICE( + "*** warning: GetNumDropItems \ + called without a valid target widget!\n"); + *aNumItems = 0; + return NS_OK; + } + + bool isList = IsTargetContextList(); + if (isList) { + if (!mSourceDataItems) { + *aNumItems = 0; + return NS_OK; + } + mSourceDataItems->GetLength(aNumItems); + } else { + GdkAtom gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE); + if (!gdkFlavor) { + *aNumItems = 0; + return NS_OK; + } + + nsTArray<nsCString> dragFlavors; + GetDragFlavors(dragFlavors); + GetTargetDragData(gdkFlavor, dragFlavors); + + if (mTargetDragData) { + const char* data = reinterpret_cast<char*>(mTargetDragData); + *aNumItems = CountTextUriListItems(data, mTargetDragDataLen); + } else + *aNumItems = 1; + } + LOGDRAGSERVICE(" NumOfDropItems %d", *aNumItems); + return NS_OK; +} + +void nsDragService::GetDragFlavors(nsTArray<nsCString>& aFlavors) { + for (GList* tmp = gdk_drag_context_list_targets(mTargetDragContext); tmp; + tmp = tmp->next) { + GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data); + GUniquePtr<gchar> name(gdk_atom_name(atom)); + if (!name) { + continue; + } + aFlavors.AppendElement(nsCString(name.get())); + } +} + +// Spins event loop, called from JS. +// Can lead to another round of drag_motion events. +NS_IMETHODIMP +nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) { + LOGDRAGSERVICE("nsDragService::GetData(), index %d", aItemIndex); + + // make sure that we have a transferable + if (!aTransferable) { + return NS_ERROR_INVALID_ARG; + } + + if (!mTargetWidget) { + LOGDRAGSERVICE( + "*** failed: 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)) { + LOGDRAGSERVICE(" failed to get flavors, quit."); + return rv; + } + + // check to see if this is an internal list + bool isList = IsTargetContextList(); + + if (isList) { + LOGDRAGSERVICE(" Process as a list..."); + // find a matching flavor + for (uint32_t i = 0; i < flavors.Length(); ++i) { + nsCString& flavorStr = flavors[i]; + LOGDRAGSERVICE(" [%d] flavor is %s\n", i, flavorStr.get()); + // get the item with the right index + nsCOMPtr<nsITransferable> item = + do_QueryElementAt(mSourceDataItems, aItemIndex); + if (!item) continue; + + nsCOMPtr<nsISupports> data; + LOGDRAGSERVICE(" trying to get transfer data for %s\n", flavorStr.get()); + rv = item->GetTransferData(flavorStr.get(), getter_AddRefs(data)); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" failed.\n"); + continue; + } + rv = aTransferable->SetTransferData(flavorStr.get(), data); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" fail to set transfer data into transferable!\n"); + continue; + } + LOGDRAGSERVICE(" succeeded\n"); + // ok, we got the data + return NS_OK; + } + // if we got this far, we failed + LOGDRAGSERVICE(" failed to match flavors\n"); + return NS_ERROR_FAILURE; + } + + nsTArray<nsCString> dragFlavors; + GetDragFlavors(dragFlavors); + + // 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; + if (flavorStr.EqualsLiteral(kTextMime)) { + gdkFlavor = gdk_atom_intern(gTextPlainUTF8Type, FALSE); + } else { + gdkFlavor = gdk_atom_intern(flavorStr.get(), FALSE); + } + LOGDRAGSERVICE(" we're getting data %s (gdk flavor %p)\n", flavorStr.get(), + gdkFlavor); + bool dataFound = false; + if (gdkFlavor) { + GetTargetDragData(gdkFlavor, dragFlavors); + } + if (mTargetDragData) { + LOGDRAGSERVICE(" dataFound = true\n"); + dataFound = true; + } else { + LOGDRAGSERVICE(" dataFound = false, try conversions\n"); + + // Dragging and dropping from the file manager would cause us + // to parse the source text as a nsIFile URL. + if (flavorStr.EqualsLiteral(kFileMime)) { + LOGDRAGSERVICE(" conversion %s => %s", kFileMime, gTextUriListType); + gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE); + GetTargetDragData(gdkFlavor, dragFlavors); + 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. + LOGDRAGSERVICE(" set as file %s", + NS_ConvertUTF16toUTF8(convertedText).get()); + aTransferable->SetTransferData(flavorStr.get(), file); + g_free(convertedText); + return NS_OK; + } + } + } + g_free(convertedText); + } + continue; + } + } + + // If we are looking for text/plain, try again with non utf-8 text. + if (flavorStr.EqualsLiteral(kTextMime)) { + LOGDRAGSERVICE(" conversion %s => %s", kTextMime, kTextMime); + gdkFlavor = gdk_atom_intern(kTextMime, FALSE); + GetTargetDragData(gdkFlavor, dragFlavors); + if (mTargetDragData) { + dataFound = true; + } // if plain text flavor present + } // if looking for text/plain + + // 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)) { + LOGDRAGSERVICE(" conversion %s => %s", kURLMime, gTextUriListType); + gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE); + GetTargetDragData(gdkFlavor, dragFlavors); + if (mTargetDragData) { + const char* data = reinterpret_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + uint32_t convertedTextLen = 0; + + GetTextUriListItem(data, mTargetDragDataLen, aItemIndex, + &convertedText, &convertedTextLen); + + if (convertedText) { + // out with the old, in with the new + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = convertedTextLen * 2; + dataFound = true; + } + } + if (!dataFound) { + LOGDRAGSERVICE(" conversion %s => %s", kURLMime, gMozUrlType); + gdkFlavor = gdk_atom_intern(gMozUrlType, FALSE); + GetTargetDragData(gdkFlavor, dragFlavors); + if (mTargetDragData) { + const char* castedText = reinterpret_cast<char*>(mTargetDragData); + char16_t* convertedText = nullptr; + uint32_t convertedTextLen = 0; + UTF8ToNewUTF16(castedText, mTargetDragDataLen, &convertedText, + &convertedTextLen); + if (convertedText) { + // out with the old, in with the new + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = convertedTextLen * 2; + dataFound = true; + } + } + } + } + + } // else we try one last ditch effort to find our data + + if (dataFound) { + LOGDRAGSERVICE(" actual data found %s\n", + GUniquePtr<gchar>(gdk_atom_name(gdkFlavor)).get()); + + if (flavorStr.EqualsLiteral(kTextMime)) { + // The text is in UTF-8, so convert the text into UTF-16 + const char* text = static_cast<char*>(mTargetDragData); + NS_ConvertUTF8toUTF16 ucs2string(text, mTargetDragDataLen); + char16_t* convertedText = ToNewUnicode(ucs2string, mozilla::fallible); + if (convertedText) { + g_free(mTargetDragData); + mTargetDragData = convertedText; + mTargetDragDataLen = ucs2string.Length() * 2; + } + } + + if (flavorStr.EqualsLiteral(kJPEGImageMime) || + flavorStr.EqualsLiteral(kJPGImageMime) || + flavorStr.EqualsLiteral(kPNGImageMime) || + flavorStr.EqualsLiteral(kGIFImageMime)) { + LOGDRAGSERVICE(" saving as image %s\n", flavorStr.get()); + + nsCOMPtr<nsIInputStream> byteStream; + NS_NewByteInputStream(getter_AddRefs(byteStream), + Span((char*)mTargetDragData, mTargetDragDataLen), + NS_ASSIGNMENT_COPY); + aTransferable->SetTransferData(flavorStr.get(), byteStream); + continue; + } + + if (!flavorStr.EqualsLiteral(kCustomTypesMime)) { + // the DOM only wants LF, so convert from MacOS line endings + // to DOM line endings. + nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks( + flavorStr.EqualsLiteral(kRTFMime), &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! + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) { + LOGDRAGSERVICE("nsDragService::IsDataFlavorSupported(%p) %s", + mTargetDragContext.get(), 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) { + LOGDRAGSERVICE( + "*** 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) { + LOGDRAGSERVICE(" 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) { + LOGDRAGSERVICE(" quit"); + return NS_OK; + } + mSourceDataItems->GetLength(&numDragItems); + LOGDRAGSERVICE(" drag items %d", 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) { + LOGDRAGSERVICE(" checking %s against %s\n", flavors[i].get(), + aDataFlavor); + if (flavors[i].Equals(aDataFlavor)) { + LOGDRAGSERVICE(" found.\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); + } + + for (; tmp; tmp = tmp->next) { + /* Bug 331198 */ + GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data); + GUniquePtr<gchar> name(gdk_atom_name(atom)); + if (!name) { + continue; + } + + if (strcmp(name.get(), aDataFlavor) == 0) { + *_retval = true; + } + // check for automatic text/uri-list -> text/x-moz-url mapping + else if (strcmp(name.get(), gTextUriListType) == 0 && + (strcmp(aDataFlavor, kURLMime) == 0 || + strcmp(aDataFlavor, kFileMime) == 0)) { + *_retval = true; + } + // check for automatic _NETSCAPE_URL -> text/x-moz-url mapping + else if (strcmp(name.get(), gMozUrlType) == 0 && + (strcmp(aDataFlavor, kURLMime) == 0)) { + *_retval = true; + } + + if (*_retval) { + LOGDRAGSERVICE(" supported, with converting %s => %s", name.get(), + aDataFlavor); + } + } + + if (!*_retval) { + LOGDRAGSERVICE(" %s is not supported", aDataFlavor); + } + + return NS_OK; +} + +void nsDragService::ReplyToDragMotion(GdkDragContext* aDragContext, + guint aTime) { + LOGDRAGSERVICE("nsDragService::ReplyToDragMotion(%p) can drop %d", + aDragContext, mCanDrop); + + GdkDragAction action = (GdkDragAction)0; + if (mCanDrop) { + // notify the dragger if we can drop + switch (mDragAction) { + case DRAGDROP_ACTION_COPY: + LOGDRAGSERVICE(" set explicit action copy"); + action = GDK_ACTION_COPY; + break; + case DRAGDROP_ACTION_LINK: + LOGDRAGSERVICE(" set explicit action link"); + action = GDK_ACTION_LINK; + break; + case DRAGDROP_ACTION_NONE: + LOGDRAGSERVICE(" set explicit action none"); + action = (GdkDragAction)0; + break; + default: + LOGDRAGSERVICE(" set explicit action move"); + action = GDK_ACTION_MOVE; + break; + } + } else { + LOGDRAGSERVICE(" mCanDrop is false, disable drop"); + } + + // gdk_drag_status() is a kind of red herring here. + // It does not control final D&D operation type (copy/move) but controls + // drop/no-drop D&D state and default cursor type (copy/move). + + // Actual D&D operation is determined by mDragAction which is set by + // SetDragAction() from UpdateDragAction() or gecko/layout. + + // State passed to gdk_drag_status() sets default D&D cursor type + // which can be switched by key control (CTRL/SHIFT). + // If user changes D&D cursor (and D&D operation) we're notified by + // gdk_drag_context_get_selected_action() and update mDragAction. + + // But if we pass mDragAction back to gdk_drag_status() the D&D operation + // becames locked and won't be returned when D&D modifiers (CTRL/SHIFT) + // are released. + + // This gdk_drag_context_get_selected_action() -> gdk_drag_status() -> + // gdk_drag_context_get_selected_action() cycle happens on Wayland. + if (widget::GdkIsWaylandDisplay() && action == GDK_ACTION_COPY) { + LOGDRAGSERVICE(" Wayland: switch copy to move"); + action = GDK_ACTION_MOVE; + } + + LOGDRAGSERVICE(" gdk_drag_status() action %d", action); + gdk_drag_status(aDragContext, action, aTime); +} + +void nsDragService::EnsureCachedDataValidForContext( + GdkDragContext* aDragContext) { + if (mCachedDragContext != (uintptr_t)aDragContext) { + mCachedData.Clear(); + mCachedDragContext = (uintptr_t)aDragContext; + } +} + +void nsDragService::TargetDataReceived(GtkWidget* aWidget, + GdkDragContext* aContext, gint aX, + gint aY, + GtkSelectionData* aSelectionData, + guint aInfo, guint32 aTime) { + LOGDRAGSERVICE("nsDragService::TargetDataReceived(%p)", aContext); + TargetResetData(); + + EnsureCachedDataValidForContext(aContext); + + 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); + GUniquePtr<gchar> name(gdk_atom_name(target)); + nsDependentCString flavor(name.get()); + + LOGDRAGSERVICE(" got data, MIME %s", flavor.get()); + + if (len > 0 && data) { + mTargetDragDataLen = len; + mTargetDragData = g_malloc(mTargetDragDataLen); + memcpy(mTargetDragData, data, mTargetDragDataLen); + + LOGDRAGSERVICE(" got data, len = %d", mTargetDragDataLen); + + nsTArray<uint8_t> copy; + if (!copy.SetLength(len, fallible)) { + return; + } + memcpy(copy.Elements(), data, len); + + mCachedData.InsertOrUpdate(flavor, std::move(copy)); + } else { + LOGDRAGSERVICE("Failed to get data. selection data len was %d\n", + mTargetDragDataLen); + mCachedData.InsertOrUpdate(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); + } + + // 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); + GUniquePtr<gchar> name(gdk_atom_name(atom)); + if (name && !strcmp(name.get(), gMimeListType)) { + return true; + } + } + + return retval; +} + +// Spins event loop, called from eDragTaskMotion handler by +// DispatchMotionEvents(). +// Can lead to another round of drag_motion events. +void nsDragService::GetTargetDragData(GdkAtom aFlavor, + nsTArray<nsCString>& aDropFlavors) { + LOGDRAGSERVICE("nsDragService::GetTargetDragData(%p) '%s'\n", + mTargetDragContext.get(), + GUniquePtr<gchar>(gdk_atom_name(aFlavor)).get()); + + // reset our target data areas + TargetResetData(); + + GUniquePtr<gchar> name(gdk_atom_name(aFlavor)); + nsDependentCString flavor(name.get()); + + // Return early when requested MIME is not offered by D&D. + if (!aDropFlavors.Contains(flavor)) { + LOGDRAGSERVICE(" %s is missing", flavor.get()); + return; + } + + if (mTargetDragContext) { + // 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. + EnsureCachedDataValidForContext(mTargetDragContext); + if (auto cached = mCachedData.Lookup(flavor)) { + mTargetDragDataLen = cached->Length(); + LOGDRAGSERVICE(" using cached data for %s, length is %d", flavor.get(), + mTargetDragDataLen); + + if (mTargetDragDataLen) { + mTargetDragData = g_malloc(mTargetDragDataLen); + memcpy(mTargetDragData, cached->Elements(), mTargetDragDataLen); + } + + mTargetDragDataReceived = true; + LOGDRAGSERVICE(" %s found in cache", flavor.get()); + return; + } + + gtk_drag_get_data(mTargetWidget, mTargetDragContext, aFlavor, mTargetTime); + + LOGDRAGSERVICE(" about to start inner iteration."); + gtk_main_iteration(); + + PRTime entryTime = PR_Now(); + while (!mTargetDragDataReceived && mDoingDrag) { + // check the number of iterations + LOGDRAGSERVICE(" doing iteration...\n"); + PR_Sleep(PR_MillisecondsToInterval(10)); /* sleep for 10 ms/iteration */ + if (PR_Now() - entryTime > NS_DND_TIMEOUT) { + LOGDRAGSERVICE(" failed to get D&D data in time!\n"); + break; + } + gtk_main_iteration(); + } + } + +#ifdef MOZ_LOGGING + if (mTargetDragDataLen && mTargetDragData) { + LOGDRAGSERVICE(" %s got from system", flavor.get()); + } else { + LOGDRAGSERVICE(" %s failed to get from system", flavor.get()); + } +#endif +} + +void nsDragService::TargetResetData(void) { + mTargetDragDataReceived = false; + // make sure to free old data if we have to + g_free(mTargetDragData); + mTargetDragData = 0; + mTargetDragDataLen = 0; +} + +static void TargetArrayAddTarget(nsTArray<GtkTargetEntry*>& aTargetArray, + const char* aTarget) { + GtkTargetEntry* target = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); + target->target = g_strdup(aTarget); + target->flags = 0; + aTargetArray.AppendElement(target); + LOGDRAGSERVICESTATIC("adding target %s\n", aTarget); +} + +static bool CanExportAsURLTarget(const char16_t* aURLData, uint32_t aURLLen) { + for (const nsLiteralString& disallowed : kDisallowedExportedSchemes) { + auto len = disallowed.AsString().Length(); + if (len < aURLLen) { + if (!memcmp(disallowed.get(), aURLData, + /* len is char16_t char count */ len * 2)) { + LOGDRAGSERVICESTATIC("rejected URL scheme %s\n", + NS_ConvertUTF16toUTF8(disallowed).get()); + return false; + } + } + } + return true; +} + +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); + LOGDRAGSERVICE(" numDragItems = %d", 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. + TargetArrayAddTarget(targetArray, gMimeListType); + + // 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)) { + TargetArrayAddTarget(targetArray, gTextUriListType); + break; + } + } + } // 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]; + + TargetArrayAddTarget(targetArray, flavorStr.get()); + + // If there is a file, add the text/uri-list type. + if (flavorStr.EqualsLiteral(kFileMime)) { + TargetArrayAddTarget(targetArray, gTextUriListType); + } + // Check to see if this is text/plain. + else if (flavorStr.EqualsLiteral(kTextMime)) { + TargetArrayAddTarget(targetArray, gTextPlainUTF8Type); + } + // 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)) { + nsCOMPtr<nsISupports> data; + if (NS_SUCCEEDED(currItem->GetTransferData(flavorStr.get(), + getter_AddRefs(data)))) { + void* tmpData = nullptr; + uint32_t tmpDataLen = 0; + nsPrimitiveHelpers::CreateDataFromPrimitive( + nsDependentCString(flavorStr.get()), data, &tmpData, + &tmpDataLen); + if (tmpData) { + if (CanExportAsURLTarget(reinterpret_cast<char16_t*>(tmpData), + tmpDataLen / 2)) { + TargetArrayAddTarget(targetArray, gMozUrlType); + } + free(tmpData); + } + } + } + // check if application/x-moz-file-promise url is supported. + // If so, advertise text/uri-list. + else if (flavorStr.EqualsLiteral(kFilePromiseURLMime)) { + TargetArrayAddTarget(targetArray, gTextUriListType); + } + // XdndDirectSave, use on X.org only. + else if (widget::GdkIsX11Display() && !widget::IsXWaylandProtocol() && + flavorStr.EqualsLiteral(kFilePromiseMime)) { + TargetArrayAddTarget(targetArray, gXdndDirectSaveType); + } + // kNativeImageMime + else if (flavorStr.EqualsLiteral(kNativeImageMime)) { + TargetArrayAddTarget(targetArray, kPNGImageMime); + TargetArrayAddTarget(targetArray, kJPEGImageMime); + TargetArrayAddTarget(targetArray, kJPGImageMime); + TargetArrayAddTarget(targetArray, kGIFImageMime); + } + } + } + } + + // 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) { + LOGDRAGSERVICE("SourceEndDragSession(%p) result %s\n", aContext, + kGtkDragResults[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(); + GdkScreen* screen = gdk_display_get_default_screen(display); + GtkWindow* window = GetGtkWindow(mSourceDocument); + GdkWindow* gdkWindow = window ? gtk_widget_get_window(GTK_WIDGET(window)) + : gdk_screen_get_root_window(screen); + gdk_window_get_device_position( + gdkWindow, gdk_drag_context_get_device(aContext), &x, &y, nullptr); + gint scale = gdk_window_get_scale_factor(gdkWindow); + SetDragEndPoint(LayoutDeviceIntPoint(x * scale, y * scale)); + LOGDRAGSERVICE(" 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 == GTK_DRAG_RESULT_SUCCESS) { + LOGDRAGSERVICE(" drop is accepted"); + // 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) { + LOGDRAGSERVICE(" drop action is none"); + dropEffect = DRAGDROP_ACTION_NONE; + } else if (action & GDK_ACTION_COPY) { + LOGDRAGSERVICE(" drop action is copy"); + dropEffect = DRAGDROP_ACTION_COPY; + } else if (action & GDK_ACTION_LINK) { + LOGDRAGSERVICE(" drop action is link"); + dropEffect = DRAGDROP_ACTION_LINK; + } else if (action & GDK_ACTION_MOVE) { + LOGDRAGSERVICE(" drop action is move"); + dropEffect = DRAGDROP_ACTION_MOVE; + } else { + LOGDRAGSERVICE(" drop action is copy"); + dropEffect = DRAGDROP_ACTION_COPY; + } + } else { + LOGDRAGSERVICE(" drop action is none"); + dropEffect = DRAGDROP_ACTION_NONE; + if (aResult != GTK_DRAG_RESULT_NO_TARGET) { + LOGDRAGSERVICE(" drop is user chancelled\n"); + mUserCancelled = true; + } + } + + if (mDataTransfer) { + mDataTransfer->SetDropEffectInt(dropEffect); + } + + // Schedule the appropriate drag end dom events. + Schedule(eDragTaskSourceEnd, nullptr, nullptr, LayoutDeviceIntPoint(), 0); +} + +static nsresult GetDownloadDetails(nsITransferable* aTransferable, + nsIURI** aSourceURI, nsAString& aFilename) { + *aSourceURI = nullptr; + MOZ_ASSERT(aTransferable != nullptr, "aTransferable must not be null"); + + // get the URI from the kFilePromiseURLMime flavor + nsCOMPtr<nsISupports> urlPrimitive; + nsresult rv = aTransferable->GetTransferData(kFilePromiseURLMime, + getter_AddRefs(urlPrimitive)); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsISupportsString> srcUrlPrimitive = do_QueryInterface(urlPrimitive); + if (!srcUrlPrimitive) { + return NS_ERROR_FAILURE; + } + + nsAutoString srcUri; + srcUrlPrimitive->GetData(srcUri); + if (srcUri.IsEmpty()) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIURI> sourceURI; + NS_NewURI(getter_AddRefs(sourceURI), srcUri); + + nsAutoString srcFileName; + nsCOMPtr<nsISupports> fileNamePrimitive; + rv = aTransferable->GetTransferData(kFilePromiseDestFilename, + getter_AddRefs(fileNamePrimitive)); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsISupportsString> srcFileNamePrimitive = + do_QueryInterface(fileNamePrimitive); + if (srcFileNamePrimitive) { + srcFileNamePrimitive->GetData(srcFileName); + } else { + nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI); + if (!sourceURL) { + return NS_ERROR_FAILURE; + } + nsAutoCString urlFileName; + sourceURL->GetFileName(urlFileName); + NS_UnescapeURL(urlFileName); + CopyUTF8toUTF16(urlFileName, srcFileName); + } + if (srcFileName.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + sourceURI.swap(*aSourceURI); + aFilename = srcFileName; + return NS_OK; +} + +// See nsContentAreaDragDropDataProvider::GetFlavorData() for reference. +nsresult nsDragService::CreateTempFile(nsITransferable* aItem, + nsACString& aURI) { + LOGDRAGSERVICE("nsDragService::CreateTempFile()"); + + nsCOMPtr<nsIFile> tmpDir; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir)); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to get temp directory\n"); + return rv; + } + + nsCOMPtr<nsIInputStream> inputStream; + nsCOMPtr<nsIChannel> channel; + + // extract the file name and source uri of the promise-file data + nsAutoString wideFileName; + nsCOMPtr<nsIURI> sourceURI; + rv = GetDownloadDetails(aItem, getter_AddRefs(sourceURI), wideFileName); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE( + " Failed to extract file name and source uri from download url"); + return rv; + } + + // Check if the file is already stored at /tmp. + // It happens when drop destination is changed and SourceDataGet() is caled + // more than once. + nsAutoCString fileName; + CopyUTF16toUTF8(wideFileName, fileName); + auto fileLen = fileName.Length(); + for (const auto& url : mTempFileUrls) { + auto URLLen = url.Length(); + if (URLLen > fileLen && + fileName.Equals(nsDependentCString(url, URLLen - fileLen))) { + aURI = url; + LOGDRAGSERVICE(" recycle file %s", PromiseFlatCString(aURI).get()); + return NS_OK; + } + } + + // create and open channel for source uri + nsCOMPtr<nsIPrincipal> principal = aItem->GetRequestingPrincipal(); + nsContentPolicyType contentPolicyType = aItem->GetContentPolicyType(); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + aItem->GetCookieJarSettings(); + rv = NS_NewChannel(getter_AddRefs(channel), sourceURI, principal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType, cookieJarSettings); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to create new channel for source uri"); + return rv; + } + + rv = channel->Open(getter_AddRefs(inputStream)); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to open channel for source uri"); + return rv; + } + + // build the file:///tmp/dnd_file URL + tmpDir->Append(NS_LITERAL_STRING_FROM_CSTRING("dnd_file")); + rv = tmpDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed create tmp dir"); + return rv; + } + + // store a copy of that temporary directory so we can + // clean them up when nsDragService is destructed + nsCOMPtr<nsIFile> tempFile; + tmpDir->Clone(getter_AddRefs(tempFile)); + mTemporaryFiles.AppendObject(tempFile); + if (mTempFileTimerID) { + g_source_remove(mTempFileTimerID); + mTempFileTimerID = 0; + } + + // extend file:///tmp/dnd_file/<filename> URL + tmpDir->Append(wideFileName); + + nsCOMPtr<nsIOutputStream> outputStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), tmpDir); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to open output stream for temporary file"); + return rv; + } + + char buffer[8192]; + uint32_t readCount = 0; + uint32_t writeCount = 0; + while (1) { + rv = inputStream->Read(buffer, sizeof(buffer), &readCount); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to read data from source uri"); + return rv; + } + + if (readCount == 0) break; + + rv = outputStream->Write(buffer, readCount, &writeCount); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to write data to temporary file"); + return rv; + } + } + + inputStream->Close(); + rv = outputStream->Close(); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to write data to temporary file"); + return rv; + } + + nsCOMPtr<nsIURI> uri; + rv = NS_NewFileURI(getter_AddRefs(uri), tmpDir); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to get file URI"); + return rv; + } + nsCOMPtr<nsIURL> fileURL(do_QueryInterface(uri)); + if (!fileURL) { + LOGDRAGSERVICE(" Failed to query file interface"); + return NS_ERROR_FAILURE; + } + rv = fileURL->GetSpec(aURI); + if (NS_FAILED(rv)) { + LOGDRAGSERVICE(" Failed to get filepath"); + return rv; + } + + // store url of temporary file + mTempFileUrls.AppendElement()->Assign(aURI); + LOGDRAGSERVICE(" storing tmp file as %s", PromiseFlatCString(aURI).get()); + return NS_OK; +} + +bool nsDragService::SourceDataAppendURLFileItem(nsACString& aURI, + nsITransferable* aItem) { + // If there is a file available, create a URI from the file. + nsCOMPtr<nsISupports> data; + nsresult rv = aItem->GetTransferData(kFileMime, getter_AddRefs(data)); + NS_ENSURE_SUCCESS(rv, false); + if (nsCOMPtr<nsIFile> file = do_QueryInterface(data)) { + nsCOMPtr<nsIURI> fileURI; + NS_NewFileURI(getter_AddRefs(fileURI), file); + if (fileURI) { + fileURI->GetSpec(aURI); + return true; + } + } + return false; +} + +bool nsDragService::SourceDataAppendURLItem(nsITransferable* aItem, + bool aExternalDrop, + nsACString& aURI) { + nsCOMPtr<nsISupports> data; + nsresult rv = aItem->GetTransferData(kURLMime, getter_AddRefs(data)); + if (NS_FAILED(rv)) { + return SourceDataAppendURLFileItem(aURI, aItem); + } + + nsCOMPtr<nsISupportsString> string = do_QueryInterface(data); + if (!string) { + return false; + } + + nsAutoString text; + string->GetData(text); + if (!aExternalDrop || CanExportAsURLTarget(text.get(), text.Length())) { + AppendUTF16toUTF8(text, aURI); + return true; + } + + // We're dropping to another application and the URL can't be exported + // as it's internal one (mailbox:// etc.) + // Try to get file target directly. + if (SourceDataAppendURLFileItem(aURI, aItem)) { + return true; + } + + // We can't get the file directly so try to download it and save to tmp. + // The desktop or file manager expects for drags of promise-file data + // the text/uri-list flavor set to a temporary file that contains the + // promise-file data. + // We open a stream on the <protocol>:// url here and save the content + // to file:///tmp/dnd_file/<filename> and pass this url + // as text/uri-list flavor. + + // check whether transferable contains FilePromiseUrl flavor... + nsCOMPtr<nsISupports> promiseData; + rv = aItem->GetTransferData(kFilePromiseURLMime, getter_AddRefs(promiseData)); + NS_ENSURE_SUCCESS(rv, false); + + // ... if so, create a temporary file and pass its url + return NS_SUCCEEDED(CreateTempFile(aItem, aURI)); +} + +void nsDragService::SourceDataGetUriList(GdkDragContext* aContext, + GtkSelectionData* aSelectionData, + uint32_t aDragItems) { + // Check if we're transfering data to another application. + // gdk_drag_context_get_dest_window() on X11 returns GdkWindow even for + // different application so use nsWindow::GetWindow() to check if that's + // our window. + const bool isExternalDrop = + widget::GdkIsX11Display() + ? !nsWindow::GetWindow(gdk_drag_context_get_dest_window(aContext)) + : !gdk_drag_context_get_dest_window(aContext); + + LOGDRAGSERVICE("nsDragService::SourceDataGetUriLists() len %d external %d", + aDragItems, isExternalDrop); + + // Disable processing of native events until we store all files to /tmp. + // Otherwise user can quit drop before we have all files saved + // and that cancels whole D&D. + AutoSuspendNativeEvents suspend; + + nsAutoCString uriList; + for (uint32_t i = 0; i < aDragItems; i++) { + nsCOMPtr<nsITransferable> item = do_QueryElementAt(mSourceDataItems, i); + if (!item) { + continue; + } + nsAutoCString uri; + if (!SourceDataAppendURLItem(item, isExternalDrop, uri)) { + continue; + } + // text/x-moz-url is of form url + "\n" + title. + // We just want the url. + int32_t separatorPos = uri.FindChar(u'\n'); + if (separatorPos >= 0) { + uri.Truncate(separatorPos); + } + uriList.Append(uri); + uriList.AppendLiteral("\r\n"); + } + + LOGDRAGSERVICE("URI list\n%s", uriList.get()); + GdkAtom target = gtk_selection_data_get_target(aSelectionData); + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)uriList.get(), + uriList.Length()); +} + +void nsDragService::SourceDataGetImage(nsITransferable* aItem, + GtkSelectionData* aSelectionData) { + LOGDRAGSERVICE("nsDragService::SourceDataGetImage()"); + + nsresult rv; + nsCOMPtr<nsISupports> data; + rv = aItem->GetTransferData(kNativeImageMime, getter_AddRefs(data)); + NS_ENSURE_SUCCESS_VOID(rv); + + LOGDRAGSERVICE(" posting image\n"); + nsCOMPtr<imgIContainer> image = do_QueryInterface(data); + if (!image) { + LOGDRAGSERVICE(" do_QueryInterface failed\n"); + return; + } + RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(image); + if (!pixbuf) { + LOGDRAGSERVICE(" ImageToPixbuf failed\n"); + return; + } + gtk_selection_data_set_pixbuf(aSelectionData, pixbuf); + LOGDRAGSERVICE(" image data set\n"); + return; +} + +void nsDragService::SourceDataGetXDND(nsITransferable* aItem, + GdkDragContext* aContext, + GtkSelectionData* aSelectionData) { + LOGDRAGSERVICE("nsDragService::SourceDataGetXDND"); + + // Indicate failure by default. + GdkAtom target = gtk_selection_data_get_target(aSelectionData); + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"E", 1); + + GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE); + GdkAtom type = gdk_atom_intern(kTextMime, FALSE); + + GdkWindow* srcWindow = gdk_drag_context_get_source_window(aContext); + if (!srcWindow) { + LOGDRAGSERVICE(" failed to get source GdkWindow!"); + return; + } + + // Ensure null termination. + nsAutoCString data; + { + GUniquePtr<guchar> gdata; + gint length = 0; + if (!gdk_property_get(srcWindow, property, type, 0, INT32_MAX, FALSE, + nullptr, nullptr, &length, getter_Transfers(gdata))) { + LOGDRAGSERVICE(" failed to get gXdndDirectSaveType GdkWindow property."); + return; + } + data.Assign(nsDependentCSubstring((const char*)gdata.get(), length)); + } + + GUniquePtr<char> hostname; + GUniquePtr<char> fullpath( + g_filename_from_uri(data.get(), getter_Transfers(hostname), nullptr)); + if (!fullpath) { + LOGDRAGSERVICE(" failed to get file from uri."); + return; + } + + // 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.get())) { + LOGDRAGSERVICE(" ignored drag because of different host."); + // Special error code "F" for this case. + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"F", 1); + return; + } + } + } + + LOGDRAGSERVICE(" XdndDirectSave filepath is %s", fullpath.get()); + + nsCOMPtr<nsIFile> file; + if (NS_FAILED(NS_NewNativeLocalFile(nsDependentCString(fullpath.get()), false, + getter_AddRefs(file)))) { + LOGDRAGSERVICE(" failed to get local 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)); + + aItem->SetTransferData(kFilePromiseDirectoryMime, directory); + + nsCOMPtr<nsISupportsString> filenamePrimitive = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + if (!filenamePrimitive) { + return; + } + + nsAutoString leafName; + file->GetLeafName(leafName); + filenamePrimitive->SetData(leafName); + + aItem->SetTransferData(kFilePromiseDestFilename, filenamePrimitive); + + nsCOMPtr<nsISupports> promiseData; + nsresult rv = + aItem->GetTransferData(kFilePromiseMime, getter_AddRefs(promiseData)); + NS_ENSURE_SUCCESS_VOID(rv); + + // Indicate success. + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"S", 1); + return; +} + +bool nsDragService::SourceDataGetText(nsITransferable* aItem, + const nsACString& aMIMEType, + bool aNeedToDoConversionToPlainText, + GtkSelectionData* aSelectionData) { + LOGDRAGSERVICE("nsDragService::SourceDataGetPlain()"); + + nsresult rv; + nsCOMPtr<nsISupports> data; + rv = aItem->GetTransferData(PromiseFlatCString(aMIMEType).get(), + getter_AddRefs(data)); + NS_ENSURE_SUCCESS(rv, false); + + void* tmpData = nullptr; + uint32_t tmpDataLen = 0; + + nsPrimitiveHelpers::CreateDataFromPrimitive(aMIMEType, 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 (aNeedToDoConversionToPlainText) { + 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 + GdkAtom target = gtk_selection_data_get_target(aSelectionData); + gtk_selection_data_set(aSelectionData, target, 8, (guchar*)tmpData, + tmpDataLen); + // this wasn't allocated with glib + free(tmpData); + } + + return true; +} + +// We're asked to get data from mSourceDataItems and pass it to +// GtkSelectionData (Gtk D&D interface). +// We need to check mSourceDataItems data type and try to convert it +// to data type accepted by Gtk. +void nsDragService::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext, + GtkSelectionData* aSelectionData, + guint32 aTime) { + LOGDRAGSERVICE("nsDragService::SourceDataGet(%p)", aContext); + + GdkAtom target = gtk_selection_data_get_target(aSelectionData); + GUniquePtr<gchar> requestedTypeName(gdk_atom_name(target)); + if (!requestedTypeName) { + LOGDRAGSERVICE(" failed to get atom name.\n"); + return; + } + + LOGDRAGSERVICE(" Gtk asks us for %s data type\n", requestedTypeName.get()); + // check to make sure that we have data items to return. + if (!mSourceDataItems) { + LOGDRAGSERVICE(" Failed to get our data items\n"); + return; + } + + uint32_t dragItems; + mSourceDataItems->GetLength(&dragItems); + LOGDRAGSERVICE(" source data items %d", dragItems); + + nsDependentCString mimeFlavor(requestedTypeName.get()); + if (mimeFlavor.EqualsLiteral(gTextUriListType)) { + SourceDataGetUriList(aContext, aSelectionData, dragItems); + return; + } + +#ifdef MOZ_LOGGING + if (dragItems > 1) { + LOGDRAGSERVICE( + " There are %d data items but we're asked for %s MIME type. Only " + "first data element can be transfered!", + dragItems, mimeFlavor.get()); + } +#endif + + nsCOMPtr<nsITransferable> item = do_QueryElementAt(mSourceDataItems, 0); + if (!item) { + LOGDRAGSERVICE(" Failed to get SourceDataItems!"); + return; + } + + if (mimeFlavor.EqualsLiteral(kTextMime) || + mimeFlavor.EqualsLiteral(gTextPlainUTF8Type)) { + SourceDataGetText(item, nsDependentCString(kTextMime), + /* aNeedToDoConversionToPlainText */ true, + aSelectionData); + // no fallback for text mime types + return; + } + // Someone is asking for the special Direct Save Protocol type. + else if (mimeFlavor.EqualsLiteral(gXdndDirectSaveType)) { + SourceDataGetXDND(item, aContext, aSelectionData); + // no fallback for XDND mime types + return; + } else if (mimeFlavor.EqualsLiteral(kPNGImageMime) || + mimeFlavor.EqualsLiteral(kJPEGImageMime) || + mimeFlavor.EqualsLiteral(kJPGImageMime) || + mimeFlavor.EqualsLiteral(kGIFImageMime)) { + // no fallback for image mime types + SourceDataGetImage(item, aSelectionData); + return; + } else if (mimeFlavor.EqualsLiteral(gMozUrlType)) { + // Someone was asking for _NETSCAPE_URL. We need to get it from + // transferable as x-moz-url and convert it to plain text. + // No need to check + if (SourceDataGetText(item, nsDependentCString(kURLMime), + /* aNeedToDoConversionToPlainText */ true, + aSelectionData)) { + return; + } + } + // Just try to get and set whatever we're asked for. + SourceDataGetText(item, mimeFlavor, + /* aNeedToDoConversionToPlainText */ false, aSelectionData); +} + +void nsDragService::SourceBeginDrag(GdkDragContext* aContext) { + LOGDRAGSERVICE("nsDragService::SourceBeginDrag(%p)\n", 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)) { + LOGDRAGSERVICE(" transferable doesn't contain '%s", + kFilePromiseDestFilename); + return; + } + + nsCOMPtr<nsISupportsString> fileName = do_QueryInterface(data); + if (!fileName) { + LOGDRAGSERVICE(" failed to get file name"); + 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()); + break; + } + } +} + +void nsDragService::SetDragIcon(GdkDragContext* aContext) { + if (!mHasImage && !mSelection) return; + + LOGDRAGSERVICE("nsDragService::SetDragIcon(%p)", aContext); + + LayoutDeviceIntRect dragRect; + nsPresContext* pc; + RefPtr<SourceSurface> surface; + DrawDrag(mSourceNode, mRegion, mScreenPosition, &dragRect, &surface, &pc); + if (!pc) { + LOGDRAGSERVICE(" PresContext is missing!"); + return; + } + + const auto screenPoint = + LayoutDeviceIntPoint::Round(mScreenPosition * pc->CSSToDevPixelScale()); + 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. + // This is fixed in GTK 3.24 + // by + // https://gitlab.gnome.org/GNOME/gtk/-/commit/c27c4e2048acb630feb24c31288f802345e99f4c + bool gtk_drag_set_icon_widget_is_working = + gtk_check_version(3, 19, 4) != nullptr || + gtk_check_version(3, 24, 0) == nullptr; + if (mDragPopup && gtk_drag_set_icon_widget_is_working) { + 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) { + // When mDragPopup has a parent it's already attached to D&D context. + // That may happens when D&D operation is aborted but not finished + // on Gtk side yet so let's remove it now. + if (GtkWidget* parent = gtk_widget_get_parent(gtkWidget)) { + gtk_container_remove(GTK_CONTAINER(parent), gtkWidget); + } + LOGDRAGSERVICE(" set drag popup [%p]", widget.get()); + OpenDragPopup(); + gtk_drag_set_icon_widget(aContext, gtkWidget, offsetX, offsetY); + return; + } else { + LOGDRAGSERVICE(" NS_NATIVE_SHELLWIDGET is missing!"); + } + } else { + LOGDRAGSERVICE(" NearestWidget is missing!"); + } + } else { + LOGDRAGSERVICE(" PrimaryFrame is missing!"); + } + } + + if (surface) { + LOGDRAGSERVICE(" We have a surface"); + if (!SetAlphaPixmap(surface, aContext, offsetX, offsetY, dragRect)) { + RefPtr<GdkPixbuf> dragPixbuf = nsImageToPixbuf::SourceSurfaceToPixbuf( + surface, dragRect.width, dragRect.height); + if (dragPixbuf) { + LOGDRAGSERVICE(" set drag pixbuf"); + gtk_drag_set_icon_pixbuf(aContext, dragPixbuf, offsetX, offsetY); + } else { + LOGDRAGSERVICE(" SourceSurfaceToPixbuf failed!"); + } + } + } else { + LOGDRAGSERVICE(" Surface is missing!"); + } +} + +static void invisibleSourceDragBegin(GtkWidget* aWidget, + GdkDragContext* aContext, gpointer aData) { + LOGDRAGSERVICESTATIC("invisibleSourceDragBegin (%p)", aContext); + 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) { + LOGDRAGSERVICESTATIC("invisibleSourceDragDataGet (%p)", aContext); + nsDragService* dragService = (nsDragService*)aData; + dragService->SourceDataGet(aWidget, aContext, aSelectionData, aTime); +} + +static gboolean invisibleSourceDragFailed(GtkWidget* aWidget, + GdkDragContext* aContext, + gint aResult, gpointer aData) { +#ifdef MOZ_WAYLAND + // Wayland and X11 uses different drag results here. When drag target is + // missing X11 passes GDK_DRAG_CANCEL_NO_TARGET + // (from gdk_dnd_handle_button_event()/gdkdnd-x11.c) + // as backend X11 has info about other application windows. + // Wayland does not have such info so it always passes + // GDK_DRAG_CANCEL_ERROR error code + // (see data_source_cancelled/gdkselection-wayland.c). + // Bug 1527976 + if (widget::GdkIsWaylandDisplay() && aResult == 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); + GUniquePtr<gchar> name(gdk_atom_name(atom)); + if (name && !strcmp(name.get(), gTabDropType)) { + aResult = GTK_DRAG_RESULT_NO_TARGET; + LOGDRAGSERVICESTATIC("invisibleSourceDragFailed(%p): Wayland tab drop", + aContext); + break; + } + } + } +#endif + LOGDRAGSERVICESTATIC("invisibleSourceDragFailed(%p) %s", aContext, + kGtkDragResults[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) { + LOGDRAGSERVICESTATIC("invisibleSourceDragEnd(%p)", aContext); + nsDragService* dragService = (nsDragService*)aData; + + // The drag has ended. Release the hostages! + dragService->SourceEndDragSession(aContext, 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, + 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, 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, LayoutDeviceIntPoint(), 0)) { + NS_WARNING("Drag leave after drop"); + } +} + +gboolean nsDragService::ScheduleDropEvent(nsWindow* aWindow, + GdkDragContext* aDragContext, + LayoutDeviceIntPoint aWindowPoint, + guint aTime) { + if (!Schedule(eDragTaskDrop, aWindow, aDragContext, aWindowPoint, aTime)) { + NS_WARNING("Additional drag drop ignored"); + return FALSE; + } + + SetDragEndPoint(aWindowPoint); + + // We'll reply with gtk_drag_finish(). + return TRUE; +} + +#ifdef MOZ_LOGGING +const char* nsDragService::GetDragServiceTaskName(DragTask aTask) { + static const char* taskNames[] = {"eDragTaskNone", "eDragTaskMotion", + "eDragTaskLeave", "eDragTaskDrop", + "eDragTaskSourceEnd"}; + MOZ_ASSERT(size_t(aTask) < ArrayLength(taskNames)); + return taskNames[aTask]; +} +#endif + +gboolean nsDragService::Schedule(DragTask aTask, nsWindow* aWindow, + GdkDragContext* aDragContext, + 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. + LOGDRAGSERVICE("nsDragService::Schedule(%p) task %s window %p\n", + aDragContext, GetDragServiceTaskName(aTask), aWindow); + + if (mScheduledTask == eDragTaskSourceEnd || + (mScheduledTask == eDragTaskDrop && aTask != eDragTaskSourceEnd)) { + LOGDRAGSERVICE(" task does not fit recent task %s, quit!\n", + GetDragServiceTaskName(mScheduledTask)); + return FALSE; + } + + mScheduledTask = aTask; + mPendingWindow = aWindow; + mPendingDragContext = aDragContext; + mPendingWindowPoint = aWindowPoint; + mPendingTime = aTime; + + if (!mTaskSource) { + // High priority is used here because we want to process motion events + // right after drag_motion event handler which is called by Gtk. + // An ideal scenario is to call TaskDispatchCallback() directly here + // but we can't do that. TaskDispatchCallback() spins gtk event loop + // while nsDragService::Schedule() is already called from event loop + // (by drag_motion* gtk_widget events) so that direct call will cause + // nested recursion. + mTaskSource = g_timeout_add_full(G_PRIORITY_HIGH, 0, TaskDispatchCallback, + this, nullptr); + } + + // We need to reply to drag_motion event on Wayland immediately, + // see Bug 1730203. + if (widget::GdkIsWaylandDisplay() && mScheduledTask == eDragTaskMotion) { + UpdateDragAction(aDragContext); + ReplyToDragMotion(aDragContext, aTime); + } + + return TRUE; +} + +gboolean nsDragService::TaskDispatchCallback(gpointer data) { + RefPtr<nsDragService> dragService = static_cast<nsDragService*>(data); + AutoEventLoop loop(dragService); + return dragService->RunScheduledTask(); +} + +gboolean nsDragService::RunScheduledTask() { + LOGDRAGSERVICE( + "nsDragService::RunScheduledTask() task %s mTargetWindow %p " + "mPendingWindow %p\n", + GetDragServiceTaskName(mScheduledTask), mTargetWindow.get(), + mPendingWindow.get()); + + // Don't run RunScheduledTask() twice. As we use it in main thread only + // we don't need to be thread safe here. + if (mScheduledTaskIsRunning) { + LOGDRAGSERVICE(" sheduled task is already running, quit."); + return FALSE; + } + AutoRestore<bool> guard(mScheduledTaskIsRunning); + mScheduledTaskIsRunning = true; + + if (mTargetWindow && mTargetWindow != mPendingWindow) { + LOGDRAGSERVICE(" dispatch eDragExit (%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) { + LOGDRAGSERVICE(" quit, selected task %s\n", GetDragServiceTaskName(task)); + 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 ? mTargetWindow->GetGtkWidget() : nullptr; + LOGDRAGSERVICE(" start drag session mTargetWindow %p mTargetWidget %p\n", + mTargetWindow.get(), mTargetWidget.get()); + LOGDRAGSERVICE(" mPendingDragContext %p => mTargetDragContext %p\n", + mPendingDragContext.get(), mTargetDragContext.get()); + mTargetDragContext = std::move(mPendingDragContext); + mTargetTime = mPendingTime; + + // 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) { + LOGDRAGSERVICE(" process motion event\n"); + UpdateDragAction(); + TakeDragEventDispatchedToChildProcess(); // Clear the old value. + DispatchMotionEvents(); + if (task == eDragTaskMotion) { + if (TakeDragEventDispatchedToChildProcess()) { + mTargetDragContextForRemote = mTargetDragContext; + } else { + // Reply to tell the source whether we can drop and what + // action would be taken. + ReplyToDragMotion(); + } + } + } + + if (task == eDragTaskDrop) { + LOGDRAGSERVICE(" process drop task\n"); + 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) { + LOGDRAGSERVICE(" drag finished\n"); + gtk_drag_finish(mTargetDragContext, success, + /* del = */ FALSE, mTargetTime); + } + // 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. + LOGDRAGSERVICE(" clear mTargetWindow mTargetWidget and other data\n"); + mTargetWidget = nullptr; + mTargetDragContext = nullptr; + + // 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. + LOGDRAGSERVICE(" remove task source\n"); + 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(GdkDragContext* aDragContext) { + // 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. + LOGDRAGSERVICE("nsDragService::UpdateDragAction(%p)", aDragContext); + + // default is to do nothing + int action = nsIDragService::DRAGDROP_ACTION_NONE; + GdkDragAction gdkAction = GDK_ACTION_DEFAULT; + if (aDragContext) { + gdkAction = gdk_drag_context_get_actions(aDragContext); + LOGDRAGSERVICE(" gdk_drag_context_get_actions() returns 0x%X", gdkAction); + + // When D&D modifiers (CTRL/SHIFT) are involved, + // gdk_drag_context_get_actions() on X11 returns selected action but + // Wayland returns all allowed actions. + + // So we need to call gdk_drag_context_get_selected_action() on Wayland + // to get potential D&D modifier. + // gdk_drag_context_get_selected_action() is also affected by + // gdk_drag_status(), see nsDragService::ReplyToDragMotion(). + if (widget::GdkIsWaylandDisplay()) { + GdkDragAction gdkActionSelected = + gdk_drag_context_get_selected_action(aDragContext); + LOGDRAGSERVICE(" gdk_drag_context_get_selected_action() returns 0x%X", + gdkActionSelected); + if (gdkActionSelected) { + gdkAction = gdkActionSelected; + } + } + } + + // set the default just in case nothing matches below + if (gdkAction & GDK_ACTION_DEFAULT) { + LOGDRAGSERVICE(" set default move"); + action = nsIDragService::DRAGDROP_ACTION_MOVE; + } + // first check to see if move is set + if (gdkAction & GDK_ACTION_MOVE) { + LOGDRAGSERVICE(" set explicit move"); + action = nsIDragService::DRAGDROP_ACTION_MOVE; + } else if (gdkAction & GDK_ACTION_LINK) { + // then fall to the others + LOGDRAGSERVICE(" set explicit link"); + action = nsIDragService::DRAGDROP_ACTION_LINK; + } else if (gdkAction & GDK_ACTION_COPY) { + // copy is ctrl + LOGDRAGSERVICE(" set explicit copy"); + action = nsIDragService::DRAGDROP_ACTION_COPY; + } + + // update the drag information + SetDragAction(action); +} + +void nsDragService::UpdateDragAction() { UpdateDragAction(mTargetDragContext); } + +NS_IMETHODIMP +nsDragService::UpdateDragEffect() { + LOGDRAGSERVICE("nsDragService::UpdateDragEffect() from e10s child process"); + if (mTargetDragContextForRemote) { + ReplyToDragMotion(mTargetDragContextForRemote, mTargetTime); + mTargetDragContextForRemote = nullptr; + } + return NS_OK; +} + +void nsDragService::ReplyToDragMotion() { + if (mTargetDragContext) { + ReplyToDragMotion(mTargetDragContext, mTargetTime); + } +} + +void nsDragService::DispatchMotionEvents() { + FireDragEventAtSource(eDrag, GetCurrentModifiers()); + if (mTargetWindow) { + 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 || 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(); +} + +#undef LOGDRAGSERVICE diff --git a/widget/gtk/nsDragService.h b/widget/gtk/nsDragService.h new file mode 100644 index 0000000000..36696a75e2 --- /dev/null +++ b/widget/gtk/nsDragService.h @@ -0,0 +1,264 @@ +/* -*- 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 "nsCOMArray.h" +#include "nsIObserver.h" +#include <gtk/gtk.h> +#include "nsITimer.h" + +class nsICookieJarSettings; +class nsWindow; + +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; + + // Update Drag&Drop state according child process state. + // UpdateDragEffect() is called by IPC bridge when child process + // accepts/denies D&D operation and uses stored + // mTargetDragContextForRemote context. + 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, + mozilla::LayoutDeviceIntPoint aWindowPoint, + guint aTime); + void ScheduleLeaveEvent(); + gboolean ScheduleDropEvent(nsWindow* aWindow, GdkDragContext* aDragContext, + 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); + bool SourceDataGetText(nsITransferable* aItem, const nsACString& aMIMEType, + bool aNeedToDoConversionToPlainText, + GtkSelectionData* aSelectionData); + void SourceDataGetImage(nsITransferable* aItem, + GtkSelectionData* aSelectionData); + void SourceDataGetXDND(nsITransferable* aItem, GdkDragContext* aContext, + GtkSelectionData* aSelectionData); + void SourceDataGetUriList(GdkDragContext* aContext, + GtkSelectionData* aSelectionData, + uint32_t aDragItems); + bool SourceDataAppendURLFileItem(nsACString& aURI, nsITransferable* aItem); + bool SourceDataAppendURLItem(nsITransferable* aItem, bool aExternalDrop, + nsACString& aURI); + + void SourceBeginDrag(GdkDragContext* aContext); + + // set the drag icon during drag-begin + void SetDragIcon(GdkDragContext* aContext); + + class AutoEventLoop { + RefPtr<nsDragService> mService; + + public: + explicit AutoEventLoop(RefPtr<nsDragService> aService) + : mService(std::move(aService)) { + mService->mEventLoopDepth++; + } + ~AutoEventLoop() { mService->mEventLoopDepth--; } + }; + int GetLoopDepth() const { return mEventLoopDepth; }; + + 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; + bool mScheduledTaskIsRunning; + + // Where the drag begins. We need to keep it open on Wayland. + RefPtr<nsWindow> mSourceWindow; + + // 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. + nsTHashMap<nsCStringHashKey, nsTArray<uint8_t>> mCachedData; + // mCachedData are tied to mCachedDragContext. mCachedDragContext is not + // ref counted and may be already deleted on Gtk side. + // We used it for mCachedData invalidation only and can't be used for + // any D&D operation. + uintptr_t mCachedDragContext; + + 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; + + // When we route D'n'D request to child process + // (by EventStateManager::DispatchCrossProcessEvent) + // we save GdkDragContext to mTargetDragContextForRemote. + // When we get a reply from child process we use + // the stored GdkDragContext to send reply to OS. + // + // We need to store GdkDragContext because mTargetDragContext is cleared + // after every D'n'D event. + RefPtr<GdkDragContext> mTargetDragContextForRemote; + 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, nsTArray<nsCString>& aDropFlavors); + // this will reset all of the target vars + void TargetResetData(void); + // Ensure our data cache belongs to aDragContext and clear the cache if + // aDragContext is different than mCachedDragContext. + void EnsureCachedDataValidForContext(GdkDragContext* aDragContext); + + // 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, + 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(); + MOZ_CAN_RUN_SCRIPT void DispatchMotionEvents(); + void ReplyToDragMotion(GdkDragContext* aDragContext, guint aTime); + void ReplyToDragMotion(); + void UpdateDragAction(GdkDragContext* aDragContext); + void UpdateDragAction(); + +#ifdef MOZ_LOGGING + const char* GetDragServiceTaskName(nsDragService::DragTask aTask); +#endif + void GetDragFlavors(nsTArray<nsCString>& aFlavors); + gboolean DispatchDropEvent(); + static uint32_t GetCurrentModifiers(); + + nsresult CreateTempFile(nsITransferable* aItem, nsACString& aURI); + bool RemoveTempFiles(); + static gboolean TaskRemoveTempFiles(gpointer data); + + // the url of the temporary file that has been created in the current drag + // session + nsTArray<nsCString> mTempFileUrls; + // stores all temporary files + nsCOMArray<nsIFile> mTemporaryFiles; + // timer to trigger deletion of temporary files + guint mTempFileTimerID; + // How deep we're nested in event loops + int mEventLoopDepth; +}; + +#endif // nsDragService_h__ diff --git a/widget/gtk/nsFilePicker.cpp b/widget/gtk/nsFilePicker.cpp new file mode 100644 index 0000000000..22d0f46b95 --- /dev/null +++ b/widget/gtk/nsFilePicker.cpp @@ -0,0 +1,706 @@ +/* -*- 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 "nsIStringBundle.h" +#include "mozilla/Components.h" +#include "mozilla/Preferences.h" + +#include "nsArrayEnumerator.h" +#include "nsEnumeratorUtils.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "MozContainer.h" +#include "WidgetUtilsGtk.h" + +#include "nsFilePicker.h" + +#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 */ + +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(nsIFilePicker::Mode 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) { + mUseNativeFileChooser = + widget::ShouldUsePortal(widget::PortalKind::FilePicker); +} + +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(nsIFilePicker::ResultCode* 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; + + default: + /* no additional setup needed */ + 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); +} + +bool nsFilePicker::WarnForNonReadableFile(void* file_chooser) { + nsCOMPtr<nsIFile> file; + GetFile(getter_AddRefs(file)); + if (!file) { + return false; + } + + bool isReadable = false; + file->IsReadable(&isReadable); + if (isReadable) { + return false; + } + + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (!stringService) { + return false; + } + + nsCOMPtr<nsIStringBundle> filepickerBundle; + nsresult rv = stringService->CreateBundle( + "chrome://global/locale/filepicker.properties", + getter_AddRefs(filepickerBundle)); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoString errorMessage; + rv = filepickerBundle->GetStringFromName("selectedFileNotReadableError", + errorMessage); + if (NS_FAILED(rv)) { + return false; + } + + GtkDialogFlags flags = GTK_DIALOG_DESTROY_WITH_PARENT; + auto* cancel_dialog = gtk_message_dialog_new( + GTK_WINDOW(file_chooser), flags, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + "%s", NS_ConvertUTF16toUTF8(errorMessage).get()); + gtk_dialog_run(GTK_DIALOG(cancel_dialog)); + gtk_widget_destroy(cancel_dialog); + + return true; +} + +void nsFilePicker::Done(void* file_chooser, gint response) { + mRunning = false; + + nsIFilePicker::ResultCode 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; + } + } else if (mMode == nsIFilePicker::modeOpen) { + if (WarnForNonReadableFile(file_chooser)) { + result = nsIFilePicker::returnCancel; + } + } + 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 && *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); + } + } +} + +#undef LOG diff --git a/widget/gtk/nsFilePicker.h b/widget/gtk/nsFilePicker.h new file mode 100644 index 0000000000..496df49372 --- /dev/null +++ b/widget/gtk/nsFilePicker.h @@ -0,0 +1,89 @@ +/* -*- 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(nsIFilePicker::ResultCode* aReturn) override; + void ReadValuesFromFileChooser(void* file_chooser); + + bool WarnForNonReadableFile(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; + nsIFilePicker::ResultCode 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..5129ba419d --- /dev/null +++ b/widget/gtk/nsGTKToolkit.h @@ -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/. */ + +#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 final { + public: + nsGTKToolkit() = default; + + static nsGTKToolkit* GetToolkit(); + static void Shutdown() { + delete gToolkit; + gToolkit = nullptr; + } + + /** + * Get/set our startup token value. (XDG_ACTIVATION_TOKEN/DESKTOP_STARTUP_ID) + * When non-empty, this is applied to the next toplevel window to be shown or + * focused (and then immediately cleared). + */ + void SetStartupToken(const nsACString& aToken) { mStartupToken = aToken; } + const nsCString& GetStartupToken() const { return mStartupToken; } + + /** + * 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() const { return mFocusTimestamp; } + + private: + static nsGTKToolkit* gToolkit; + + nsCString mStartupToken; + uint32_t mFocusTimestamp = 0; +}; + +#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..4a83ea6ec0 --- /dev/null +++ b/widget/gtk/nsGtkKeyUtils.cpp @@ -0,0 +1,2491 @@ +/* -*- 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 <dlfcn.h> +#include <gdk/gdkkeysyms-compat.h> +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +# include <X11/XKBlib.h> +# include "X11UndefineNone.h" +#endif +#include "IMContextWrapper.h" +#include "WidgetUtils.h" +#include "WidgetUtilsGtk.h" +#include "x11/keysym2ucs.h" +#include "nsContentUtils.h" +#include "nsGtkUtils.h" +#include "nsIBidiKeyboard.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsWindow.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 + +// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync` +// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too +// big file. +// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. +mozilla::LazyLogModule gKeyLog("KeyboardHandler"); + +namespace mozilla { +namespace widget { + +#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; +#ifdef MOZ_X11 +Time KeymapWrapper::sLastRepeatableKeyTime = 0; +#endif +KeymapWrapper::RepeatState KeymapWrapper::sRepeatState = + KeymapWrapper::NOT_PRESSED; + +#ifdef MOZ_WAYLAND +wl_seat* KeymapWrapper::sSeat = nullptr; +int KeymapWrapper::sSeatID = -1; +wl_keyboard* KeymapWrapper::sKeyboard = nullptr; +#endif + +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 = new KeymapWrapper(); + sInstance->Init(); + } + return sInstance; +} + +#ifdef MOZ_WAYLAND +void KeymapWrapper::EnsureInstance() { (void)GetInstance(); } + +void KeymapWrapper::InitBySystemSettingsWayland() { + MOZ_UNUSED(WaylandDisplayGet()); +} +#endif + +/* 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(gKeyLog, LogLevel::Info, + ("%p Constructor, mGdkKeymap=%p", this, mGdkKeymap)); + + g_object_ref(mGdkKeymap); + +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + InitXKBExtension(); + } +#endif +} + +void KeymapWrapper::Init() { + if (mInitialized) { + return; + } + mInitialized = true; + + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p Init, mGdkKeymap=%p", this, mGdkKeymap)); + + mModifierKeys.Clear(); + memset(mModifierMasks, 0, sizeof(mModifierMasks)); + +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + InitBySystemSettingsX11(); + } +#endif +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + InitBySystemSettingsWayland(); + } +#endif + +#ifdef MOZ_X11 + gdk_window_add_filter(nullptr, FilterEvents, this); +#endif + + MOZ_LOG(gKeyLog, 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))); +} + +#ifdef MOZ_X11 +void KeymapWrapper::InitXKBExtension() { + PodZero(&mKeyboardState); + + int xkbMajorVer = XkbMajorVersion; + int xkbMinorVer = XkbMinorVersion; + if (!XkbLibraryVersion(&xkbMajorVer, &xkbMinorVer)) { + MOZ_LOG(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XkbSelectEventDetails() for XkbControlsNotify, display=0x%p", + this, display)); + return; + } + + if (!XGetKeyboardControl(display, &mKeyboardState)) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p InitXKBExtension failed due to failure of " + "XGetKeyboardControl(), display=0x%p", + this, display)); + return; + } + + MOZ_LOG(gKeyLog, LogLevel::Info, ("%p InitXKBExtension, Succeeded", this)); +} + +void KeymapWrapper::InitBySystemSettingsX11() { + MOZ_LOG(gKeyLog, 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(gKeyLog, LogLevel::Info, + ("%p InitBySystemSettings, " + "Failed due to null xkeymap", + this)); + return; + } + + XModifierKeymap* xmodmap = XGetModifierMapping(display); + if (!xmodmap) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p InitBySystemSettings, " + "Failed due to null xmodmap", + this)); + XFree(xkeymap); + return; + } + MOZ_LOG(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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); +} +#endif + +#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(gKeyLog, 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) { + KeymapWrapper::SetFocusIn(surface, serial); +} + +static void keyboard_handle_leave(void* data, struct wl_keyboard* keyboard, + uint32_t serial, struct wl_surface* surface) { + KeymapWrapper::SetFocusOut(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) { + wl_keyboard* keyboard = KeymapWrapper::GetKeyboard(); + if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !keyboard) { + keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr); + KeymapWrapper::SetKeyboard(keyboard); + } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && keyboard) { + KeymapWrapper::ClearKeyboard(); + } +} + +static const struct wl_seat_listener seat_listener = { + seat_handle_capabilities, +}; + +#endif + +KeymapWrapper::~KeymapWrapper() { +#ifdef MOZ_X11 + gdk_window_remove_filter(nullptr, FilterEvents, this); +#endif + if (mOnKeysChangedSignalHandle) { + g_signal_handler_disconnect(mGdkKeymap, mOnKeysChangedSignalHandle); + } + if (mOnDirectionChangedSignalHandle) { + g_signal_handler_disconnect(mGdkKeymap, mOnDirectionChangedSignalHandle); + } + g_object_unref(mGdkKeymap); + MOZ_LOG(gKeyLog, LogLevel::Info, ("%p Destructor", this)); +} + +#ifdef MOZ_X11 +/* 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, LogLevel::Info, + ("%p FilterEvents failed due to failure " + "of XGetKeyboardControl(), display=0x%p", + self, xkbEvent->any.display)); + } + break; + } + } + + return GDK_FILTER_CONTINUE; +} +#endif + +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() { + if (sInstance) { + sInstance->mInitialized = false; + ResetBidiKeyboard(); + } +} + +/* static */ +void KeymapWrapper::OnKeysChanged(GdkKeymap* aGdkKeymap, + KeymapWrapper* aKeymapWrapper) { + MOZ_LOG(gKeyLog, 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(gKeyLog, LogLevel::Info, + ("OnDirectionChanged, aGdkKeymap=%p, aKeymapWrapper=%p", aGdkKeymap, + aKeymapWrapper)); + + ResetBidiKeyboard(); +} + +/* static */ +guint KeymapWrapper::GetCurrentModifierState() { + GdkModifierType modifiers; + GdkDisplay* display = gdk_display_get_default(); + GdkScreen* screen = gdk_display_get_default_screen(display); + GdkWindow* window = gdk_screen_get_root_window(screen); + gdk_window_get_device_position(window, GdkGetPointer(), 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 */ +guint KeymapWrapper::ConvertWidgetModifierToGdkState( + nsIWidget::Modifiers aNativeModifiers) { + if (!aNativeModifiers) { + return 0; + } + struct ModifierMapEntry { + nsIWidget::Modifiers mWidgetModifier; + Modifier mModifier; + }; + // TODO: Currently, we don't treat L/R of each modifier on Linux. + // TODO: No proper native modifier for Level5. + static constexpr ModifierMapEntry sModifierMap[] = { + {nsIWidget::CAPS_LOCK, Modifier::CAPS_LOCK}, + {nsIWidget::NUM_LOCK, Modifier::NUM_LOCK}, + {nsIWidget::SHIFT_L, Modifier::SHIFT}, + {nsIWidget::SHIFT_R, Modifier::SHIFT}, + {nsIWidget::CTRL_L, Modifier::CTRL}, + {nsIWidget::CTRL_R, Modifier::CTRL}, + {nsIWidget::ALT_L, Modifier::ALT}, + {nsIWidget::ALT_R, Modifier::ALT}, + {nsIWidget::ALTGRAPH, Modifier::LEVEL3}, + {nsIWidget::COMMAND_L, Modifier::SUPER}, + {nsIWidget::COMMAND_R, Modifier::SUPER}}; + + guint state = 0; + KeymapWrapper* instance = GetInstance(); + for (const ModifierMapEntry& entry : sModifierMap) { + if (aNativeModifiers & entry.mWidgetModifier) { + state |= instance->GetModifierMask(entry.mModifier); + } + } + return state; +} + +/* 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(gKeyLog, 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( + gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Back\" command event")); + return; + case GDK_Forward: + aWindow->DispatchCommandEvent(nsGkAtoms::Forward); + MOZ_LOG(gKeyLog, 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(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Stop\" command event")); + return; + case GDK_Search: + aWindow->DispatchCommandEvent(nsGkAtoms::Search); + MOZ_LOG(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Search\" command event")); + return; + case GDK_Favorites: + aWindow->DispatchCommandEvent(nsGkAtoms::Bookmarks); + MOZ_LOG(gKeyLog, 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(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Copy\" content command " + "event")); + return; + case GDK_Cut: + case GDK_F20: + aWindow->DispatchContentCommandEvent(eContentCommandCut); + MOZ_LOG(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched \"Cut\" content command " + "event")); + return; + case GDK_Paste: + case GDK_F18: + aWindow->DispatchContentCommandEvent(eContentCommandPaste); + MOZ_LOG(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched eKeyPress event " + "(status=%s)", + GetStatusName(status))); + } else { + MOZ_LOG(gKeyLog, 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(gKeyLog, LogLevel::Info, + (" HandleKeyPressEvent(), dispatched a set of composition " + "events")); + } +} + +/* static */ +bool KeymapWrapper::HandleKeyReleaseEvent(nsWindow* aWindow, + GdkEventKey* aGdkKeyEvent) { + MOZ_LOG(gKeyLog, 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(gKeyLog, 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(gKeyLog, LogLevel::Error, + (" HandleKeyReleaseEvent(), didn't dispatch eKeyUp event")); + return false; + } + + MOZ_LOG(gKeyLog, 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(); +#ifdef MOZ_X11 + if (aGdkKeyEvent->is_modifier && GdkIsX11Display(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; + } + } + } + } +#endif + 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.mNativeKeyEvent = static_cast<void*>(aGdkKeyEvent); + aKeyEvent.mIsRepeat = + sRepeatState == REPEATING && + aGdkKeyEvent->hardware_keycode == sLastRepeatableHardwareKeyCode; + + MOZ_LOG( + gKeyLog, 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) { +#ifdef MOZ_X11 + 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; +#else + return false; +#endif +} + +/* 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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(gKeyLog, 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)); +} + +#ifdef MOZ_WAYLAND +void KeymapWrapper::SetFocusIn(wl_surface* aFocusSurface, + uint32_t aFocusSerial) { + LOGW("KeymapWrapper::SetFocusIn() surface %p ID %d serial %d", aFocusSurface, + aFocusSurface ? wl_proxy_get_id((struct wl_proxy*)aFocusSurface) : 0, + aFocusSerial); + + KeymapWrapper* keymapWrapper = KeymapWrapper::GetInstance(); + keymapWrapper->mFocusSurface = aFocusSurface; + keymapWrapper->mFocusSerial = aFocusSerial; +} + +// aFocusSurface can be null in case that focused surface is already destroyed. +void KeymapWrapper::SetFocusOut(wl_surface* aFocusSurface) { + KeymapWrapper* keymapWrapper = KeymapWrapper::GetInstance(); + LOGW("KeymapWrapper::SetFocusOut surface %p ID %d", aFocusSurface, + aFocusSurface ? wl_proxy_get_id((struct wl_proxy*)aFocusSurface) : 0); + + keymapWrapper->mFocusSurface = nullptr; + keymapWrapper->mFocusSerial = 0; +} + +void KeymapWrapper::GetFocusInfo(wl_surface** aFocusSurface, + uint32_t* aFocusSerial) { + KeymapWrapper* keymapWrapper = KeymapWrapper::GetInstance(); + *aFocusSurface = keymapWrapper->mFocusSurface; + *aFocusSerial = keymapWrapper->mFocusSerial; +} + +void KeymapWrapper::SetSeat(wl_seat* aSeat, int aId) { + sSeat = aSeat; + sSeatID = aId; + wl_seat_add_listener(aSeat, &seat_listener, nullptr); +} + +void KeymapWrapper::ClearSeat(int aId) { + if (sSeatID == aId) { + ClearKeyboard(); + sSeat = nullptr; + sSeatID = -1; + } +} + +wl_seat* KeymapWrapper::GetSeat() { return sSeat; } + +void KeymapWrapper::SetKeyboard(wl_keyboard* aKeyboard) { + sKeyboard = aKeyboard; +} + +wl_keyboard* KeymapWrapper::GetKeyboard() { return sKeyboard; } + +void KeymapWrapper::ClearKeyboard() { + if (sKeyboard) { + wl_keyboard_destroy(sKeyboard); + sKeyboard = nullptr; + } +} +#endif + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/nsGtkKeyUtils.h b/widget/gtk/nsGtkKeyUtils.h new file mode 100644 index 0000000000..c5c8096934 --- /dev/null +++ b/widget/gtk/nsGtkKeyUtils.h @@ -0,0 +1,515 @@ +/* -*- 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 "mozilla/EventForwards.h" +#include "nsIWidget.h" +#include "nsTArray.h" + +#include <gdk/gdk.h> +#ifdef MOZ_X11 +# include <X11/XKBlib.h> +#endif +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +# include <xkbcommon/xkbcommon.h> +#endif +#include "X11UndefineNone.h" + +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); + + /** + * Convert native modifiers for `nsIWidget::SynthesizeNative*()` to + * GDK's state. + */ + static guint ConvertWidgetModifierToGdkState( + nsIWidget::Modifiers aNativeModifiers); + + /** + * 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); + + /** + * Wayland global focus handlers + */ + static void SetFocusIn(wl_surface* aFocusSurface, uint32_t aFocusSerial); + static void SetFocusOut(wl_surface* aFocusSurface); + static void GetFocusInfo(wl_surface** aFocusSurface, uint32_t* aFocusSerial); + + static void SetSeat(wl_seat* aSeat, int aId); + static void ClearSeat(int aId); + static wl_seat* GetSeat(); + + static void SetKeyboard(wl_keyboard* aKeyboard); + static wl_keyboard* GetKeyboard(); + static void ClearKeyboard(); + + /** + * EnsureInstance() is provided on Wayland to register Wayland callbacks + * early. + */ + static void EnsureInstance(); +#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(); + + private: + /** + * GetInstance() returns a KeymapWrapper instance. + * + * @return A singleton instance of KeymapWrapper. + */ + static KeymapWrapper* GetInstance(); + + KeymapWrapper(); + ~KeymapWrapper(); + + bool mInitialized; + + /** + * Initializing methods. + */ + void Init(); +#ifdef MOZ_X11 + void InitXKBExtension(); + void InitBySystemSettingsX11(); +#endif +#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; + +#ifdef MOZ_X11 + /** + * 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; +#endif + + /** + * Pointer of the singleton instance. + */ + static KeymapWrapper* sInstance; + + /** + * Auto key repeat management. + */ + static guint sLastRepeatableHardwareKeyCode; +#ifdef MOZ_X11 + static Time sLastRepeatableKeyTime; +#endif + 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); + +#ifdef MOZ_X11 + /** + * 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); +#endif + + /** + * 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 + +#ifdef MOZ_WAYLAND + static wl_seat* sSeat; + static int sSeatID; + static wl_keyboard* sKeyboard; + wl_surface* mFocusSurface = nullptr; + uint32_t mFocusSerial = 0; +#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..ae26049e7f --- /dev/null +++ b/widget/gtk/nsGtkUtils.h @@ -0,0 +1,59 @@ +/* -*- 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))); +} + +// Type-safe alternative to glib's g_clear_pointer. +// +// Using `g_clear_pointer` itself causes UBSan to report undefined +// behavior. The function-based definition of `g_clear_pointer` (as +// opposed to the older preprocessor macro) treats the `destroy` +// function as a `void (*)(void *)`, but the actual destroy functions +// that are used (say `wl_buffer_destroy`) usually have more specific +// pointer types. +// +// C++ draft n4901 [expr.call] para 6: +// +// Calling a function through an expression whose function type E +// is different from the function type F of the called function’s +// definition results in undefined behavior unless the type +// “pointer to F” can be converted to the type “pointer to E” via +// a function pointer conversion (7.3.14). +// +// §7.3.14 only talks about converting between noexcept and ordinary +// function pointers. +template <class T> +static inline void MozClearPointer(T*& pointer, void (*destroy)(T*)) { + T* hold = pointer; + pointer = nullptr; + if (hold) { + destroy(hold); + } +} + +template <class T> +static inline void MozClearHandleID(T& handle, gboolean (*destroy)(T)) { + if (handle) { + destroy(handle); + handle = 0; + } +} + +#endif // nsGtkUtils_h__ diff --git a/widget/gtk/nsImageToPixbuf.cpp b/widget/gtk/nsImageToPixbuf.cpp new file mode 100644 index 0000000000..73094b74ef --- /dev/null +++ b/widget/gtk/nsImageToPixbuf.cpp @@ -0,0 +1,121 @@ +/* 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" +#include "GRefPtr.h" +#include "nsCOMPtr.h" + +using mozilla::gfx::DataSourceSurface; +using mozilla::gfx::SurfaceFormat; + +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; +} + +already_AddRefed<GdkPixbuf> nsImageToPixbuf::ImageToPixbuf( + imgIContainer* aImage, const mozilla::Maybe<nsIntSize>& aOverrideSize) { + RefPtr<SourceSurface> surface; + + const uint32_t flags = + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY; + if (aOverrideSize) { + surface = aImage->GetFrameAtSize(*aOverrideSize, + imgIContainer::FRAME_CURRENT, flags); + } else { + surface = aImage->GetFrame(imgIContainer::FRAME_CURRENT, flags); + } + + // 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) { + if (aOverrideSize) { + surface = + aImage->GetFrameAtSize(*aOverrideSize, imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_NONE); + } else { + surface = aImage->GetFrame(imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_NONE); + } + } + + NS_ENSURE_TRUE(surface, nullptr); + + return SourceSurfaceToPixbuf(surface, surface->GetSize().width, + surface->GetSize().height); +} + +already_AddRefed<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"); + + RefPtr<GdkPixbuf> pixbuf = + dont_AddRef(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.forget(); +} diff --git a/widget/gtk/nsImageToPixbuf.h b/widget/gtk/nsImageToPixbuf.h new file mode 100644 index 0000000000..500489d7eb --- /dev/null +++ b/widget/gtk/nsImageToPixbuf.h @@ -0,0 +1,37 @@ +/* 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 "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "nsSize.h" + +class imgIContainer; +typedef struct _GdkPixbuf GdkPixbuf; + +namespace mozilla::gfx { +class SourceSurface; +} // namespace mozilla::gfx + +class nsImageToPixbuf final { + using SourceSurface = mozilla::gfx::SourceSurface; + + public: + // Friendlier version of ConvertImageToPixbuf for callers inside of + // widget + static already_AddRefed<GdkPixbuf> ImageToPixbuf( + imgIContainer* aImage, + const mozilla::Maybe<nsIntSize>& aOverrideSize = mozilla::Nothing()); + static already_AddRefed<GdkPixbuf> SourceSurfaceToPixbuf( + SourceSurface* aSurface, int32_t aWidth, int32_t aHeight); + + private: + ~nsImageToPixbuf() = default; +}; + +#endif diff --git a/widget/gtk/nsLookAndFeel.cpp b/widget/gtk/nsLookAndFeel.cpp new file mode 100644 index 0000000000..24c196c3c7 --- /dev/null +++ b/widget/gtk/nsLookAndFeel.cpp @@ -0,0 +1,2101 @@ +/* -*- 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 <dlfcn.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 "GRefPtr.h" +#include "GUniquePtr.h" +#include "nsGtkUtils.h" +#include "gfxPlatformGtk.h" +#include "mozilla/FontPropertyTypes.h" +#include "mozilla/Preferences.h" +#include "mozilla/RelativeLuminanceUtils.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Telemetry.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/WidgetUtilsGtk.h" +#include "ScreenHelperGTK.h" +#include "ScrollbarDrawing.h" + +#include "gtkdrawing.h" +#include "nsString.h" +#include "nsStyleConsts.h" +#include "gfxFontConstants.h" +#include "WidgetUtils.h" +#include "nsWindow.h" + +#include "mozilla/gfx/2D.h" + +#include <cairo-gobject.h> +#include <dlfcn.h> +#include "WidgetStyleCache.h" +#include "prenv.h" +#include "nsCSSColorUtils.h" +#include "mozilla/Preferences.h" + +using namespace mozilla; +using namespace mozilla::widget; + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +static LazyLogModule gLnfLog("LookAndFeel"); +# define LOGLNF(...) MOZ_LOG(gLnfLog, LogLevel::Debug, (__VA_ARGS__)) +# define LOGLNF_ENABLED() MOZ_LOG_TEST(gLnfLog, LogLevel::Debug) +#else +# define LOGLNF(args) +# define LOGLNF_ENABLED() false +#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))) + +static bool sIgnoreChangedSettings = false; + +static void OnSettingsChange() { + if (sIgnoreChangedSettings) { + return; + } + // TODO: We could be more granular here, but for now assume everything + // changed. + LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout); + widget::IMContextWrapper::OnThemeChanged(); +} + +static void settings_changed_cb(GtkSettings*, GParamSpec*, void*) { + OnSettingsChange(); +} + +static bool sCSDAvailable; + +static nsCString GVariantToString(GVariant* aVariant) { + nsCString ret; + gchar* s = g_variant_print(aVariant, TRUE); + if (s) { + ret.Assign(s); + g_free(s); + } + return ret; +} + +static nsDependentCString GVariantGetString(GVariant* aVariant) { + gsize len = 0; + const gchar* v = g_variant_get_string(aVariant, &len); + return nsDependentCString(v, len); +} + +static void settings_changed_signal_cb(GDBusProxy* proxy, gchar* sender_name, + gchar* signal_name, GVariant* parameters, + gpointer user_data) { + LOGLNF("Settings Change sender=%s signal=%s params=%s\n", sender_name, + signal_name, GVariantToString(parameters).get()); + if (strcmp(signal_name, "SettingChanged")) { + NS_WARNING("Unknown change signal for settings"); + return; + } + RefPtr<GVariant> ns = dont_AddRef(g_variant_get_child_value(parameters, 0)); + RefPtr<GVariant> key = dont_AddRef(g_variant_get_child_value(parameters, 1)); + // Third parameter is the value, but we don't care about it. + if (!ns || !key || !g_variant_is_of_type(ns, G_VARIANT_TYPE_STRING) || + !g_variant_is_of_type(key, G_VARIANT_TYPE_STRING)) { + MOZ_ASSERT(false, "Unexpected setting change signal parameters"); + return; + } + + auto* lnf = static_cast<nsLookAndFeel*>(user_data); + + auto nsStr = GVariantGetString(ns); + auto keyStr = GVariantGetString(key); + if (nsStr.Equals("org.freedesktop.appearance"_ns) && + keyStr.Equals("color-scheme"_ns)) { + lnf->OnColorSchemeSettingChanged(); + } +} + +nsLookAndFeel::nsLookAndFeel() { + static constexpr nsLiteralCString kObservedSettings[] = { + // Affects system font sizes. + "notify::gtk-xft-dpi"_ns, + // Affects mSystemTheme and mAltTheme as expected. + "notify::gtk-theme-name"_ns, + // System fonts? + "notify::gtk-font-name"_ns, + // prefers-reduced-motion + "notify::gtk-enable-animations"_ns, + // CSD media queries, etc. + "notify::gtk-decoration-layout"_ns, + // Text resolution affects system font and widget sizes. + "notify::resolution"_ns, + // These three Affect mCaretBlinkTime + "notify::gtk-cursor-blink"_ns, + "notify::gtk-cursor-blink-time"_ns, + "notify::gtk-cursor-blink-timeout"_ns, + // Affects SelectTextfieldsOnKeyFocus + "notify::gtk-entry-select-on-focus"_ns, + // Affects ScrollToClick + "notify::gtk-primary-button-warps-slider"_ns, + // Affects SubmenuDelay + "notify::gtk-menu-popup-delay"_ns, + // Affects DragThresholdX/Y + "notify::gtk-dnd-drag-threshold"_ns, + }; + + GtkSettings* settings = gtk_settings_get_default(); + for (const auto& setting : kObservedSettings) { + g_signal_connect_after(settings, setting.get(), + G_CALLBACK(settings_changed_cb), nullptr); + } + + sCSDAvailable = + nsWindow::GetSystemGtkWindowDecoration() != nsWindow::GTK_DECORATION_NONE; + + if (ShouldUsePortal(PortalKind::Settings)) { + GUniquePtr<GError> error; + mDBusSettingsProxy = dont_AddRef(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.Settings", nullptr, getter_Transfers(error))); + if (mDBusSettingsProxy) { + g_signal_connect(mDBusSettingsProxy, "g-signal", + G_CALLBACK(settings_changed_signal_cb), this); + } else { + LOGLNF("Can't create DBus proxy for settings: %s\n", error->message); + } + } +} + +nsLookAndFeel::~nsLookAndFeel() { + if (mDBusSettingsProxy) { + g_signal_handlers_disconnect_by_func( + mDBusSettingsProxy, FuncToGpointer(settings_changed_signal_cb), this); + mDBusSettingsProxy = nullptr; + } + g_signal_handlers_disconnect_by_func( + gtk_settings_get_default(), FuncToGpointer(settings_changed_cb), nullptr); +} + +#if 0 +static void DumpStyleContext(GtkStyleContext* aStyle) { + static auto sGtkStyleContextToString = + reinterpret_cast<char* (*)(GtkStyleContext*, gint)>( + dlsym(RTLD_DEFAULT, "gtk_style_context_to_string")); + char* str = sGtkStyleContextToString(aStyle, ~0); + printf("%s\n", str); + g_free(str); + str = gtk_widget_path_to_string(gtk_style_context_get_path(aStyle)); + printf("%s\n", str); + g_free(str); +} +#endif + +// 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 GetColorFromImagePattern(const GValue* aValue, nscolor* aColor) { + 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; + } + + cairo_surface_t* surface; + if (cairo_pattern_get_surface(pattern, &surface) != CAIRO_STATUS_SUCCESS) { + return false; + } + + cairo_format_t format = cairo_image_surface_get_format(surface); + if (format == CAIRO_FORMAT_INVALID) { + return false; + } + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + int stride = cairo_image_surface_get_stride(surface); + if (!width || !height) { + return false; + } + + // Guesstimate the central pixel would have a sensible color. + int x = width / 2; + int y = height / 2; + + unsigned char* data = cairo_image_surface_get_data(surface); + switch (format) { + // Most (all?) GTK images / patterns / etc use ARGB32. + case CAIRO_FORMAT_ARGB32: { + size_t offset = x * 4 + y * stride; + uint32_t* pixel = reinterpret_cast<uint32_t*>(data + offset); + *aColor = gfx::sRGBColor::UnusualFromARGB(*pixel).ToABGR(); + return true; + } + default: + break; + } + + return false; +} + +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 +void nsLookAndFeel::PerThemeData::InitCellHighlightColors() { + int32_t minLuminosityDifference = NS_SUFFICIENT_LUMINOSITY_DIFFERENCE_BG; + int32_t backLuminosityDifference = + NS_LUMINOSITY_DIFFERENCE(mMozWindowBackground, mFieldBackground); + if (backLuminosityDifference >= minLuminosityDifference) { + mMozCellHighlightBackground = mMozWindowBackground; + mMozCellHighlightText = mMozWindowText; + return; + } + + 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); +} + +void nsLookAndFeel::NativeInit() { EnsureInit(); } + +void nsLookAndFeel::RefreshImpl() { + mInitialized = false; + moz_gtk_refresh(); + + nsXPLookAndFeel::RefreshImpl(); +} + +nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme, + nscolor& aColor) { + EnsureInit(); + + auto& theme = aScheme == ColorScheme::Light ? LightTheme() : DarkTheme(); + return theme.GetColor(aID, aColor); +} + +static bool ShouldUseColorForActiveDarkScrollbarThumb(nscolor aColor) { + auto IsDifferentEnough = [](int32_t aChannel, int32_t aOtherChannel) { + return std::abs(aChannel - aOtherChannel) > 10; + }; + return IsDifferentEnough(NS_GET_R(aColor), NS_GET_G(aColor)) || + IsDifferentEnough(NS_GET_R(aColor), NS_GET_B(aColor)); +} + +static bool ShouldUseThemedScrollbarColor(StyleSystemColor aID, nscolor aColor, + bool aIsDark) { + if (!aIsDark) { + return true; + } + if (StaticPrefs::widget_non_native_theme_scrollbar_dark_themed()) { + return true; + } + return aID == StyleSystemColor::ThemedScrollbarThumbActive && + StaticPrefs::widget_non_native_theme_scrollbar_active_always_themed(); +} + +nsresult nsLookAndFeel::PerThemeData::GetColor(ColorID aID, + nscolor& aColor) const { + nsresult res = NS_OK; + + switch (aID) { + // These colors don't seem to be used for anything anymore in Mozilla + // The CSS2 colors below are used. + 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::Windowtext: + case ColorID::MozDialogtext: + aColor = mMozWindowText; + break; + case ColorID::IMESelectedRawTextBackground: + case ColorID::IMESelectedConvertedTextBackground: + case ColorID::MozDragtargetzone: + case ColorID::Highlight: // preference selected item, + aColor = mTextSelectedBackground; + break; + case ColorID::Highlighttext: + if (NS_GET_A(mTextSelectedBackground) < 155) { + aColor = NS_SAME_AS_FOREGROUND_COLOR; + break; + } + [[fallthrough]]; + case ColorID::IMESelectedRawTextForeground: + case ColorID::IMESelectedConvertedTextForeground: + aColor = mTextSelectedText; + break; + case ColorID::Selecteditem: + aColor = mSelectedItem; + break; + case ColorID::Selecteditemtext: + aColor = mSelectedItemText; + break; + case ColorID::Accentcolor: + aColor = mAccentColor; + break; + case ColorID::Accentcolortext: + aColor = mAccentColorText; + break; + case ColorID::MozCellhighlight: + aColor = mMozCellHighlightBackground; + break; + case ColorID::MozCellhighlighttext: + aColor = mMozCellHighlightText; + 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::Scrollbar: + aColor = mThemedScrollbar; + break; + case ColorID::ThemedScrollbar: + aColor = mThemedScrollbar; + if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) { + return NS_ERROR_FAILURE; + } + break; + case ColorID::ThemedScrollbarInactive: + aColor = mThemedScrollbarInactive; + if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) { + return NS_ERROR_FAILURE; + } + break; + case ColorID::ThemedScrollbarThumb: + aColor = mThemedScrollbarThumb; + if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) { + return NS_ERROR_FAILURE; + } + break; + case ColorID::ThemedScrollbarThumbHover: + aColor = mThemedScrollbarThumbHover; + if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) { + return NS_ERROR_FAILURE; + } + break; + case ColorID::ThemedScrollbarThumbActive: + aColor = mThemedScrollbarThumbActive; + if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) { + return NS_ERROR_FAILURE; + } + break; + case ColorID::ThemedScrollbarThumbInactive: + aColor = mThemedScrollbarThumbInactive; + if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) { + return NS_ERROR_FAILURE; + } + 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. + aColor = mMenuTextInactive; + break; + case ColorID::Activecaption: + aColor = mTitlebarBackground; + break; + case ColorID::Captiontext: // text in active window caption (titlebar) + aColor = mTitlebarText; + break; + case ColorID::Inactivecaption: + // inactive window caption + aColor = mTitlebarInactiveBackground; + break; + case ColorID::Inactivecaptiontext: // text in active window caption + // (titlebar) + aColor = mTitlebarInactiveText; + 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::Threedface: + case ColorID::Buttonface: + case ColorID::MozButtondisabledface: + // 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: + case ColorID::Buttonborder: + case ColorID::MozDisabledfield: + aColor = mIsDark ? *GenericDarkColor(aID) : NS_RGB(0xE0, 0xE0, 0xE0); + break; + case ColorID::Threeddarkshadow: + aColor = mIsDark ? *GenericDarkColor(aID) : 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: + case ColorID::MozButtonactiveface: + aColor = mButtonHoverFace; + break; + case ColorID::MozButtonhovertext: + aColor = mButtonHoverText; + break; + case ColorID::MozButtonactivetext: + aColor = mButtonActiveText; + break; + case ColorID::MozMenuhover: + aColor = mMenuHover; + break; + case ColorID::MozMenuhoverdisabled: + aColor = NS_TRANSPARENT; + break; + case ColorID::MozMenuhovertext: + aColor = mMenuHoverText; + break; + case ColorID::MozOddtreerow: + aColor = mOddCellBackground; + break; + case ColorID::MozNativehyperlinktext: + aColor = mNativeHyperLinkText; + break; + case ColorID::MozNativevisitedhyperlinktext: + aColor = mNativeVisitedHyperLinkText; + break; + case ColorID::MozComboboxtext: + aColor = mComboBoxText; + break; + case ColorID::MozColheadertext: + aColor = mMozColHeaderText; + break; + case ColorID::MozColheaderhovertext: + aColor = mMozColHeaderHoverText; + break; + case ColorID::SpellCheckerUnderline: + case ColorID::Mark: + case ColorID::Marktext: + aColor = GetStandinForNativeColor( + aID, mIsDark ? ColorScheme::Dark : ColorScheme::Light); + 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::CaretBlinkCount: + EnsureInit(); + aResult = mCaretBlinkCount; + 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_VERTICAL); + aResult = ConvertGTKStepperStyleToMozillaScrollArrowStyle(scrollbar); + 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::AlertNotificationOrigin: + aResult = NS_ALERT_TOP; + break; + case IntID::IMERawInputUnderlineStyle: + case IntID::IMEConvertedTextUnderlineStyle: + aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid); + break; + case IntID::IMESelectedRawTextUnderlineStyle: + case IntID::IMESelectedConvertedTextUnderline: + aResult = static_cast<int32_t>(StyleTextDecorationStyle::None); + break; + case IntID::SpellCheckerUnderlineStyle: + aResult = static_cast<int32_t>(StyleTextDecorationStyle::Wavy); + break; + case IntID::MenuBarDrag: + EnsureInit(); + aResult = mSystemTheme.mMenuSupportsDrag; + break; + case IntID::ScrollbarButtonAutoRepeatBehavior: + aResult = 1; + break; + case IntID::SwipeAnimationEnabled: + aResult = 1; + break; + case IntID::ContextMenuOffsetVertical: + case IntID::ContextMenuOffsetHorizontal: + aResult = 2; + break; + case IntID::GTKCSDAvailable: + aResult = sCSDAvailable; + break; + case IntID::GTKCSDMaximizeButton: + EnsureInit(); + aResult = mCSDMaximizeButton; + break; + case IntID::GTKCSDMinimizeButton: + EnsureInit(); + aResult = mCSDMinimizeButton; + break; + case IntID::GTKCSDCloseButton: + EnsureInit(); + aResult = mCSDCloseButton; + break; + case IntID::GTKCSDReversedPlacement: + EnsureInit(); + aResult = mCSDReversedPlacement; + break; + case IntID::PrefersReducedMotion: { + aResult = mPrefersReducedMotion; + break; + } + case IntID::SystemUsesDarkTheme: { + EnsureInit(); + if (mColorSchemePreference) { + aResult = *mColorSchemePreference == ColorScheme::Dark; + } else { + aResult = mSystemTheme.mIsDark; + } + break; + } + case IntID::GTKCSDMaximizeButtonPosition: + aResult = mCSDMaximizeButtonPosition; + break; + case IntID::GTKCSDMinimizeButtonPosition: + aResult = mCSDMinimizeButtonPosition; + break; + case IntID::GTKCSDCloseButtonPosition: + aResult = mCSDCloseButtonPosition; + break; + case IntID::UseAccessibilityTheme: + // If high contrast is enabled, enable prefers-reduced-transparency media + // query as well as there is no dedicated option. + case IntID::PrefersReducedTransparency: + EnsureInit(); + aResult = mSystemTheme.mHighContrast; + break; + case IntID::InvertedColors: + // No GTK API for checking if inverted colors is enabled + aResult = 0; + break; + case IntID::TitlebarRadius: { + EnsureInit(); + aResult = EffectiveTheme().mTitlebarRadius; + break; + } + case IntID::AllowOverlayScrollbarsOverlap: { + aResult = 1; + break; + } + case IntID::ScrollbarFadeBeginDelay: { + aResult = 1000; + break; + } + case IntID::ScrollbarFadeDuration: { + aResult = 400; + break; + } + case IntID::ScrollbarDisplayOnMouseMove: { + aResult = 1; + break; + } + case IntID::PanelAnimations: + aResult = [&]() -> bool { + if (!sCSDAvailable) { + // Disabled on systems without CSD, see bug 1385079. + return false; + } + if (GdkIsWaylandDisplay()) { + // Disabled on wayland, see bug 1800442 and bug 1800368. + return false; + } + if (IsKdeDesktopEnvironment()) { + // Disabled on KDE, see bug 1813070. + return false; + } + return true; + }(); + break; + case IntID::UseOverlayScrollbars: { + aResult = StaticPrefs::widget_gtk_overlay_scrollbars_enabled(); + break; + } + case IntID::TouchDeviceSupportPresent: + aResult = widget::WidgetUtilsGTK::IsTouchDeviceSupportPresent() ? 1 : 0; + 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 = mSystemTheme.mCaretRatio; + break; + case FloatID::TextScaleFactor: + aResult = gfxPlatformGtk::GetFontScaleFactor(); + 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::FromInt(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) { + return mSystemTheme.GetFont(aID, aFontName, aFontStyle); +} + +bool nsLookAndFeel::PerThemeData::GetFont(FontID aID, nsString& aFontName, + gfxFontStyle& aFontStyle) const { + switch (aID) { + case FontID::Menu: // css2 + case FontID::MozPullDownMenu: // css3 + aFontName = mMenuFontName; + aFontStyle = mMenuFontStyle; + break; + + case FontID::MozField: // css3 + case FontID::MozList: // css3 + aFontName = mFieldFontName; + aFontStyle = mFieldFontStyle; + break; + + case FontID::MozButton: // 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 + default: + aFontName = mDefaultFontName; + aFontStyle = mDefaultFontStyle; + break; + } + + // Convert GDK pixels to CSS pixels. + // When "layout.css.devPixelsPerPx" > 0, this is not a direct conversion. + // The difference produces a scaling of system fonts in proportion with + // other scaling from the change in CSS pixel sizes. + aFontStyle.size /= LookAndFeel::GetTextScaleFactor(); + 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 GetGtkSettingsStringKey(const char* aKey) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + nsCString ret; + GtkSettings* settings = gtk_settings_get_default(); + char* value = nullptr; + g_object_get(settings, aKey, &value, nullptr); + if (value) { + ret.Assign(value); + g_free(value); + } + return ret; +} + +static nsCString GetGtkTheme() { + return GetGtkSettingsStringKey("gtk-theme-name"); +} + +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; +} + +// 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. +static bool GetThemeIsDark() { + GdkRGBA bg, fg; + GtkStyleContext* 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); + return RelativeLuminanceUtils::Compute(GDK_RGBA_TO_NS_RGBA(bg)) < + RelativeLuminanceUtils::Compute(GDK_RGBA_TO_NS_RGBA(fg)); +} + +void nsLookAndFeel::RestoreSystemTheme() { + LOGLNF("RestoreSystemTheme(%s, %d, %d)\n", mSystemTheme.mName.get(), + mSystemTheme.mPreferDarkTheme, mSystemThemeOverridden); + + if (!mSystemThemeOverridden) { + return; + } + + // Available on Gtk 3.20+. + static auto sGtkSettingsResetProperty = + (void (*)(GtkSettings*, const gchar*))dlsym( + RTLD_DEFAULT, "gtk_settings_reset_property"); + + 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", mSystemTheme.mName.get(), + "gtk-application-prefer-dark-theme", + mSystemTheme.mPreferDarkTheme, nullptr); + } + moz_gtk_refresh(); + mSystemThemeOverridden = false; +} + +static bool AnyColorChannelIsDifferent(nscolor aColor) { + return NS_GET_R(aColor) != NS_GET_G(aColor) || + NS_GET_R(aColor) != NS_GET_B(aColor); +} + +void nsLookAndFeel::ConfigureAndInitializeAltTheme() { + GtkSettings* settings = gtk_settings_get_default(); + + bool fellBackToDefaultTheme = false; + + // Try to select the opposite variant of the current theme first... + LOGLNF(" toggling gtk-application-prefer-dark-theme\n"); + g_object_set(settings, "gtk-application-prefer-dark-theme", + !mSystemTheme.mIsDark, nullptr); + moz_gtk_refresh(); + + // Toggling gtk-application-prefer-dark-theme is not enough generally to + // switch from dark to light theme. If the theme didn't change, and we have + // a dark theme, try to first remove -Dark{,er,est} from the theme name to + // find the light variant. + if (mSystemTheme.mIsDark && mSystemTheme.mIsDark == GetThemeIsDark()) { + nsCString potentialLightThemeName = mSystemTheme.mName; + // clang-format off + constexpr nsLiteralCString kSubstringsToRemove[] = { + "-darkest"_ns, "-darker"_ns, "-dark"_ns, + "-Darkest"_ns, "-Darker"_ns, "-Dark"_ns, + "_darkest"_ns, "_darker"_ns, "_dark"_ns, + "_Darkest"_ns, "_Darker"_ns, "_Dark"_ns, + }; + // clang-format on + bool found = false; + for (auto& s : kSubstringsToRemove) { + potentialLightThemeName = mSystemTheme.mName; + potentialLightThemeName.ReplaceSubstring(s, ""_ns); + if (potentialLightThemeName.Length() != mSystemTheme.mName.Length()) { + found = true; + break; + } + } + if (found) { + g_object_set(settings, "gtk-theme-name", potentialLightThemeName.get(), + nullptr); + moz_gtk_refresh(); + } + } + + if (mSystemTheme.mIsDark == GetThemeIsDark()) { + // If the theme still didn't change enough, fall back to Adwaita with the + // appropriate color preference. + g_object_set(settings, "gtk-theme-name", "Adwaita", + "gtk-application-prefer-dark-theme", !mSystemTheme.mIsDark, + nullptr); + moz_gtk_refresh(); + + // If it _still_ didn't change enough, and we're dark, try to set + // Adwaita-dark as a theme name. This might be needed in older GTK versions. + if (!mSystemTheme.mIsDark && !GetThemeIsDark()) { + g_object_set(settings, "gtk-theme-name", "Adwaita-dark", nullptr); + moz_gtk_refresh(); + } + + fellBackToDefaultTheme = true; + } + + mAltTheme.Init(); + + // Some of the alt theme colors we can grab from the system theme, if we fell + // back to the default light / dark themes. + if (fellBackToDefaultTheme) { + if (StaticPrefs::widget_gtk_alt_theme_selection()) { + mAltTheme.mTextSelectedText = mSystemTheme.mTextSelectedText; + mAltTheme.mTextSelectedBackground = mSystemTheme.mTextSelectedBackground; + } + + if (StaticPrefs::widget_gtk_alt_theme_scrollbar_active() && + (!mAltTheme.mIsDark || ShouldUseColorForActiveDarkScrollbarThumb( + mSystemTheme.mThemedScrollbarThumbActive))) { + mAltTheme.mThemedScrollbarThumbActive = + mSystemTheme.mThemedScrollbarThumbActive; + } + + if (StaticPrefs::widget_gtk_alt_theme_accent()) { + mAltTheme.mAccentColor = mSystemTheme.mAccentColor; + mAltTheme.mAccentColorText = mSystemTheme.mAccentColorText; + } + } + + // Special case for Adwaita: In GTK3 we don't have more proper accent colors, + // so we use the selected background colors. Those colors, however, don't have + // much contrast in dark mode (see bug 1741293). We know, however, that GTK4 + // uses the light accent color for the dark theme, see: + // + // https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/named-colors.html#accent-colors + // + // So we manually do that. + if (mSystemTheme.mName.EqualsLiteral("Adwaita") || + mSystemTheme.mName.EqualsLiteral("Adwaita-dark")) { + auto& dark = mSystemTheme.mIsDark ? mSystemTheme : mAltTheme; + auto& light = mSystemTheme.mIsDark ? mAltTheme : mSystemTheme; + + dark.mAccentColor = light.mAccentColor; + dark.mAccentColorText = light.mAccentColorText; + } + + // Right now we're using the opposite color-scheme theme, make sure to record + // it. + mSystemThemeOverridden = true; +} + +Maybe<ColorScheme> nsLookAndFeel::ComputeColorSchemeSetting() { + { + // Check the pref explicitly here. Usually this shouldn't be needed, but + // since we can only load one GTK theme at a time, and the pref will + // override the effective value that the rest of gecko assumes for the + // "system" color scheme, we need to factor it in our GTK theme decisions. + int32_t pref = 0; + if (NS_SUCCEEDED(Preferences::GetInt("ui.systemUsesDarkTheme", &pref))) { + return Some(pref ? ColorScheme::Dark : ColorScheme::Light); + } + } + + if (!mDBusSettingsProxy) { + return Nothing(); + } + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef(g_dbus_proxy_call_sync( + mDBusSettingsProxy, "Read", + g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"), + G_DBUS_CALL_FLAGS_NONE, + StaticPrefs::widget_gtk_settings_portal_timeout_ms(), nullptr, + getter_Transfers(error))); + if (!variant) { + LOGLNF("color-scheme query error: %s\n", error->message); + return Nothing(); + } + LOGLNF("color-scheme query result: %s\n", GVariantToString(variant).get()); + variant = dont_AddRef(g_variant_get_child_value(variant, 0)); + while (variant && g_variant_is_of_type(variant, G_VARIANT_TYPE_VARIANT)) { + // Unbox the return value. + variant = dont_AddRef(g_variant_get_variant(variant)); + } + if (!variant || !g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32)) { + MOZ_ASSERT(false, "Unexpected color-scheme query return value"); + return Nothing(); + } + switch (g_variant_get_uint32(variant)) { + default: + MOZ_FALLTHROUGH_ASSERT("Unexpected color-scheme query return value"); + case 0: + break; + case 1: + return Some(ColorScheme::Dark); + case 2: + return Some(ColorScheme::Light); + } + return Nothing(); +} + +void nsLookAndFeel::Initialize() { + LOGLNF("nsLookAndFeel::Initialize"); + MOZ_DIAGNOSTIC_ASSERT(!mInitialized); + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "LookAndFeel init should be done on the main thread"); + + mInitialized = true; + + GtkSettings* settings = gtk_settings_get_default(); + if (MOZ_UNLIKELY(!settings)) { + NS_WARNING("EnsureInit: No settings"); + return; + } + + AutoRestore<bool> restoreIgnoreSettings(sIgnoreChangedSettings); + sIgnoreChangedSettings = true; + + // Our current theme may be different from the system theme if we're matching + // the Firefox theme or using the alt theme intentionally due to the + // color-scheme preference. Make sure to restore the original system theme. + RestoreSystemTheme(); + + // First initialize global settings. + InitializeGlobalSettings(); + + // Record our system theme settings now. + mSystemTheme.Init(); + + // Find the alternative-scheme theme (light if the system theme is dark, or + // vice versa), configure it and initialize it. + ConfigureAndInitializeAltTheme(); + + LOGLNF("System Theme: %s. Alt Theme: %s\n", mSystemTheme.mName.get(), + mAltTheme.mName.get()); + + // Go back to the system theme or keep the alt theme configured, depending on + // Firefox theme or user color-scheme preference. + ConfigureFinalEffectiveTheme(); + + RecordTelemetry(); +} + +void nsLookAndFeel::OnColorSchemeSettingChanged() { + if (NS_WARN_IF(mColorSchemePreference == ComputeColorSchemeSetting())) { + // We sometimes get duplicate color-scheme changes from dbus, avoid doing + // extra work if not needed. + return; + } + OnSettingsChange(); +} + +void nsLookAndFeel::InitializeGlobalSettings() { + GtkSettings* settings = gtk_settings_get_default(); + + mColorSchemePreference = ComputeColorSchemeSetting(); + + gboolean enableAnimations = false; + g_object_get(settings, "gtk-enable-animations", &enableAnimations, nullptr); + mPrefersReducedMotion = !enableAnimations; + + gint blink_time = 0; // In milliseconds + gint blink_timeout = 0; // in seconds + gboolean blink; + g_object_get(settings, "gtk-cursor-blink-time", &blink_time, + "gtk-cursor-blink-timeout", &blink_timeout, "gtk-cursor-blink", + &blink, nullptr); + // From + // https://docs.gtk.org/gtk3/property.Settings.gtk-cursor-blink-timeout.html: + // + // Setting this to zero has the same effect as setting + // GtkSettings:gtk-cursor-blink to FALSE. + // + mCaretBlinkTime = blink && blink_timeout ? (int32_t)blink_time : 0; + + if (mCaretBlinkTime) { + // blink_time * 2 because blink count is a full blink cycle. + mCaretBlinkCount = + std::max(1, int32_t(std::ceil(float(blink_timeout * 1000) / + (float(blink_time) * 2.0f)))); + } else { + mCaretBlinkCount = -1; + } + + 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; + } + } +} + +void nsLookAndFeel::ConfigureFinalEffectiveTheme() { + MOZ_ASSERT(mSystemThemeOverridden, + "By this point, the alt theme should be configured"); + + const bool shouldUseSystemTheme = [&] { + // NOTE: We can't call ColorSchemeForChrome directly because this might run + // while we're computing it. + switch (ColorSchemeSettingForChrome()) { + case ChromeColorSchemeSetting::Light: + return !mSystemTheme.mIsDark; + case ChromeColorSchemeSetting::Dark: + return mSystemTheme.mIsDark; + case ChromeColorSchemeSetting::System: + break; + }; + if (!mColorSchemePreference) { + return true; + } + bool preferenceIsDark = *mColorSchemePreference == ColorScheme::Dark; + return preferenceIsDark == mSystemTheme.mIsDark; + }(); + + const bool usingSystem = !mSystemThemeOverridden; + LOGLNF("OverrideSystemThemeIfNeeded(matchesSystem=%d, usingSystem=%d)\n", + shouldUseSystemTheme, usingSystem); + + if (shouldUseSystemTheme) { + RestoreSystemTheme(); + } else if (usingSystem) { + LOGLNF("Setting theme %s, %d\n", mAltTheme.mName.get(), + mAltTheme.mPreferDarkTheme); + + GtkSettings* settings = gtk_settings_get_default(); + if (mSystemTheme.mName == mAltTheme.mName) { + // Prefer setting only gtk-application-prefer-dark-theme, so we can still + // get notified from notify::gtk-theme-name if the user changes the theme. + g_object_set(settings, "gtk-application-prefer-dark-theme", + mAltTheme.mPreferDarkTheme, nullptr); + } else { + g_object_set(settings, "gtk-theme-name", mAltTheme.mName.get(), + "gtk-application-prefer-dark-theme", + mAltTheme.mPreferDarkTheme, nullptr); + } + moz_gtk_refresh(); + mSystemThemeOverridden = true; + } +} + +static nscolor GetBackgroundColor( + GtkStyleContext* aStyle, nscolor aForForegroundColor, + GtkStateFlags aState = GTK_STATE_FLAG_NORMAL, + nscolor aOverBackgroundColor = NS_TRANSPARENT) { + GdkRGBA gdkColor; + gtk_style_context_get_background_color(aStyle, aState, &gdkColor); + nscolor color = GDK_RGBA_TO_NS_RGBA(gdkColor); + if (NS_GET_A(color)) { + if (color != aOverBackgroundColor) { + return color; + } + } + + // Try to synthesize a color from a background-image. + GValue value = G_VALUE_INIT; + gtk_style_context_get_property(aStyle, "background-image", aState, &value); + auto cleanup = MakeScopeExit([&] { g_value_unset(&value); }); + + if (GetColorFromImagePattern(&value, &color)) { + return color; + } + + { + GdkRGBA light, dark; + if (GetGradientColors(&value, &light, &dark)) { + nscolor l = GDK_RGBA_TO_NS_RGBA(light); + nscolor d = GDK_RGBA_TO_NS_RGBA(dark); + // Return the one with more contrast. + // TODO(emilio): This could do interpolation or what not but seems + // overkill. + return NS_LUMINOSITY_DIFFERENCE(l, aForForegroundColor) > + NS_LUMINOSITY_DIFFERENCE(d, aForForegroundColor) + ? l + : d; + } + } + + return NS_TRANSPARENT; +} + +static bool GetNamedColorPair(GtkStyleContext* aStyle, const char* aBgName, + const char* aFgName, nscolor* aBg, nscolor* aFg) { + GdkRGBA bg, fg; + if (!gtk_style_context_lookup_color(aStyle, aBgName, &bg) || + !gtk_style_context_lookup_color(aStyle, aFgName, &fg)) { + return false; + } + + *aBg = GDK_RGBA_TO_NS_RGBA(bg); + *aFg = GDK_RGBA_TO_NS_RGBA(fg); + + // If the colors are semi-transparent and the theme provides a + // background color, blend with them to get the "final" color, see + // bug 1717077. + if (NS_GET_A(*aBg) != 255 && + (gtk_style_context_lookup_color(aStyle, "bg_color", &bg) || + gtk_style_context_lookup_color(aStyle, "theme_bg_color", &bg))) { + *aBg = NS_ComposeColors(GDK_RGBA_TO_NS_RGBA(bg), *aBg); + } + + // A semi-transparent foreground color would be kinda silly, but is done + // for symmetry. + if (NS_GET_A(*aFg) != 255 && + (gtk_style_context_lookup_color(aStyle, "fg_color", &fg) || + gtk_style_context_lookup_color(aStyle, "theme_fg_color", &fg))) { + *aFg = NS_ComposeColors(GDK_RGBA_TO_NS_RGBA(fg), *aFg); + } + + return true; +} + +static void EnsureColorPairIsOpaque(nscolor& aBg, nscolor& aFg) { + // Blend with white, ensuring the color is opaque, so that the UI doesn't have + // to care about alpha. + aBg = NS_ComposeColors(NS_RGB(0xff, 0xff, 0xff), aBg); + aFg = NS_ComposeColors(aBg, aFg); +} + +static void PreferDarkerBackground(nscolor& aBg, nscolor& aFg) { + // We use the darker one unless the foreground isn't really a color (is all + // white / black / gray) and the background is, in which case we stick to what + // we have. + if (RelativeLuminanceUtils::Compute(aBg) > + RelativeLuminanceUtils::Compute(aFg) && + (AnyColorChannelIsDifferent(aFg) || !AnyColorChannelIsDifferent(aBg))) { + std::swap(aBg, aFg); + } +} + +void nsLookAndFeel::PerThemeData::Init() { + mName = GetGtkTheme(); + + GtkStyleContext* style; + + mHighContrast = StaticPrefs::widget_content_gtk_high_contrast_enabled() && + mName.Find("HighContrast"_ns) >= 0; + + mPreferDarkTheme = GetPreferDarkTheme(); + + mIsDark = GetThemeIsDark(); + + mCompatibleWithHTMLLightColors = + !mIsDark && IsGtkThemeCompatibleWithHTMLColors(); + + GdkRGBA color; + // 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)); + + 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); + + // Make sure that the thumb is visible, at least. + const bool fallbackToUnthemedColors = [&] { + if (!StaticPrefs::widget_gtk_theme_scrollbar_colors_enabled()) { + return true; + } + + if (!ShouldHonorThemeScrollbarColors()) { + return true; + } + // If any of the scrollbar thumb colors are fully transparent, fall back to + // non-native ones. + if (!NS_GET_A(mThemedScrollbarThumb) || + !NS_GET_A(mThemedScrollbarThumbHover) || + !NS_GET_A(mThemedScrollbarThumbActive)) { + return true; + } + // If the thumb and track are the same color and opaque, fall back to + // non-native colors as well. + if (mThemedScrollbar == mThemedScrollbarThumb && + NS_GET_A(mThemedScrollbar) == 0xff) { + return true; + } + return false; + }(); + + if (fallbackToUnthemedColors) { + if (mIsDark) { + // Taken from Adwaita-dark. + mThemedScrollbar = NS_RGB(0x31, 0x31, 0x31); + mThemedScrollbarInactive = NS_RGB(0x2d, 0x2d, 0x2d); + mThemedScrollbarThumb = NS_RGB(0xa3, 0xa4, 0xa4); + mThemedScrollbarThumbInactive = NS_RGB(0x59, 0x5a, 0x5a); + } else { + // Taken from Adwaita. + mThemedScrollbar = NS_RGB(0xce, 0xce, 0xce); + mThemedScrollbarInactive = NS_RGB(0xec, 0xed, 0xef); + mThemedScrollbarThumb = NS_RGB(0x82, 0x81, 0x7e); + mThemedScrollbarThumbInactive = NS_RGB(0xce, 0xcf, 0xce); + } + + mThemedScrollbarThumbHover = ThemeColors::AdjustUnthemedScrollbarThumbColor( + mThemedScrollbarThumb, dom::ElementState::HOVER); + mThemedScrollbarThumbActive = + ThemeColors::AdjustUnthemedScrollbarThumbColor( + mThemedScrollbarThumb, dom::ElementState::ACTIVE); + } + + // 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_color(style, GTK_STATE_FLAG_NORMAL, &color); + mMozWindowText = GDK_RGBA_TO_NS_RGBA(color); + + mMozWindowBackground = GetBackgroundColor(style, mMozWindowText); + + 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); + + 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_BOX_LABEL); + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mInfoText = GDK_RGBA_TO_NS_RGBA(color); + + style = GetStyleContext(MOZ_GTK_TOOLTIP); + mInfoBackground = GetBackgroundColor(style, mInfoText); + + 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); + } + + const auto effectiveHeaderBarStyle = + HeaderBarShouldDrawContainer(MOZ_GTK_HEADER_BAR) ? MOZ_GTK_HEADERBAR_FIXED + : MOZ_GTK_HEADER_BAR; + style = GetStyleContext(effectiveHeaderBarStyle); + { + gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color); + mTitlebarText = GDK_RGBA_TO_NS_RGBA(color); + mTitlebarBackground = GetBackgroundColor(style, mTitlebarText); + + gtk_style_context_get_color(style, GTK_STATE_FLAG_BACKDROP, &color); + mTitlebarInactiveText = GDK_RGBA_TO_NS_RGBA(color); + mTitlebarInactiveBackground = + GetBackgroundColor(style, mTitlebarText, GTK_STATE_FLAG_BACKDROP); + mTitlebarRadius = IsSolidCSDStyleUsed() ? 0 : GetBorderRadius(style); + } + + style = GetStyleContext(MOZ_GTK_MENUPOPUP); + mMenuBackground = [&] { + nscolor color = GetBackgroundColor(style, mMenuText); + if (NS_GET_A(color)) { + return color; + } + // Some themes only style menupopups with the backdrop pseudo-class. Since a + // context / popup menu always seems to match that, try that before giving + // up. + color = GetBackgroundColor(style, mMenuText, GTK_STATE_FLAG_BACKDROP); + if (NS_GET_A(color)) { + return color; + } + // If we get here we couldn't figure out the right color to use. Rather than + // falling back to transparent, fall back to the window background. + NS_WARNING( + "Couldn't find menu background color, falling back to window " + "background"); + return mMozWindowBackground; + }(); + + style = GetStyleContext(MOZ_GTK_MENUITEM); + gtk_style_context_get_color(style, GTK_STATE_FLAG_PRELIGHT, &color); + mMenuHoverText = GDK_RGBA_TO_NS_RGBA(color); + mMenuHover = NS_ComposeColors( + mMenuBackground, + GetBackgroundColor(style, mMenuHoverText, GTK_STATE_FLAG_PRELIGHT, + mMenuBackground)); + + 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); + } + + // Default selected item color is the selection background / foreground + // colors, but we prefer named colors, as those are more general purpose + // than the actual selection style, which might e.g. be too-transparent. + // + // NOTE(emilio): It's unclear which one of the theme_selected_* or the + // selected_* pairs should we prefer, in all themes that define both that + // I've found, they're always the same. + if (!GetNamedColorPair(style, "selected_bg_color", "selected_fg_color", + &mSelectedItem, &mSelectedItemText) && + !GetNamedColorPair(style, "theme_selected_bg_color", + "theme_selected_fg_color", &mSelectedItem, + &mSelectedItemText)) { + mSelectedItem = mTextSelectedBackground; + mSelectedItemText = mTextSelectedText; + } + + EnsureColorPairIsOpaque(mSelectedItem, mSelectedItemText); + + // In a similar fashion, default accent color is the selected item/text + // pair, but we also prefer named colors, if available. + // + // accent_{bg,fg}_color is not _really_ a gtk3 thing (it's a gtk4 thing), + // but if gtk 3 themes want to specify these we let them, see: + // + // https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/named-colors.html#accent-colors + if (!GetNamedColorPair(style, "accent_bg_color", "accent_fg_color", + &mAccentColor, &mAccentColorText)) { + mAccentColor = mSelectedItem; + mAccentColorText = mSelectedItemText; + } + + EnsureColorPairIsOpaque(mAccentColor, mAccentColorText); + PreferDarkerBackground(mAccentColor, mAccentColorText); + } + + // 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); + if (!NS_GET_A(mButtonHoverFace)) { + mButtonHoverFace = mMozWindowBackground; + } + + // 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); + + // 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); + } + + // 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); + + gtk_style_context_get_color(style, GTK_STATE_FLAG_VISITED, &color); + mNativeVisitedHyperLinkText = 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); + + GetSystemFontInfo(gtk_widget_get_style_context(entry), &mFieldFontName, + &mFieldFontStyle); + + gtk_widget_destroy(window); + g_object_unref(labelWidget); + + if (LOGLNF_ENABLED()) { + LOGLNF("Initialized theme %s (%d)\n", mName.get(), mPreferDarkTheme); + for (auto id : MakeEnumeratedRange(ColorID::End)) { + nscolor color; + nsresult rv = GetColor(id, color); + LOGLNF(" * color %d: pref=%s success=%d value=%x\n", int(id), + GetColorPrefName(id), NS_SUCCEEDED(rv), + NS_SUCCEEDED(rv) ? color : 0); + } + LOGLNF(" * titlebar-radius: %d\n", mTitlebarRadius); + } +} + +// virtual +char16_t nsLookAndFeel::GetPasswordCharacterImpl() { + EnsureInit(); + return mSystemTheme.mInvisibleCharacter; +} + +bool nsLookAndFeel::GetEchoPasswordImpl() { return false; } + +bool nsLookAndFeel::GetDefaultDrawInTitlebar() { return sCSDAvailable; } + +void nsLookAndFeel::GetThemeInfo(nsACString& aInfo) { + aInfo.Append(mSystemTheme.mName); + aInfo.Append(" / "); + aInfo.Append(mAltTheme.mName); +} + +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 = !ShouldHonorThemeScrollbarColors(); + 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); +} + +#undef LOGLNF +#undef LOGLNF_ENABLED diff --git a/widget/gtk/nsLookAndFeel.h b/widget/gtk/nsLookAndFeel.h new file mode 100644 index 0000000000..b62c0bb468 --- /dev/null +++ b/widget/gtk/nsLookAndFeel.h @@ -0,0 +1,180 @@ +/* -*- 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 __nsLookAndFeel +#define __nsLookAndFeel + +#include "X11UndefineNone.h" +#include "nsXPLookAndFeel.h" +#include "nsCOMPtr.h" +#include "gfxFont.h" + +enum WidgetNodeType : int; +struct _GtkStyle; +typedef struct _GDBusProxy GDBusProxy; + +class nsLookAndFeel final : public nsXPLookAndFeel { + public: + nsLookAndFeel(); + 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, ColorScheme, nscolor& aResult) override; + bool NativeGetFont(FontID aID, nsString& aFontName, + gfxFontStyle& aFontStyle) override; + + char16_t GetPasswordCharacterImpl() override; + bool GetEchoPasswordImpl() override; + + bool GetDefaultDrawInTitlebar() override; + + void GetThemeInfo(nsACString&) override; + + static const nscolor kBlack = NS_RGB(0, 0, 0); + static const nscolor kWhite = NS_RGB(255, 255, 255); + void OnColorSchemeSettingChanged(); + + protected: + static bool WidgetUsesImage(WidgetNodeType aNodeType); + void RecordLookAndFeelSpecificTelemetry() override; + static bool ShouldHonorThemeScrollbarColors(); + mozilla::Maybe<ColorScheme> ComputeColorSchemeSetting(); + + // We use up to two themes (one light, one dark), which might have different + // sets of fonts and colors. + struct PerThemeData { + nsCString mName; + + bool mIsDark = false; + bool mHighContrast = false; + bool mPreferDarkTheme = false; + + // NOTE(emilio): This is unused, but if we need to we can use it to override + // system colors with standins like we do for the non-native theme. + bool mCompatibleWithHTMLLightColors = false; + + // 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 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 mNativeVisitedHyperLinkText = 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 mMozCellHighlightBackground = kWhite; + nscolor mMozCellHighlightText = kBlack; + nscolor mTextSelectedText = kBlack; + nscolor mTextSelectedBackground = kWhite; + nscolor mAccentColor = kWhite; + nscolor mAccentColorText = kWhite; + nscolor mSelectedItem = kWhite; + nscolor mSelectedItemText = kBlack; + nscolor mMozColHeaderText = kBlack; + nscolor mMozColHeaderHoverText = kBlack; + nscolor mTitlebarText = kBlack; + nscolor mTitlebarBackground = kWhite; + nscolor mTitlebarInactiveText = kBlack; + nscolor mTitlebarInactiveBackground = kWhite; + nscolor mThemedScrollbar = kWhite; + nscolor mThemedScrollbarInactive = kWhite; + nscolor mThemedScrollbarThumb = kBlack; + nscolor mThemedScrollbarThumbHover = kBlack; + nscolor mThemedScrollbarThumbActive = kBlack; + nscolor mThemedScrollbarThumbInactive = kBlack; + + float mCaretRatio = 0.0f; + int32_t mTitlebarRadius = 0; + char16_t mInvisibleCharacter = 0; + bool mMenuSupportsDrag = false; + + void Init(); + nsresult GetColor(ColorID, nscolor&) const; + bool GetFont(FontID, nsString& aFontName, gfxFontStyle&) const; + void InitCellHighlightColors(); + }; + + PerThemeData mSystemTheme; + + // If the system theme is light, a dark theme. Otherwise, a light theme. The + // alternative theme to the current one is preferred, but otherwise we fall + // back to Adwaita / Adwaita Dark, respectively. + PerThemeData mAltTheme; + + const PerThemeData& LightTheme() const { + return mSystemTheme.mIsDark ? mAltTheme : mSystemTheme; + } + + const PerThemeData& DarkTheme() const { + return mSystemTheme.mIsDark ? mSystemTheme : mAltTheme; + } + + const PerThemeData& EffectiveTheme() const { + return mSystemThemeOverridden ? mAltTheme : mSystemTheme; + } + + RefPtr<GDBusProxy> mDBusSettingsProxy; + mozilla::Maybe<ColorScheme> mColorSchemePreference; + int32_t mCaretBlinkTime = 0; + int32_t mCaretBlinkCount = -1; + bool mCSDMaximizeButton = false; + bool mCSDMinimizeButton = false; + bool mCSDCloseButton = false; + bool mCSDReversedPlacement = false; + bool mPrefersReducedMotion = false; + bool mInitialized = false; + bool mSystemThemeOverridden = false; + int32_t mCSDMaximizeButtonPosition = 0; + int32_t mCSDMinimizeButtonPosition = 0; + int32_t mCSDCloseButtonPosition = 0; + + void EnsureInit() { + if (mInitialized) { + return; + } + Initialize(); + } + + void Initialize(); + + void RestoreSystemTheme(); + void InitializeGlobalSettings(); + void ConfigureAndInitializeAltTheme(); + void ConfigureFinalEffectiveTheme(); +}; + +#endif diff --git a/widget/gtk/nsNativeThemeGTK.cpp b/widget/gtk/nsNativeThemeGTK.cpp new file mode 100644 index 0000000000..570de37ab6 --- /dev/null +++ b/widget/gtk/nsNativeThemeGTK.cpp @@ -0,0 +1,1466 @@ +/* -*- 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 "nsPresContext.h" +#include "nsStyleConsts.h" +#include "gtkdrawing.h" +#include "ScreenHelperGTK.h" +#include "WidgetUtilsGtk.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 "nsXULPopupManager.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/Services.h" + +#include <gdk/gdkprivate.h> +#include <gtk/gtk.h> + +#include "gfxContext.h" +#include "mozilla/dom/XULButtonElement.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 "Theme.h" + +#ifdef MOZ_X11 +# ifdef CAIRO_HAS_XLIB_SURFACE +# include "cairo-xlib.h" +# endif +#endif + +#include <algorithm> +#include <dlfcn.h> + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; + +static int gLastGdkError; + +// Return widget scale factor of the monitor where the window is located by the +// most part. We intentionally honor the text scale factor here in order to +// have consistent scaling with other UI elements. +static inline CSSToLayoutDeviceScale GetWidgetScaleFactor(nsIFrame* aFrame) { + return aFrame->PresContext()->CSSToDevPixelScale(); +} + +nsNativeThemeGTK::nsNativeThemeGTK() : Theme(ScrollbarStyle()) { + if (moz_gtk_init() != MOZ_GTK_SUCCESS) { + memset(mDisabledWidgetTypes, 0xff, sizeof(mDisabledWidgetTypes)); + return; + } + + ThemeChanged(); +} + +nsNativeThemeGTK::~nsNativeThemeGTK() { moz_gtk_shutdown(); } + +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>(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))); +} + +bool nsNativeThemeGTK::GetGtkWidgetAndState(StyleAppearance aAppearance, + nsIFrame* aFrame, + WidgetNodeType& aGtkWidgetType, + GtkWidgetState* aState, + gint* aWidgetFlags) { + if (aWidgetFlags) { + *aWidgetFlags = 0; + } + + ElementState elementState = GetContentState(aFrame, aAppearance); + if (aState) { + memset(aState, 0, sizeof(GtkWidgetState)); + + // For XUL checkboxes and radio buttons, the state of the parent + // determines our state. + if (aWidgetFlags) { + if (elementState.HasState(ElementState::CHECKED)) { + *aWidgetFlags |= MOZ_GTK_WIDGET_CHECKED; + } + if (elementState.HasState(ElementState::INDETERMINATE)) { + *aWidgetFlags |= MOZ_GTK_WIDGET_INCONSISTENT; + } + } + + aState->disabled = + elementState.HasState(ElementState::DISABLED) || IsReadOnly(aFrame); + aState->active = elementState.HasState(ElementState::ACTIVE); + aState->focused = elementState.HasState(ElementState::FOCUS); + aState->inHover = elementState.HasState(ElementState::HOVER); + aState->isDefault = IsDefaultButton(aFrame); + aState->canDefault = FALSE; // XXX fix me + + 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) { + if (nsTreeBodyFrame* treeBodyFrame = do_QueryFrame(aFrame)) { + const mozilla::AtomArray& atoms = + treeBodyFrame->GetPropertyArrayForCurrentDrawingItem(); + aState->selected = atoms.Contains(nsGkAtoms::selected); + aState->inHover = atoms.Contains(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. + aState->focused = elementState.HasState(ElementState::FOCUSRING); + if (aAppearance == StyleAppearance::Radio || + aAppearance == StyleAppearance::Checkbox) { + // In XUL, checkboxes and radios shouldn't have focus rings, their + // labels do + aState->focused = FALSE; + } + + // 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::Menuarrow) { + aState->inHover = CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + aState->active = FALSE; + } + + // 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); + } + } + + 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::Checkbox: + case StyleAppearance::Radio: + aGtkWidgetType = (aAppearance == StyleAppearance::Radio) + ? MOZ_GTK_RADIOBUTTON + : MOZ_GTK_CHECKBUTTON; + 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::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::ProgressBar: + aGtkWidgetType = MOZ_GTK_PROGRESSBAR; + break; + case StyleAppearance::Progresschunk: { + nsIFrame* stateFrame = aFrame->GetParent(); + ElementState elementState = GetContentState(stateFrame, aAppearance); + + aGtkWidgetType = elementState.HasState(ElementState::INDETERMINATE) + ? 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::Menuarrow: + aGtkWidgetType = MOZ_GTK_MENUARROW; + 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, double aScaleFactor, + bool aSnapped, const Point& aDrawOrigin, + const nsIntSize& aDrawSize, + GdkRectangle& aGDKRect, + nsITheme::Transparency aTransparency) { + static auto sCairoSurfaceSetDeviceScalePtr = + (void (*)(cairo_surface_t*, double, double))dlsym( + RTLD_DEFAULT, "cairo_surface_set_device_scale"); + const bool useHiDPIWidgets = + aScaleFactor != 1.0 && sCairoSurfaceSetDeviceScalePtr; + + 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); + + Size 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 (GdkIsX11Display()) { + // If using a Cairo xlib surface, then try to reuse it. + BorrowedXlibDrawable borrow(aDrawTarget); + if (Drawable drawable = borrow.GetDrawable()) { + nsIntSize size = borrow.GetSize(); + cairo_surface_t* surf = cairo_xlib_surface_create( + borrow.GetDisplay(), drawable, 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); + } + } + } +} + +CSSIntMargin nsNativeThemeGTK::GetExtraSizeForWidget( + nsIFrame* aFrame, StyleAppearance aAppearance) { + CSSIntMargin extra; + // 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::Button: { + if (IsDefaultButton(aFrame)) { + // Some themes draw a default indicator outside the widget, + // include that in overflow + moz_gtk_button_get_default_overflow(&extra.top, &extra.left, + &extra.bottom, &extra.right); + break; + } + return {}; + } + default: + return {}; + } + return extra; +} + +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, + DrawOverflow aDrawOverflow) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect, + aDirtyRect, aDrawOverflow); + } + + 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); + + // 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. + // We prioritize the size when snapping in order to avoid distorting widgets + // that should be square, which can occur if edges are snapped independently. + bool snapped = ctx->UserToDevicePixelSnapped( + rect, gfxContext::SnapOption::PrioritizeSize); + 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). + LayoutDeviceIntRect widgetRect(0, 0, NS_lround(rect.Width()), + NS_lround(rect.Height())); + + // This is the rectangle that will actually be drawn, in gdk pixels + LayoutDeviceIntRect drawingRect( + int32_t(dirtyRect.X()), int32_t(dirtyRect.Y()), + int32_t(dirtyRect.Width()), int32_t(dirtyRect.Height())); + if (widgetRect.IsEmpty() || + !drawingRect.IntersectRect(widgetRect, 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. + auto scaleFactor = GetWidgetScaleFactor(aFrame); + LayoutDeviceIntRect gdkDevRect(-drawingRect.TopLeft(), widgetRect.Size()); + + auto gdkCssRect = CSSIntRect::RoundIn(gdkDevRect / scaleFactor); + GdkRectangle gdk_rect = {gdkCssRect.x, gdkCssRect.y, gdkCssRect.width, + gdkCssRect.height}; + + // Save actual widget scale to GtkWidgetState as we don't provide + // the frame to gtk3drawing routines. + state.image_scale = std::ceil(scaleFactor.scale); + + // translate everything so (0,0) is the top left of the drawingRect + gfxPoint origin = rect.TopLeft() + drawingRect.TopLeft().ToUnknownPoint(); + + DrawThemeWithCairo(ctx, aContext->GetDrawTarget(), state, gtkWidgetType, + flags, direction, scaleFactor.scale, snapped, + ToPoint(origin), drawingRect.Size().ToUnknownSize(), + gdk_rect, transparency); + + if (!safeState) { + // gdk_flush() call from expose event crashes Gtk+ on Wayland + // (Gnome BZ #773307) + if (GdkIsX11Display()) { + 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) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::CreateWebRenderCommandsForWidget( + aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect); + } + 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, + CSSIntMargin* 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; + } +} + +CSSIntMargin nsNativeThemeGTK::GetCachedWidgetBorder( + nsIFrame* aFrame, StyleAppearance aAppearance, + GtkTextDirection aDirection) { + CSSIntMargin result; + + 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) { + result = mBorderCache[gtkWidgetType]; + } else { + moz_gtk_get_widget_border(gtkWidgetType, &result.left, &result.top, + &result.right, &result.bottom, aDirection); + if (gtkWidgetType != MOZ_GTK_DROPDOWN) { // depends on aDirection + mBorderCacheValid[cacheIndex] |= cacheBit; + mBorderCache[gtkWidgetType] = result; + } + } + } + FixupForVerticalWritingMode(aFrame->GetWritingMode(), &result); + return result; +} + +LayoutDeviceIntMargin nsNativeThemeGTK::GetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { + CSSIntMargin result; + GtkTextDirection direction = GetTextDirection(aFrame); + switch (aAppearance) { + 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 {}; + } + moz_gtk_get_tab_border(&result.left, &result.top, &result.right, + &result.bottom, direction, (GtkTabFlags)flags, + gtkWidgetType); + } break; + default: { + result = GetCachedWidgetBorder(aFrame, aAppearance, direction); + } + } + + return (CSSMargin(result) * GetWidgetScaleFactor(aFrame)).Rounded(); +} + +bool nsNativeThemeGTK::GetWidgetPadding(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) { + switch (aAppearance) { + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Tooltip: + 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; + default: + break; + } + + return false; +} + +bool nsNativeThemeGTK::GetWidgetOverflow(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::GetWidgetOverflow(aContext, aFrame, aAppearance, + aOverflowRect); + } + auto overflow = GetExtraSizeForWidget(aFrame, aAppearance); + if (overflow == CSSIntMargin()) { + return false; + } + aOverflowRect->Inflate(CSSIntMargin::ToAppUnits(overflow)); + return true; +} + +auto nsNativeThemeGTK::IsWidgetNonNative(nsIFrame* aFrame, + StyleAppearance aAppearance) + -> NonNative { + if (IsWidgetScrollbarPart(aAppearance) || + aAppearance == StyleAppearance::FocusOutline) { + return NonNative::Always; + } + + // If the current GTK theme color scheme matches our color-scheme, then we + // can draw a native widget. + if (LookAndFeel::ColorSchemeForFrame(aFrame) == + LookAndFeel::ColorSchemeForChrome()) { + return NonNative::No; + } + + // As an special-case, for tooltips, we check if the tooltip color is the + // same between the light and dark themes. If so we can get away with drawing + // the native widget, see bug 1817396. + if (aAppearance == StyleAppearance::Tooltip) { + auto darkColor = + LookAndFeel::Color(StyleSystemColor::Infotext, ColorScheme::Dark, + LookAndFeel::UseStandins::No); + auto lightColor = + LookAndFeel::Color(StyleSystemColor::Infotext, ColorScheme::Light, + LookAndFeel::UseStandins::No); + if (darkColor == lightColor) { + return NonNative::No; + } + } + + // If the non-native theme doesn't support the widget then oh well... + if (!Theme::ThemeSupportsWidget(aFrame->PresContext(), aFrame, aAppearance)) { + return NonNative::No; + } + + return NonNative::BecauseColorMismatch; +} + +LayoutDeviceIntSize nsNativeThemeGTK::GetMinimumWidgetSize( + nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetNonNative(aFrame, aAppearance) == NonNative::Always) { + return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance); + } + + CSSIntSize result; + switch (aAppearance) { + case StyleAppearance::Splitter: { + if (IsHorizontal(aFrame)) { + moz_gtk_splitter_get_metrics(GTK_ORIENTATION_HORIZONTAL, &result.width); + } else { + moz_gtk_splitter_get_metrics(GTK_ORIENTATION_VERTICAL, &result.height); + } + } break; + case StyleAppearance::RangeThumb: { + if (IsRangeHorizontal(aFrame)) { + moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_HORIZONTAL, + &result.width, &result.height); + } else { + moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_VERTICAL, &result.width, + &result.width); + } + } break; + case StyleAppearance::TabScrollArrowBack: + case StyleAppearance::TabScrollArrowForward: { + moz_gtk_get_tab_scroll_arrow_size(&result.width, &result.height); + } break; + case StyleAppearance::MozMenulistArrowButton: { + moz_gtk_get_combo_box_entry_button_size(&result.width, &result.height); + } break; + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: { + const ToggleGTKMetrics* metrics = GetToggleMetrics( + aAppearance == StyleAppearance::Radio ? MOZ_GTK_RADIOBUTTON + : MOZ_GTK_CHECKBUTTON); + result.width = metrics->minSizeWithBorder.width; + result.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, &result.width, + &result.height); + } break; + case StyleAppearance::MozWindowButtonClose: { + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_CLOSE); + result.width = metrics->minSizeWithBorderMargin.width; + result.height = metrics->minSizeWithBorderMargin.height; + break; + } + case StyleAppearance::MozWindowButtonMinimize: { + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE); + result.width = metrics->minSizeWithBorderMargin.width; + result.height = metrics->minSizeWithBorderMargin.height; + break; + } + case StyleAppearance::MozWindowButtonMaximize: + case StyleAppearance::MozWindowButtonRestore: { + const ToolbarButtonGTKMetrics* metrics = + GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE); + result.width = metrics->minSizeWithBorderMargin.width; + result.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, &result.width, &result.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. + + CSSIntMargin border = + GetCachedWidgetBorder(aFrame, aAppearance, GetTextDirection(aFrame)); + result.width += border.LeftRight(); + result.height += border.TopBottom(); + } 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::MozField, 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()) { + result.width = height; + } else { + result.height = height; + } + } break; + case StyleAppearance::Separator: { + moz_gtk_get_toolbar_separator_width(&result.width); + } break; + case StyleAppearance::Spinner: + // hard code these sizes + result.width = 14; + result.height = 26; + break; + case StyleAppearance::Treeheadersortarrow: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + // hard code these sizes + result.width = 14; + result.height = 13; + break; + case StyleAppearance::Treetwisty: + case StyleAppearance::Treetwistyopen: { + gint expander_size; + moz_gtk_get_treeview_expander_size(&expander_size); + result.width = result.height = expander_size; + } break; + default: + break; + } + + return LayoutDeviceIntSize::Round(CSSSize(result) * + GetWidgetScaleFactor(aFrame)); +} + +NS_IMETHODIMP +nsNativeThemeGTK::WidgetStateChanged(nsIFrame* aFrame, + StyleAppearance aAppearance, + nsAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) { + *aShouldRepaint = false; + + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::WidgetStateChanged(aFrame, aAppearance, aAttribute, + aShouldRepaint, aOldValue); + } + + // Some widget types just never change state. + if (aAppearance == StyleAppearance::Toolbox || + aAppearance == StyleAppearance::Toolbar || + aAppearance == StyleAppearance::Progresschunk || + aAppearance == StyleAppearance::ProgressBar || + aAppearance == StyleAppearance::Menubar || + aAppearance == StyleAppearance::Tooltip) { + 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; + } + + // 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; + return NS_OK; + } + + // 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; + } + 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 (IsWidgetNonNative(aFrame, aAppearance) == NonNative::Always) { + return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance); + } + + 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::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::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::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::Menuarrow: + case StyleAppearance::Radiomenuitem: + case StyleAppearance::Splitter: + 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); + + 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(nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::ThemeDrawsFocusForWidget(aFrame, 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) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::GetWidgetTransparency(aFrame, aAppearance); + } + + switch (aAppearance) { + // Tooltips use gtk_paint_flat_box() on Gtk2 + // but are shaped on Gtk3 + case StyleAppearance::Tooltip: + return eTransparent; + default: + return eUnknownTransparency; + } +} + +already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() { + if (gfxPlatform::IsHeadless()) { + return do_AddRef(new Theme(Theme::ScrollbarStyle())); + } + return do_AddRef(new nsNativeThemeGTK()); +} diff --git a/widget/gtk/nsNativeThemeGTK.h b/widget/gtk/nsNativeThemeGTK.h new file mode 100644 index 0000000000..e5ef11c295 --- /dev/null +++ b/widget/gtk/nsNativeThemeGTK.h @@ -0,0 +1,118 @@ +/* -*- 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 "Theme.h" + +#include <gtk/gtk.h> +#include "gtkdrawing.h" + +class nsNativeThemeGTK final : public mozilla::widget::Theme { + using Theme = mozilla::widget::Theme; + + public: + // The nsITheme interface. + NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, const nsRect& aDirtyRect, + DrawOverflow) override; + + bool CreateWebRenderCommandsForWidget( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsIFrame*, + StyleAppearance, 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; + + bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) override; + + // Whether we draw a non-native widget. + // + // We always draw scrollbars as non-native so that all of Firefox has + // consistent scrollbar styles both in chrome and content (plus, the + // non-native scrollbars support scrollbar-width, auto-darkening...). + // + // We draw other widgets as non-native when their color-scheme doesn't match + // the current GTK theme's color-scheme. We do that because frequently + // switching GTK themes at runtime is prohibitively expensive. In that case + // (`BecauseColorMismatch`) we don't call into the non-native theme for sizing + // information (GetWidgetPadding/Border and GetMinimumWidgetSize), to avoid + // subtle sizing changes. The non-native theme can basically draw at any size, + // so we prefer to have consistent sizing information. + enum class NonNative { No, Always, BecauseColorMismatch }; + NonNative IsWidgetNonNative(nsIFrame*, StyleAppearance); + + mozilla::LayoutDeviceIntSize GetMinimumWidgetSize( + nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) 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; + + bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override; + + bool ThemeNeedsComboboxDropmarker() override; + Transparency GetWidgetTransparency(nsIFrame*, StyleAppearance) 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); + mozilla::CSSIntMargin GetExtraSizeForWidget(nsIFrame*, StyleAppearance); + 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. + mozilla::CSSIntMargin GetCachedWidgetBorder(nsIFrame* aFrame, + StyleAppearance aAppearance, + GtkTextDirection aDirection); + uint8_t mBorderCacheValid[(MOZ_GTK_WIDGET_NODE_COUNT + 7) / 8]; + mozilla::CSSIntMargin 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..3885a94c24 --- /dev/null +++ b/widget/gtk/nsPrintDialogGTK.cpp @@ -0,0 +1,621 @@ +/* -*- 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 "nsPrintfCString.h" +#include "nsIGIOService.h" +#include "nsServiceManagerUtils.h" +#include "WidgetUtils.h" +#include "WidgetUtilsGtk.h" +#include "nsIObserverService.h" + +// for gdk_x11_window_get_xid +#include <gdk/gdk.h> +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +#endif +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <gio/gunixfdlist.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, bool aHaveSelection, + 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, + bool aHaveSelection, + 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. + if (gtk_major_version > 2 || + (gtk_major_version == 2 && gtk_minor_version >= 18)) { + useNativeSelection = true; + g_object_set(dialog, "support-selection", TRUE, "has-selection", + aHaveSelection, "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, aHaveSelection); + 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"); + 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(); + + // Set our custom fields: + + 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()); + + // Temporarily set the pages-per-sheet on the GtkPrintSettings: + int32_t pagesPerSide; + aNSSettings->GetNumPagesPerSheet(&pagesPerSide); + gtk_print_settings_set_number_up(settings, pagesPerSide); + + 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->SetOutputDestination( + nsIPrintSettings::kOutputDestinationPrinter); + + 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))); + + // Move any pages-per-sheet value from the GtkPrintSettings to our + // nsIPrintSettings. (We handle pages-per-sheet internally and don't want + // the native Linux printing code to doubly apply that value!) + int32_t pagesPerSide = gtk_print_settings_get_number_up(settings); + gtk_print_settings_set_number_up(settings, 1); + aNSSettings->SetNumPagesPerSheet(pagesPerSide); + + // 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; } + +NS_IMETHODIMP +nsPrintDialogServiceGTK::ShowPrintDialog(mozIDOMWindowProxy* aParent, + bool aHaveSelection, + nsIPrintSettings* aSettings) { + MOZ_ASSERT(aParent, "aParent must not be null"); + MOZ_ASSERT(aSettings, "aSettings must not be null"); + + nsPrintDialogWidgetGTK printDialog(nsPIDOMWindowOuter::From(aParent), + aHaveSelection, 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::ShowPageSetupDialog(mozIDOMWindowProxy* 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(nsPIDOMWindowOuter::From(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->MaybeSavePrintSettingsToPrefs( + aNSSettings, 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..4388962858 --- /dev/null +++ b/widget/gtk/nsPrintDialogGTK.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef 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 final : public nsIPrintDialogService { + virtual ~nsPrintDialogServiceGTK(); + + public: + nsPrintDialogServiceGTK(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIPRINTDIALOGSERVICE +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintDialogServiceGTK, + NS_IPRINTDIALOGSERVICE_IID) + +#endif diff --git a/widget/gtk/nsPrintSettingsGTK.cpp b/widget/gtk/nsPrintSettingsGTK.cpp new file mode 100644 index 0000000000..6ca66a0fcf --- /dev/null +++ b/widget/gtk/nsPrintSettingsGTK.cpp @@ -0,0 +1,677 @@ +/* -*- 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> + +// These constants are the the strings that GTK expects as key-value pairs for +// setting CUPS duplex modes. These are not universal to all CUPS systems, which +// is why they are local to this file. +static constexpr gchar kCupsDuplex[] = "cups-Duplex"; +static constexpr gchar kCupsDuplexNone[] = "None"; +static constexpr gchar kCupsDuplexNoTumble[] = "DuplexNoTumble"; +static constexpr gchar kCupsDuplexTumble[] = "DuplexTumble"; + +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)) { + format = nsIPrintSettings::kOutputFormatPDF; + } + + *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::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) { + *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) { + NS_ENSURE_ARG_POINTER(aDuplex); + + // Default to DuplexNone. + *aDuplex = kDuplexNone; + + if (!gtk_print_settings_has_key(mPrintSettings, GTK_PRINT_SETTINGS_DUPLEX)) { + return NS_OK; + } + + switch (gtk_print_settings_get_duplex(mPrintSettings)) { + case GTK_PRINT_DUPLEX_SIMPLEX: + *aDuplex = kDuplexNone; + break; + case GTK_PRINT_DUPLEX_HORIZONTAL: + *aDuplex = kDuplexFlipOnLongEdge; + break; + case GTK_PRINT_DUPLEX_VERTICAL: + *aDuplex = kDuplexFlipOnShortEdge; + break; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsGTK::SetDuplex(int32_t aDuplex) { + uint32_t duplex = static_cast<uint32_t>(aDuplex); + MOZ_ASSERT(duplex <= kDuplexFlipOnShortEdge, + "value is out of bounds for duplex enum"); + + // 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. + switch (duplex) { + case kDuplexNone: + gtk_print_settings_set(mPrintSettings, kCupsDuplex, kCupsDuplexNone); + gtk_print_settings_set_duplex(mPrintSettings, GTK_PRINT_DUPLEX_SIMPLEX); + break; + case kDuplexFlipOnLongEdge: + gtk_print_settings_set(mPrintSettings, kCupsDuplex, kCupsDuplexNoTumble); + gtk_print_settings_set_duplex(mPrintSettings, + GTK_PRINT_DUPLEX_HORIZONTAL); + break; + case kDuplexFlipOnShortEdge: + gtk_print_settings_set(mPrintSettings, kCupsDuplex, kCupsDuplexTumble); + gtk_print_settings_set_duplex(mPrintSettings, GTK_PRINT_DUPLEX_VERTICAL); + break; + } + + return NS_OK; +} diff --git a/widget/gtk/nsPrintSettingsGTK.h b/widget/gtk/nsPrintSettingsGTK.h new file mode 100644 index 0000000000..c2d97a6208 --- /dev/null +++ b/widget/gtk/nsPrintSettingsGTK.h @@ -0,0 +1,147 @@ +/* -*- 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); + + static nsPrintSettingsGTK* From(nsIPrintSettings* aPrintSettings) { + return static_cast<nsPrintSettingsGTK*>(aPrintSettings); + } + + // 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 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..9d0dfc9379 --- /dev/null +++ b/widget/gtk/nsPrintSettingsServiceGTK.cpp @@ -0,0 +1,80 @@ +/* -*- 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 "mozilla/embedding/PPrintingTypes.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/nsShmImage.cpp b/widget/gtk/nsShmImage.cpp new file mode 100644 index 0000000000..7208b7075f --- /dev/null +++ b/widget/gtk/nsShmImage.cpp @@ -0,0 +1,326 @@ +/* -*- 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 "nsShmImage.h" + +#ifdef MOZ_HAVE_SHMIMAGE +# include "mozilla/X11Util.h" +# include "mozilla/gfx/gfxVars.h" +# include "mozilla/ipc/SharedMemory.h" +# include "gfxPlatform.h" +# include "nsPrintfCString.h" +# include "nsTArray.h" + +# include <dlfcn.h> +# include <errno.h> +# include <string.h> +# include <sys/ipc.h> +# include <sys/shm.h> + +extern "C" { +# include <X11/ImUtil.h> +} + +using namespace mozilla::ipc; +using namespace mozilla::gfx; + +nsShmImage::nsShmImage(Display* aDisplay, Drawable aWindow, Visual* aVisual, + unsigned int aDepth) + : mDisplay(aDisplay), + mConnection(XGetXCBConnection(aDisplay)), + mWindow(aWindow), + mVisual(aVisual), + mDepth(aDepth), + mFormat(mozilla::gfx::SurfaceFormat::UNKNOWN), + mSize(0, 0), + mStride(0), + mPixmap(XCB_NONE), + mGC(XCB_NONE), + mRequestPending(false), + mShmSeg(XCB_NONE), + mShmId(-1), + mShmAddr(nullptr) { + mozilla::PodZero(&mSyncRequest); +} + +nsShmImage::~nsShmImage() { DestroyImage(); } + +// If XShm isn't available to our client, we'll try XShm once, fail, +// set this to false and then never try again. +static bool gShmAvailable = true; +bool nsShmImage::UseShm() { return gShmAvailable; } + +bool nsShmImage::CreateShmSegment() { + size_t size = SharedMemory::PageAlignedSize(mStride * mSize.height); + +# if defined(__OpenBSD__) && defined(MOZ_SANDBOX) + static mozilla::LazyLogModule sPledgeLog("SandboxPledge"); + MOZ_LOG(sPledgeLog, mozilla::LogLevel::Debug, + ("%s called when pledged, returning false\n", __func__)); + return false; +# endif + mShmId = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600); + if (mShmId == -1) { + return false; + } + mShmAddr = (uint8_t*)shmat(mShmId, nullptr, 0); + mShmSeg = xcb_generate_id(mConnection); + + // Mark the handle removed so that it will destroy the segment when unmapped. + shmctl(mShmId, IPC_RMID, nullptr); + + if (mShmAddr == (void*)-1) { + // Since mapping failed, the segment is already destroyed. + mShmId = -1; + + nsPrintfCString warning("shmat(): %s (%d)\n", strerror(errno), errno); + NS_WARNING(warning.get()); + return false; + } + +# ifdef DEBUG + struct shmid_ds info; + if (shmctl(mShmId, IPC_STAT, &info) < 0) { + return false; + } + + MOZ_ASSERT(size <= info.shm_segsz, "Segment doesn't have enough space!"); +# endif + + return true; +} + +void nsShmImage::DestroyShmSegment() { + if (mShmId != -1) { + shmdt(mShmAddr); + mShmId = -1; + } +} + +static bool gShmInitialized = false; +static bool gUseShmPixmaps = false; + +bool nsShmImage::InitExtension() { + if (gShmInitialized) { + return gShmAvailable; + } + + gShmInitialized = true; + + // Bugs 1397918, 1293474 - race condition in libxcb fixed upstream as of + // version 1.11. Since we can't query libxcb's version directly, the only + // other option is to check for symbols that were added after 1.11. + // xcb_discard_reply64 was added in 1.11.1, so check for existence of + // that to verify we are using a version of libxcb with the bug fixed. + // Otherwise, we can't risk using libxcb due to aforementioned crashes. + if (!dlsym(RTLD_DEFAULT, "xcb_discard_reply64")) { + gShmAvailable = false; + return false; + } + + const xcb_query_extension_reply_t* extReply; + extReply = xcb_get_extension_data(mConnection, &xcb_shm_id); + if (!extReply || !extReply->present) { + gShmAvailable = false; + return false; + } + + xcb_shm_query_version_reply_t* shmReply = xcb_shm_query_version_reply( + mConnection, xcb_shm_query_version(mConnection), nullptr); + + if (!shmReply) { + gShmAvailable = false; + return false; + } + + gUseShmPixmaps = shmReply->shared_pixmaps && + shmReply->pixmap_format == XCB_IMAGE_FORMAT_Z_PIXMAP; + + free(shmReply); + + return true; +} + +bool nsShmImage::CreateImage(const IntSize& aSize) { + MOZ_ASSERT(mConnection && mVisual); + + if (!InitExtension()) { + return false; + } + + mSize = aSize; + + BackendType backend = gfxVars::ContentBackend(); + + mFormat = SurfaceFormat::UNKNOWN; + switch (mDepth) { + case 32: + if (mVisual->red_mask == 0xff0000 && mVisual->green_mask == 0xff00 && + mVisual->blue_mask == 0xff) { + mFormat = SurfaceFormat::B8G8R8A8; + } + break; + case 24: + // Only support the BGRX layout, and report it as BGRA to the compositor. + // The alpha channel will be discarded when we put the image. + // Cairo/pixman lacks some fast paths for compositing BGRX onto BGRA, so + // just report it as BGRX directly in that case. + if (mVisual->red_mask == 0xff0000 && mVisual->green_mask == 0xff00 && + mVisual->blue_mask == 0xff) { + mFormat = backend == BackendType::CAIRO ? SurfaceFormat::B8G8R8X8 + : SurfaceFormat::B8G8R8A8; + } + break; + case 16: + if (mVisual->red_mask == 0xf800 && mVisual->green_mask == 0x07e0 && + mVisual->blue_mask == 0x1f) { + mFormat = SurfaceFormat::R5G6B5_UINT16; + } + break; + } + + if (mFormat == SurfaceFormat::UNKNOWN) { + NS_WARNING("Unsupported XShm Image format!"); + gShmAvailable = false; + return false; + } + + // Round up stride to the display's scanline pad (in bits) as XShm expects. + int scanlinePad = _XGetScanlinePad(mDisplay, mDepth); + int bitsPerPixel = _XGetBitsPerPixel(mDisplay, mDepth); + int bitsPerLine = + ((bitsPerPixel * aSize.width + scanlinePad - 1) / scanlinePad) * + scanlinePad; + mStride = bitsPerLine / 8; + + if (!CreateShmSegment()) { + DestroyImage(); + return false; + } + + xcb_generic_error_t* error; + xcb_void_cookie_t cookie; + + cookie = xcb_shm_attach_checked(mConnection, mShmSeg, mShmId, 0); + + if ((error = xcb_request_check(mConnection, cookie))) { + NS_WARNING("Failed to attach MIT-SHM segment."); + DestroyImage(); + gShmAvailable = false; + free(error); + return false; + } + + if (gUseShmPixmaps) { + mPixmap = xcb_generate_id(mConnection); + cookie = xcb_shm_create_pixmap_checked(mConnection, mPixmap, mWindow, + aSize.width, aSize.height, mDepth, + mShmSeg, 0); + + if ((error = xcb_request_check(mConnection, cookie))) { + // Disable shared pixmaps permanently if creation failed. + mPixmap = XCB_NONE; + gUseShmPixmaps = false; + free(error); + } + } + + return true; +} + +void nsShmImage::DestroyImage() { + if (mGC) { + xcb_free_gc(mConnection, mGC); + mGC = XCB_NONE; + } + if (mPixmap != XCB_NONE) { + xcb_free_pixmap(mConnection, mPixmap); + mPixmap = XCB_NONE; + } + if (mShmSeg != XCB_NONE) { + xcb_shm_detach_checked(mConnection, mShmSeg); + mShmSeg = XCB_NONE; + } + DestroyShmSegment(); + // Avoid leaking any pending reply. No real need to wait but CentOS 6 build + // machines don't have xcb_discard_reply(). + WaitIfPendingReply(); +} + +// Wait for any in-flight shm-affected requests to complete. +// Typically X clients would wait for a XShmCompletionEvent to be received, +// but this works as it's sent immediately after the request is sent. +void nsShmImage::WaitIfPendingReply() { + if (mRequestPending) { + xcb_get_input_focus_reply_t* reply = + xcb_get_input_focus_reply(mConnection, mSyncRequest, nullptr); + free(reply); + mRequestPending = false; + } +} + +already_AddRefed<DrawTarget> nsShmImage::CreateDrawTarget( + const mozilla::LayoutDeviceIntRegion& aRegion) { + WaitIfPendingReply(); + + // Due to bug 1205045, we must avoid making GTK calls off the main thread to + // query window size. Instead we just track the largest offset within the + // image we are drawing to and grow the image to accomodate it. Since usually + // the entire window is invalidated on the first paint to it, this should grow + // the image to the necessary size quickly without many intermediate + // reallocations. + IntRect bounds = aRegion.GetBounds().ToUnknownRect(); + IntSize size(bounds.XMost(), bounds.YMost()); + if (size.width > mSize.width || size.height > mSize.height) { + DestroyImage(); + if (!CreateImage(size)) { + return nullptr; + } + } + + return gfxPlatform::CreateDrawTargetForData( + reinterpret_cast<unsigned char*>(mShmAddr) + bounds.y * mStride + + bounds.x * BytesPerPixel(mFormat), + bounds.Size(), mStride, mFormat); +} + +void nsShmImage::Put(const mozilla::LayoutDeviceIntRegion& aRegion) { + AutoTArray<xcb_rectangle_t, 32> xrects; + xrects.SetCapacity(aRegion.GetNumRects()); + + for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) { + const mozilla::LayoutDeviceIntRect& r = iter.Get(); + xcb_rectangle_t xrect = {(short)r.x, (short)r.y, (unsigned short)r.width, + (unsigned short)r.height}; + xrects.AppendElement(xrect); + } + + if (!mGC) { + mGC = xcb_generate_id(mConnection); + xcb_create_gc(mConnection, mGC, mWindow, 0, nullptr); + } + + xcb_set_clip_rectangles(mConnection, XCB_CLIP_ORDERING_YX_BANDED, mGC, 0, 0, + xrects.Length(), xrects.Elements()); + + if (mPixmap != XCB_NONE) { + xcb_copy_area(mConnection, mPixmap, mWindow, mGC, 0, 0, 0, 0, mSize.width, + mSize.height); + } else { + xcb_shm_put_image(mConnection, mWindow, mGC, mSize.width, mSize.height, 0, + 0, mSize.width, mSize.height, 0, 0, mDepth, + XCB_IMAGE_FORMAT_Z_PIXMAP, 0, mShmSeg, 0); + } + + // Send a request that returns a response so that we don't have to start a + // sync in nsShmImage::CreateDrawTarget. + mSyncRequest = xcb_get_input_focus(mConnection); + mRequestPending = true; + + xcb_flush(mConnection); +} + +#endif // MOZ_HAVE_SHMIMAGE diff --git a/widget/gtk/nsShmImage.h b/widget/gtk/nsShmImage.h new file mode 100644 index 0000000000..2d92c39e0d --- /dev/null +++ b/widget/gtk/nsShmImage.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; 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_nsShmImage_h__ +#define __mozilla_widget_nsShmImage_h__ + +#if defined(MOZ_X11) +# define MOZ_HAVE_SHMIMAGE +#endif + +#ifdef MOZ_HAVE_SHMIMAGE + +# include "mozilla/gfx/2D.h" +# include "nsIWidget.h" +# include "Units.h" + +# include <X11/Xlib-xcb.h> +# include <xcb/shm.h> + +class nsShmImage { + // bug 1168843, compositor thread may create shared memory instances that are + // destroyed by main thread on shutdown, so this must use thread-safe RC to + // avoid hitting assertion + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsShmImage) + + public: + static bool UseShm(); + + already_AddRefed<mozilla::gfx::DrawTarget> CreateDrawTarget( + const mozilla::LayoutDeviceIntRegion& aRegion); + + void Put(const mozilla::LayoutDeviceIntRegion& aRegion); + + nsShmImage(Display* aDisplay, Drawable aWindow, Visual* aVisual, + unsigned int aDepth); + + private: + ~nsShmImage(); + + bool InitExtension(); + + bool CreateShmSegment(); + void DestroyShmSegment(); + + bool CreateImage(const mozilla::gfx::IntSize& aSize); + void DestroyImage(); + + void WaitIfPendingReply(); + + Display* mDisplay; + xcb_connection_t* mConnection; + Window mWindow; + Visual* mVisual; + unsigned int mDepth; + + mozilla::gfx::SurfaceFormat mFormat; + mozilla::gfx::IntSize mSize; + int mStride; + + xcb_pixmap_t mPixmap; + xcb_gcontext_t mGC; + xcb_get_input_focus_cookie_t mSyncRequest; + bool mRequestPending; + + xcb_shm_seg_t mShmSeg; + int mShmId; + uint8_t* mShmAddr; +}; + +#endif // MOZ_HAVE_SHMIMAGE + +#endif diff --git a/widget/gtk/nsSound.cpp b/widget/gtk/nsSound.cpp new file mode 100644 index 0000000000..f25a9e9274 --- /dev/null +++ b/widget/gtk/nsSound.cpp @@ -0,0 +1,397 @@ +/* -*- 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 "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..aaf13239e0 --- /dev/null +++ b/widget/gtk/nsToolkit.cpp @@ -0,0 +1,24 @@ +/* -*- 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; + +//------------------------------------------------------------------------------- +// 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..2b3215fd40 --- /dev/null +++ b/widget/gtk/nsUserIdleServiceGTK.cpp @@ -0,0 +1,125 @@ +/* -*- 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" +#include "WidgetUtilsGtk.h" + +using mozilla::LogLevel; + +static mozilla::LazyLogModule sIdleLog("nsIUserIdleService"); + +#ifdef MOZ_X11 +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 _XScreenSaverQueryExtension_fn _XSSQueryExtension = nullptr; +static _XScreenSaverAllocInfo_fn _XSSAllocInfo = nullptr; +static _XScreenSaverQueryInfo_fn _XSSQueryInfo = nullptr; +#endif +static bool sInitialized = false; + +static void Initialize() { +#ifdef MOZ_X11 + if (!mozilla::widget::GdkIsX11Display()) { + 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; +#endif +} + +#ifdef MOZ_X11 +nsUserIdleServiceGTK::nsUserIdleServiceGTK() : mXssInfo(nullptr) { +#else +nsUserIdleServiceGTK::nsUserIdleServiceGTK() { +#endif + Initialize(); +} + +nsUserIdleServiceGTK::~nsUserIdleServiceGTK() { +#ifdef MOZ_X11 + if (mXssInfo) XFree(mXssInfo); +#endif + +// 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) { +#ifdef MOZ_X11 + 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")); +#endif + return false; +} diff --git a/widget/gtk/nsUserIdleServiceGTK.h b/widget/gtk/nsUserIdleServiceGTK.h new file mode 100644 index 0000000000..a1dd010f3a --- /dev/null +++ b/widget/gtk/nsUserIdleServiceGTK.h @@ -0,0 +1,55 @@ +/* -*- 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" +#ifdef MOZ_X11 +# include <X11/Xlib.h> +# include <X11/Xutil.h> +# include <gdk/gdkx.h> +#endif + +#ifdef MOZ_X11 +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; +#endif + +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(); +#ifdef MOZ_X11 + XScreenSaverInfo* mXssInfo; +#endif + + protected: + nsUserIdleServiceGTK(); +}; + +#endif // nsUserIdleServiceGTK_h__ diff --git a/widget/gtk/nsWaylandDisplay.cpp b/widget/gtk/nsWaylandDisplay.cpp new file mode 100644 index 0000000000..0fc7433539 --- /dev/null +++ b/widget/gtk/nsWaylandDisplay.cpp @@ -0,0 +1,185 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWaylandDisplay.h" + +#include <dlfcn.h> + +#include "base/message_loop.h" // for MessageLoop +#include "base/task.h" // for NewRunnableMethod, etc +#include "mozilla/gfx/Logging.h" // for gfxCriticalNote +#include "mozilla/StaticMutex.h" +#include "mozilla/Array.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/Sprintf.h" +#include "WidgetUtilsGtk.h" +#include "nsGtkKeyUtils.h" + +namespace mozilla::widget { + +static nsWaylandDisplay* gWaylandDisplay; + +void WaylandDisplayRelease() { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), + "WaylandDisplay can be released in main thread only!"); + if (!gWaylandDisplay) { + NS_WARNING("WaylandDisplayRelease(): Wayland display is missing!"); + return; + } + delete gWaylandDisplay; + gWaylandDisplay = nullptr; +} + +wl_display* WaylandDisplayGetWLDisplay() { + GdkDisplay* disp = gdk_display_get_default(); + if (!GdkIsWaylandDisplay(disp)) { + return nullptr; + } + return gdk_wayland_display_get_wl_display(disp); +} + +nsWaylandDisplay* WaylandDisplayGet() { + if (!gWaylandDisplay) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), + "WaylandDisplay can be created in main thread only!"); + wl_display* waylandDisplay = WaylandDisplayGetWLDisplay(); + if (!waylandDisplay) { + return nullptr; + } + gWaylandDisplay = new nsWaylandDisplay(waylandDisplay); + } + return gWaylandDisplay; +} + +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::SetIdleInhibitManager( + zwp_idle_inhibit_manager_v1* aIdleInhibitManager) { + mIdleInhibitManager = aIdleInhibitManager; +} + +void nsWaylandDisplay::SetViewporter(wp_viewporter* aViewporter) { + mViewporter = aViewporter; +} + +void nsWaylandDisplay::SetRelativePointerManager( + zwp_relative_pointer_manager_v1* aRelativePointerManager) { + mRelativePointerManager = aRelativePointerManager; +} + +void nsWaylandDisplay::SetPointerConstraints( + zwp_pointer_constraints_v1* aPointerConstraints) { + mPointerConstraints = aPointerConstraints; +} + +void nsWaylandDisplay::SetDmabuf(zwp_linux_dmabuf_v1* aDmabuf) { + mDmabuf = aDmabuf; +} + +void nsWaylandDisplay::SetXdgActivation(xdg_activation_v1* aXdgActivation) { + mXdgActivation = aXdgActivation; +} + +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); + display->SetShm(shm); + } 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); + display->SetIdleInhibitManager(idle_inhibit_manager); + } else if (strcmp(interface, "zwp_relative_pointer_manager_v1") == 0) { + auto* relative_pointer_manager = + WaylandRegistryBind<zwp_relative_pointer_manager_v1>( + registry, id, &zwp_relative_pointer_manager_v1_interface, 1); + display->SetRelativePointerManager(relative_pointer_manager); + } else if (strcmp(interface, "zwp_pointer_constraints_v1") == 0) { + auto* pointer_constraints = WaylandRegistryBind<zwp_pointer_constraints_v1>( + registry, id, &zwp_pointer_constraints_v1_interface, 1); + display->SetPointerConstraints(pointer_constraints); + } 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); + display->SetCompositor(compositor); + } else if (strcmp(interface, "wl_subcompositor") == 0) { + auto* subcompositor = WaylandRegistryBind<wl_subcompositor>( + registry, id, &wl_subcompositor_interface, 1); + display->SetSubcompositor(subcompositor); + } else if (strcmp(interface, "wp_viewporter") == 0) { + auto* viewporter = WaylandRegistryBind<wp_viewporter>( + registry, id, &wp_viewporter_interface, 1); + display->SetViewporter(viewporter); + } else 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); + display->SetDmabuf(dmabuf); + } else if (strcmp(interface, "xdg_activation_v1") == 0) { + auto* activation = WaylandRegistryBind<xdg_activation_v1>( + registry, id, &xdg_activation_v1_interface, 1); + display->SetXdgActivation(activation); + // Install keyboard handlers for main thread only + } else if (strcmp(interface, "wl_seat") == 0) { + auto* seat = + WaylandRegistryBind<wl_seat>(registry, id, &wl_seat_interface, 1); + KeymapWrapper::SetSeat(seat, id); + } +} + +static void global_registry_remover(void* data, wl_registry* registry, + uint32_t id) { + KeymapWrapper::ClearSeat(id); +} + +static const struct wl_registry_listener registry_listener = { + global_registry_handler, global_registry_remover}; + +nsWaylandDisplay::~nsWaylandDisplay() {} + +static void WlLogHandler(const char* format, va_list args) { + char error[1000]; + VsprintfLiteral(error, format, args); + gfxCriticalNote << "Wayland protocol error: " << error; +} + +nsWaylandDisplay::nsWaylandDisplay(wl_display* aDisplay) + : mThreadId(PR_GetCurrentThread()), mDisplay(aDisplay) { + // GTK sets the log handler on display creation, thus we overwrite it here + // in a similar fashion + wl_log_set_handler_client(WlLogHandler); + + mRegistry = wl_display_get_registry(mDisplay); + wl_registry_add_listener(mRegistry, ®istry_listener, this); + wl_display_roundtrip(mDisplay); + wl_display_roundtrip(mDisplay); + + // Check we have critical Wayland interfaces. + // Missing ones indicates a compositor bug and we can't continue. + MOZ_DIAGNOSTIC_ASSERT(GetShm(), "We're missing shm interface!"); + MOZ_DIAGNOSTIC_ASSERT(GetCompositor(), "We're missing compositor interface!"); + MOZ_DIAGNOSTIC_ASSERT(GetSubcompositor(), + "We're missing subcompositor interface!"); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/nsWaylandDisplay.h b/widget/gtk/nsWaylandDisplay.h new file mode 100644 index 0000000000..fea0d459f9 --- /dev/null +++ b/widget/gtk/nsWaylandDisplay.h @@ -0,0 +1,111 @@ +/* -*- 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 __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/idle-inhibit-unstable-v1-client-protocol.h" +#include "mozilla/widget/relative-pointer-unstable-v1-client-protocol.h" +#include "mozilla/widget/pointer-constraints-unstable-v1-client-protocol.h" +#include "mozilla/widget/linux-dmabuf-unstable-v1-client-protocol.h" +#include "mozilla/widget/viewporter-client-protocol.h" +#include "mozilla/widget/xdg-activation-v1-client-protocol.h" +#include "mozilla/widget/xdg-output-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: + // Create nsWaylandDisplay object on top of native Wayland wl_display + // connection. + explicit nsWaylandDisplay(wl_display* aDisplay); + + wl_display* GetDisplay() { return mDisplay; }; + wl_compositor* GetCompositor() { return mCompositor; }; + wl_subcompositor* GetSubcompositor() { return mSubcompositor; }; + wl_shm* GetShm() { return mShm; }; + zwp_idle_inhibit_manager_v1* GetIdleInhibitManager() { + return mIdleInhibitManager; + } + wp_viewporter* GetViewporter() { return mViewporter; }; + zwp_relative_pointer_manager_v1* GetRelativePointerManager() { + return mRelativePointerManager; + } + zwp_pointer_constraints_v1* GetPointerConstraints() { + return mPointerConstraints; + } + zwp_linux_dmabuf_v1* GetDmabuf() { return mDmabuf; }; + xdg_activation_v1* GetXdgActivation() { return mXdgActivation; }; + + void SetShm(wl_shm* aShm); + void SetCompositor(wl_compositor* aCompositor); + void SetSubcompositor(wl_subcompositor* aSubcompositor); + void SetDataDeviceManager(wl_data_device_manager* aDataDeviceManager); + void SetIdleInhibitManager(zwp_idle_inhibit_manager_v1* aIdleInhibitManager); + void SetViewporter(wp_viewporter* aViewporter); + void SetRelativePointerManager( + zwp_relative_pointer_manager_v1* aRelativePointerManager); + void SetPointerConstraints(zwp_pointer_constraints_v1* aPointerConstraints); + void SetDmabuf(zwp_linux_dmabuf_v1* aDmabuf); + void SetXdgActivation(xdg_activation_v1* aXdgActivation); + + ~nsWaylandDisplay(); + + private: + PRThread* mThreadId = nullptr; + wl_registry* mRegistry = nullptr; + wl_display* mDisplay = nullptr; + wl_compositor* mCompositor = nullptr; + wl_subcompositor* mSubcompositor = nullptr; + wl_shm* mShm = nullptr; + zwp_idle_inhibit_manager_v1* mIdleInhibitManager = nullptr; + zwp_relative_pointer_manager_v1* mRelativePointerManager = nullptr; + zwp_pointer_constraints_v1* mPointerConstraints = nullptr; + wp_viewporter* mViewporter = nullptr; + zwp_linux_dmabuf_v1* mDmabuf = nullptr; + xdg_activation_v1* mXdgActivation = nullptr; + bool mExplicitSync = false; +}; + +wl_display* WaylandDisplayGetWLDisplay(); +nsWaylandDisplay* WaylandDisplayGet(); +void WaylandDisplayRelease(); + +} // 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..70b47d5124 --- /dev/null +++ b/widget/gtk/nsWidgetFactory.cpp @@ -0,0 +1,70 @@ +/* -*- 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" +#include "nsClipboard.h" +#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; + +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>(); +} + +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..f9312f6274 --- /dev/null +++ b/widget/gtk/nsWidgetFactory.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_gtk_nsWidgetFactory_h +#define widget_gtk_nsWidgetFactory_h + +#include "nscore.h" +#include "nsID.h" + +class nsISupports; + +nsresult nsAppShellConstructor(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..8ed12c734d --- /dev/null +++ b/widget/gtk/nsWindow.cpp @@ -0,0 +1,9813 @@ +/* -*- 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 <algorithm> +#include <cstdint> +#include <dlfcn.h> +#include <gdk/gdkkeysyms.h> +#include <wchar.h> + +#include "VsyncSource.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxImageSurface.h" +#include "gfxPlatformGtk.h" +#include "gfxUtils.h" +#include "GLContextProvider.h" +#include "GLContext.h" +#include "GtkCompositorWidget.h" +#include "gtkdrawing.h" +#include "imgIContainer.h" +#include "InputData.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Components.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/gfxVars.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/gfx/HelpersCairo.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/LayersTypes.h" +#include "mozilla/layers/CompositorBridgeChild.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" +#include "mozilla/Likely.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/NativeKeyBindingsType.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_mozilla.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/SwipeTracker.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WidgetUtils.h" +#include "mozilla/WritingModes.h" +#ifdef MOZ_X11 +# include "mozilla/X11Util.h" +#endif +#include "mozilla/XREAppData.h" +#include "NativeKeyBindings.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsAppRunner.h" +#include "nsDragService.h" +#include "nsGTKToolkit.h" +#include "nsGtkKeyUtils.h" +#include "nsGtkCursors.h" +#include "nsGfxCIID.h" +#include "nsGtkUtils.h" +#include "nsIFile.h" +#include "nsIGSettingsService.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsImageToPixbuf.h" +#include "nsINode.h" +#include "nsIRollupListener.h" +#include "nsIScreenManager.h" +#include "nsIUserIdleServiceInternal.h" +#include "nsIWidgetListener.h" +#include "nsLayoutUtils.h" +#include "nsMenuPopupFrame.h" +#include "nsPresContext.h" +#include "nsShmImage.h" +#include "nsString.h" +#include "nsWidgetsCID.h" +#include "nsViewManager.h" +#include "nsXPLookAndFeel.h" +#include "prlink.h" +#include "Screen.h" +#include "ScreenHelperGTK.h" +#include "SystemTimeConverter.h" +#include "WidgetUtilsGtk.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/LocalAccessible.h" +# include "mozilla/a11y/Platform.h" +# include "nsAccessibilityService.h" +#endif + +#ifdef MOZ_X11 +# include <gdk/gdkkeysyms-compat.h> +# include <X11/Xatom.h> +# include <X11/extensions/XShm.h> +# include <X11/extensions/shape.h> +# include "gfxXlibSurface.h" +# include "GLContextGLX.h" // for GLContextGLX::FindVisual() +# include "GLContextEGL.h" // for GLContextEGL::FindVisual() +# include "WindowSurfaceX11Image.h" +# include "WindowSurfaceX11SHM.h" +#endif +#ifdef MOZ_WAYLAND +# include <gdk/gdkkeysyms-compat.h> +# include "nsIClipboard.h" +# include "nsView.h" +#endif + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layers; +using namespace mozilla::widget; +#ifdef MOZ_X11 +using mozilla::gl::GLContextEGL; +using mozilla::gl::GLContextGLX; +#endif + +// 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; + +gint GDK_TOUCHPAD_GESTURE_MASK = 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; + +/* 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); + +/* callbacks from widgets */ +static gboolean expose_event_cb(GtkWidget* widget, cairo_t* cr); +static gboolean configure_event_cb(GtkWidget* widget, GdkEventConfigure* event); +static void widget_map_cb(GtkWidget* widget); +static void widget_unmap_cb(GtkWidget* widget); +static void widget_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); +MOZ_CAN_RUN_SCRIPT 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_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 SystemTimeConverter<guint32>& TimeConverter() { + static SystemTimeConverter<guint32> sTimeConverterSingleton; + return sTimeConverterSingleton; +} + +bool nsWindow::sTransparentMainWindow = false; + +// forward declare from mozgtk +extern "C" MOZ_EXPORT void mozgtk_linker_holder(); + +namespace mozilla { + +#ifdef MOZ_X11 +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; +}; +#endif + +} // namespace mozilla + +// 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 gUseAspectRatio = true; +static uint32_t gLastTouchID = 0; +// See Bug 1777269 for details. We don't know if the suspected leave notify +// event is a correct one when we get it. +// Store it and issue it later from enter notify event if it's correct, +// throw it away otherwise. +static GUniquePtr<GdkEventCrossing> sStoredLeaveNotifyEvent; + +#define NS_WINDOW_TITLE_MAX_LENGTH 4095 + +// cursor cache +static GdkCursor* gCursorCache[eCursorCount]; + +// 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; +} + +// Don't set parent (transient for) if nothing changes. +// gtk_window_set_transient_for() blows up wl_subsurfaces used by aWindow +// even if aParent is the same. +static void GtkWindowSetTransientFor(GtkWindow* aWindow, GtkWindow* aParent) { + GtkWindow* parent = gtk_window_get_transient_for(aWindow); + if (parent != aParent) { + gtk_window_set_transient_for(aWindow, aParent); + } +} + +#define gtk_window_set_transient_for(a, b) \ + { \ + MOZ_ASSERT_UNREACHABLE( \ + "gtk_window_set_transient_for() can't be used directly."); \ + } + +nsWindow::nsWindow() + : mIsDestroyed(false), + mIsShown(false), + mNeedsShow(false), + mIsMapped(false), + mEnabled(true), + mCreated(false), + mHandleTouchEvent(false), + mIsDragPopup(false), + mWindowScaleFactorChanged(true), + mCompositedScreen(gdk_screen_is_composited(gdk_screen_get_default())), + mIsAccelerated(false), + mWindowShouldStartDragging(false), + mHasMappedToplevel(false), + mRetryPointerGrab(false), + mPanInProgress(false), + mDrawToContainer(false), + mTitlebarBackdropState(false), + mIsPIPWindow(false), + mIsWaylandPanelWindow(false), + mIsChildWindow(false), + mAlwaysOnTop(false), + mNoAutoHide(false), + mIsTransparent(false), + mHasReceivedSizeAllocate(false), + mWidgetCursorLocked(false), + mPopupTrackInHierarchy(false), + mPopupTrackInHierarchyConfigured(false), + mHiddenPopupPositioned(false), + mTransparencyBitmapForTitlebar(false), + mHasAlphaVisual(false), + mPopupAnchored(false), + mPopupContextMenu(false), + mPopupMatchesLayout(false), + mPopupChanged(false), + mPopupTemporaryHidden(false), + mPopupClosed(false), + mPopupUseMoveToRect(false), + mWaitingForMoveToRectCallback(false), + mMovedAfterMoveToRect(false), + mResizedAfterMoveToRect(false), + mConfiguredClearColor(false), + mGotNonBlankPaint(false), + mNeedsToRetryCapturingMouse(false) { + mWindowType = WindowType::Child; + mSizeConstraints.mMaxSize = GetSafeWindowSize(mSizeConstraints.mMaxSize); + + 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 (GdkIsWaylandDisplay()) { + nsCOMPtr<nsIClipboard> clipboard = + do_GetService("@mozilla.org/widget/clipboard;1"); + NS_ASSERTION(clipboard, "Failed to init clipboard!"); + } +#endif + } + // Dummy call to mozgtk to prevent the linker from removing + // the dependency with --as-needed. + // see toolkit/library/moz.build for details. + mozgtk_linker_holder(); +} + +nsWindow::~nsWindow() { + LOG("nsWindow::~nsWindow()"); + Destroy(); +} + +/* static */ +void nsWindow::ReleaseGlobals() { + for (auto& cursor : gCursorCache) { + if (cursor) { + g_object_unref(cursor); + cursor = nullptr; + } + } +} + +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() { + if (mWidgetListener) { + mWidgetListener->WindowDeactivated(); + } + +#ifdef ACCESSIBILITY + DispatchDeactivateEventAccessible(); +#endif // ACCESSIBILITY +} + +void nsWindow::DispatchResized() { + LOG("nsWindow::DispatchResized() size [%d, %d]", (int)(mBounds.width), + (int)(mBounds.height)); + + mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1); + if (mWidgetListener) { + mWidgetListener->WindowResized(this, mBounds.width, mBounds.height); + } + if (mAttachedWidgetListener) { + mAttachedWidgetListener->WindowResized(this, mBounds.width, mBounds.height); + } +} + +void nsWindow::MaybeDispatchResized() { + if (mNeedsDispatchSize != LayoutDeviceIntSize(-1, -1) && !mIsDestroyed) { + mBounds.SizeTo(mNeedsDispatchSize); + // Check mBounds size + if (mCompositorSession && + !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) { + gfxCriticalNoteOnce << "Invalid mBounds in MaybeDispatchResized " + << mBounds << " size state " << mSizeMode; + } + + if (mWindowType == WindowType::TopLevel) { + UpdateTopLevelOpaqueRegion(); + } + + // Notify the GtkCompositorWidget of a ClientSizeChange + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize()); + } + + 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() { + // Check requested size, as mBounds might not have been updated. + return !mLastSizeRequest.IsEmpty(); +} + +// Walk the list of child windows and call destroy on them. +void nsWindow::DestroyChildWindows() { + LOG("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(); + } + } +} + +void nsWindow::Destroy() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + if (mIsDestroyed || !mCreated) { + return; + } + + LOG("nsWindow::Destroy\n"); + + mIsDestroyed = true; + mCreated = false; + + MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove); + + ClearTransparencyBitmap(); + +#ifdef MOZ_WAYLAND + // Shut down our local vsync source + if (mWaylandVsyncSource) { + mWaylandVsyncSource->Shutdown(); + mWaylandVsyncSource = nullptr; + } + mWaylandVsyncDispatcher = nullptr; +#endif + + /** Need to clean our LayerManager up while still alive */ + DestroyLayerManager(); + + // Ensure any resources assigned to the window get cleaned up first + // to avoid double-freeing. + mSurfaceProvider.CleanupResources(); + + 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({}); + } + } + + // 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) { + LOG("automatically losing focus...\n"); + gFocusWindow = nullptr; + } + + if (sStoredLeaveNotifyEvent) { + nsWindow* window = + get_window_for_gdk_window(sStoredLeaveNotifyEvent->window); + if (window == this) { + sStoredLeaveNotifyEvent = nullptr; + } + } + + // We need to detach accessible object here because mContainer is a custom + // widget and doesn't call gtk_widget_real_destroy() from destroy handler + // as regular widgets. + if (AtkObject* ac = gtk_widget_get_accessible(GTK_WIDGET(mContainer))) { + gtk_accessible_set_widget(GTK_ACCESSIBLE(ac), nullptr); + } + + gtk_widget_destroy(mShell); + mShell = nullptr; + mContainer = nullptr; + + MOZ_ASSERT(!mGdkWindow, + "mGdkWindow should be NULL when mContainer is destroyed"); + +#ifdef ACCESSIBILITY + if (mRootAccessible) { + mRootAccessible = nullptr; + } +#endif + + // Save until last because OnDestroy() may cause us to be deleted. + OnDestroy(); +} + +nsIWidget* nsWindow::GetParent() { return mParent; } + +float nsWindow::GetDPI() { + float dpi = 96.0f; + nsCOMPtr<nsIScreen> screen = GetWidgetScreen(); + if (screen) { + screen->GetDpi(&dpi); + } + return dpi; +} + +double nsWindow::GetDefaultScaleInternal() { return FractionalScaleFactor(); } + +DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScale() { +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + return DesktopToLayoutDeviceScale(GdkCeiledScaleFactor()); + } +#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 (GdkIsWaylandDisplay()) { + 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(); +} + +// Reparent a child window to a new parent. +void nsWindow::SetParent(nsIWidget* aNewParent) { + LOG("nsWindow::SetParent() new parent %p", aNewParent); + if (!mIsChildWindow) { + NS_WARNING("Used by child widgets only"); + return; + } + + nsCOMPtr<nsIWidget> kungFuDeathGrip = this; + if (mParent) { + mParent->RemoveChild(this); + } + mParent = aNewParent; + + // We're already deleted, quit. + if (!mGdkWindow || mIsDestroyed || !aNewParent) { + return; + } + aNewParent->AddChild(this); + + auto* newParent = static_cast<nsWindow*>(aNewParent); + + // New parent is deleted, quit. + if (newParent->mIsDestroyed) { + Destroy(); + return; + } + + GdkWindow* window = GetToplevelGdkWindow(); + GdkWindow* parentWindow = newParent->GetToplevelGdkWindow(); + LOG(" child GdkWindow %p set parent GdkWindow %p", window, parentWindow); + gdk_window_reparent(window, parentWindow, 0, 0); + SetHasMappedToplevel(newParent && newParent->mHasMappedToplevel); +} + +bool nsWindow::WidgetTypeSupportsAcceleration() { + if (mWindowType == WindowType::Invisible) { + return false; + } + + 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 == WindowType::Popup) { + return HasRemoteContent() && 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( + !mParent, + "nsWindow::ReparentNativeWidget() works on toplevel windows only."); + + auto* newParent = static_cast<nsWindow*>(aNewParent); + GtkWindow* newParentWidget = GTK_WINDOW(newParent->GetGtkWidget()); + + LOG("nsWindow::ReparentNativeWidget new parent %p\n", newParent); + GtkWindowSetTransientFor(GTK_WINDOW(mShell), newParentWidget); +} + +void nsWindow::SetModal(bool aModal) { + LOG("nsWindow::SetModal %d\n", aModal); + if (mIsDestroyed) return; + gtk_window_set_modal(GTK_WINDOW(mShell), aModal ? TRUE : FALSE); +} + +// nsIWidget method, which means IsShown. +bool nsWindow::IsVisible() const { return mIsShown; } + +bool nsWindow::IsMapped() const { + // TODO: Enable for X11 when Mozilla testsuite is moved to new + // testing environment from Ubuntu 18.04 which is broken. + return GdkIsWaylandDisplay() ? mIsMapped : true; +} + +void nsWindow::RegisterTouchWindow() { + mHandleTouchEvent = true; + mTouches.Clear(); +} + +LayoutDeviceIntPoint nsWindow::GetScreenEdgeSlop() { + if (DrawsToCSDTitlebar()) { + return GetClientOffset(); + } + return {}; +} + +void nsWindow::ConstrainPosition(DesktopIntPoint& aPoint) { + if (!mShell || GdkIsWaylandDisplay()) { + 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<nsIScreenManager> screenmgr = + do_GetService("@mozilla.org/gfx/screenmanager;1"); + if (!screenmgr) { + return; + } + nsCOMPtr<nsIScreen> screen; + screenmgr->ScreenForRect(aPoint.x, aPoint.y, logWidth, logHeight, + getter_AddRefs(screen)); + // We don't have any screen so leave the coordinates as is + if (!screen) { + return; + } + + // For normalized windows, use the desktop work area. + // For full screen windows, use the desktop. + DesktopIntRect screenRect = mSizeMode == nsSizeMode_Fullscreen + ? screen->GetRectDisplayPix() + : screen->GetAvailRectDisplayPix(); + + // Expand for the decoration size if needed. + auto slop = + DesktopIntPoint::Round(GetScreenEdgeSlop() / GetDesktopToDeviceScale()); + screenRect.Inflate(slop.x, slop.y); + + if (aPoint.x < screenRect.x) { + aPoint.x = screenRect.x; + } else if (aPoint.x >= screenRect.XMost() - logWidth) { + aPoint.x = screenRect.XMost() - logWidth; + } + + if (aPoint.y < screenRect.y) { + aPoint.y = screenRect.y; + } else if (aPoint.y >= screenRect.YMost() - logHeight) { + aPoint.y = screenRect.YMost() - logHeight; + } +} + +void nsWindow::SetSizeConstraints(const SizeConstraints& aConstraints) { + mSizeConstraints.mMinSize = GetSafeWindowSize(aConstraints.mMinSize); + mSizeConstraints.mMaxSize = GetSafeWindowSize(aConstraints.mMaxSize); + + ApplySizeConstraints(); +} + +bool nsWindow::DrawsToCSDTitlebar() const { + return mSizeMode == nsSizeMode_Normal && + mGtkWindowDecoration == GTK_DECORATION_CLIENT && mDrawInTitlebar; +} + +void nsWindow::AddCSDDecorationSize(int* aWidth, int* aHeight) { + if (!DrawsToCSDTitlebar()) { + return; + } + GtkBorder decorationSize = GetCSDDecorationSize(IsPopup()); + *aWidth += decorationSize.left + decorationSize.right; + *aHeight += decorationSize.top + decorationSize.bottom; +} + +#ifdef MOZ_WAYLAND +bool nsWindow::GetCSDDecorationOffset(int* aDx, int* aDy) { + if (!DrawsToCSDTitlebar()) { + return false; + } + GtkBorder decorationSize = GetCSDDecorationSize(IsPopup()); + *aDx = decorationSize.left; + *aDy = decorationSize.top; + return true; +} +#endif + +void nsWindow::ApplySizeConstraints() { + 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()) { + if (GdkIsWaylandDisplay()) { + gtk_widget_set_size_request(GTK_WIDGET(mContainer), geometry.min_width, + geometry.min_height); + } + AddCSDDecorationSize(&geometry.min_width, &geometry.min_height); + hints |= GDK_HINT_MIN_SIZE; + } + if (mSizeConstraints.mMaxSize != + LayoutDeviceIntSize(NS_MAXSIZE, NS_MAXSIZE)) { + AddCSDDecorationSize(&geometry.max_width, &geometry.max_height); + hints |= GDK_HINT_MAX_SIZE; + } + + 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; + + mIsShown = aState; + +#ifdef MOZ_LOGGING + LOG("nsWindow::Show state %d frame %s\n", aState, GetFrameTag().get()); + if (!aState && mSourceDragContext && GdkIsWaylandDisplay()) { + LOG(" closing Drag&Drop source window, D&D will be canceled!"); + } +#endif + + // 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(const Maybe<LayoutDeviceIntPoint>& aMove, + LayoutDeviceIntSize aSize) { + LOG("nsWindow::ResizeInt w:%d h:%d\n", aSize.width, aSize.height); + const bool moved = aMove && *aMove != mBounds.TopLeft(); + if (moved) { + mBounds.MoveTo(*aMove); + LOG(" with move to left:%d top:%d", aMove->x.value, aMove->y.value); + } + + ConstrainSize(&aSize.width, &aSize.height); + LOG(" ConstrainSize: w:%d h;%d\n", aSize.width, aSize.height); + + const bool resized = aSize != mLastSizeRequest || mBounds.Size() != aSize; +#if MOZ_LOGGING + LOG(" resized %d aSize [%d, %d] mLastSizeRequest [%d, %d] mBounds [%d, %d]", + resized, aSize.width, aSize.height, mLastSizeRequest.width, + mLastSizeRequest.height, mBounds.width, mBounds.height); +#endif + + // For top-level windows, aSize should possibly be + // interpreted as frame bounds, but NativeMoveResize treats these as window + // bounds (Bug 581866). + mLastSizeRequest = aSize; + // Check size + if (mCompositorSession && + !wr::WindowSizeSanityCheck(aSize.width, aSize.height)) { + gfxCriticalNoteOnce << "Invalid aSize in ResizeInt " << aSize + << " size state " << mSizeMode; + } + + // Recalculate aspect ratio when resized from DOM + if (mAspectRatio != 0.0) { + LockAspectRatio(true); + } + + if (!mCreated) { + return; + } + + if (!moved && !resized) { + LOG(" not moved or resized, quit"); + return; + } + + NativeMoveResize(moved, resized); + + // We optimistically assume size changes immediately in two cases: + // 1. Override-redirect window: Size is controlled by only us. + // 2. Managed window that has not not yet received a size-allocate event: + // Resize() Callers expect initial sizes to be applied synchronously. + // If the size request is not honored, then we'll correct in + // OnSizeAllocate(). + // + // When a managed window has already received a size-allocate, we cannot + // assume we'll always get a notification if our request does not get + // honored: "If the configure request has not changed, we don't ever resend + // it, because it could mean fighting the user or window manager." + // https://gitlab.gnome.org/GNOME/gtk/-/blob/3.24.31/gtk/gtkwindow.c#L9782 + // So we don't update mBounds until OnSizeAllocate() when we know the + // request is granted. + bool isOrWillBeVisible = mHasReceivedSizeAllocate || mNeedsShow || mIsShown; + if (!isOrWillBeVisible || + gtk_window_get_window_type(GTK_WINDOW(mShell)) == GTK_WINDOW_POPUP) { + mBounds.SizeTo(aSize); + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyClientSizeChanged(aSize); + } + DispatchResized(); + } +} + +void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) { + LOG("nsWindow::Resize %f %f\n", aWidth, aHeight); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + auto size = LayoutDeviceIntSize::Round(scale * aWidth, scale * aHeight); + + ResizeInt(Nothing(), size); +} + +void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) { + LOG("nsWindow::Resize [%f,%f] -> [%f x %f] repaint %d\n", aX, aY, aWidth, + aHeight, aRepaint); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + auto size = LayoutDeviceIntSize::Round(scale * aWidth, scale * aHeight); + auto topLeft = LayoutDeviceIntPoint::Round(scale * aX, scale * aY); + + ResizeInt(Some(topLeft), size); +} + +void nsWindow::Enable(bool aState) { mEnabled = aState; } + +bool nsWindow::IsEnabled() const { return mEnabled; } + +void nsWindow::Move(double aX, double aY) { + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t x = NSToIntRound(aX * scale); + int32_t y = NSToIntRound(aY * scale); + + LOG("nsWindow::Move to %d x %d\n", x, y); + + if (mSizeMode != nsSizeMode_Normal && (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog)) { + LOG(" size state is not normal, bailing"); + return; + } + + // 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. + LOG(" bounds %d x %d\n", mBounds.x, mBounds.y); + if (x == mBounds.x && y == mBounds.y && mWindowType != WindowType::Popup) { + LOG(" position is the same, return\n"); + return; + } + + // XXX Should we do some AreBoundsSane check here? + + mBounds.x = x; + mBounds.y = y; + + if (!mCreated) { + LOG(" is not created, return.\n"); + return; + } + + NativeMoveResize(/* move */ true, /* resize */ false); +} + +bool nsWindow::IsPopup() const { return mWindowType == WindowType::Popup; } + +bool nsWindow::IsWaylandPopup() const { + return GdkIsWaylandDisplay() && IsPopup(); +} + +static nsMenuPopupFrame* GetMenuPopupFrame(nsIFrame* aFrame) { + return do_QueryFrame(aFrame); +} + +void nsWindow::AppendPopupToHierarchyList(nsWindow* aToplevelWindow) { + mWaylandToplevel = aToplevelWindow; + + nsWindow* popup = aToplevelWindow; + while (popup && popup->mWaylandPopupNext) { + popup = popup->mWaylandPopupNext; + } + popup->mWaylandPopupNext = this; + + mWaylandPopupPrev = popup; + mWaylandPopupNext = nullptr; + mPopupChanged = true; + mPopupClosed = false; +} + +void nsWindow::RemovePopupFromHierarchyList() { + // We're already removed from the popup hierarchy + if (!IsInPopupHierarchy()) { + return; + } + mWaylandPopupPrev->mWaylandPopupNext = mWaylandPopupNext; + if (mWaylandPopupNext) { + mWaylandPopupNext->mWaylandPopupPrev = mWaylandPopupPrev; + mWaylandPopupNext->mPopupChanged = true; + } + mWaylandPopupNext = mWaylandPopupPrev = nullptr; +} + +// Gtk refuses to map popup window with x < 0 && y < 0 relative coordinates +// see https://gitlab.gnome.org/GNOME/gtk/-/issues/4071 +// as a workaround just fool around and place the popup temporary to 0,0. +bool nsWindow::WaylandPopupRemoveNegativePosition(int* aX, int* aY) { + // https://gitlab.gnome.org/GNOME/gtk/-/issues/4071 applies to temporary + // windows only + GdkWindow* window = gtk_widget_get_window(mShell); + if (!window || gdk_window_get_window_type(window) != GDK_WINDOW_TEMP) { + return false; + } + + LOG("nsWindow::WaylandPopupRemoveNegativePosition()"); + + int x, y; + gtk_window_get_position(GTK_WINDOW(mShell), &x, &y); + bool moveBack = (x < 0 && y < 0); + if (moveBack) { + gtk_window_move(GTK_WINDOW(mShell), 0, 0); + if (aX) { + *aX = x; + } + if (aY) { + *aY = y; + } + } + + gdk_window_get_geometry(window, &x, &y, nullptr, nullptr); + if (x < 0 && y < 0) { + gdk_window_move(window, 0, 0); + } + + return moveBack; +} + +void nsWindow::ShowWaylandPopupWindow() { + LOG("nsWindow::ShowWaylandPopupWindow. Expected to see visible."); + MOZ_ASSERT(IsWaylandPopup()); + + if (!mPopupTrackInHierarchy) { + LOG(" popup is not tracked in popup hierarchy, show it now"); + gtk_widget_show(mShell); + return; + } + + // Popup position was checked before gdk_window_move_to_rect() callback + // so just show it. + if (mPopupUseMoveToRect && mWaitingForMoveToRectCallback) { + LOG(" active move-to-rect callback, show it as is"); + gtk_widget_show(mShell); + return; + } + + if (gtk_widget_is_visible(mShell)) { + LOG(" is already visible, quit"); + return; + } + + int x, y; + bool moved = WaylandPopupRemoveNegativePosition(&x, &y); + gtk_widget_show(mShell); + if (moved) { + LOG(" move back to (%d, %d) and show", x, y); + gtk_window_move(GTK_WINDOW(mShell), x, y); + } +} + +void nsWindow::WaylandPopupMarkAsClosed() { + LOG("nsWindow::WaylandPopupMarkAsClosed: [%p]\n", this); + mPopupClosed = true; + // If we have any child popup window notify it about + // parent switch. + if (mWaylandPopupNext) { + mWaylandPopupNext->mPopupChanged = true; + } +} + +nsWindow* nsWindow::WaylandPopupFindLast(nsWindow* aPopup) { + while (aPopup && aPopup->mWaylandPopupNext) { + aPopup = aPopup->mWaylandPopupNext; + } + return aPopup; +} + +// Hide and potentially removes popup from popup hierarchy. +void nsWindow::HideWaylandPopupWindow(bool aTemporaryHide, + bool aRemoveFromPopupList) { + LOG("nsWindow::HideWaylandPopupWindow: remove from list %d\n", + aRemoveFromPopupList); + if (aRemoveFromPopupList) { + RemovePopupFromHierarchyList(); + } + + if (!mPopupClosed) { + mPopupClosed = !aTemporaryHide; + } + + bool visible = gtk_widget_is_visible(mShell); + LOG(" gtk_widget_is_visible() = %d\n", visible); + + // Restore only popups which are really visible + mPopupTemporaryHidden = aTemporaryHide && visible; + + // Hide only visible popups or popups closed pernamently. + if (visible) { + gtk_widget_hide(mShell); + + // If there's pending Move-To-Rect callback and we hide the popup + // the callback won't be called any more. + mWaitingForMoveToRectCallback = false; + } + + if (mPopupClosed) { + LOG(" Clearing mMoveToRectPopupSize\n"); + mMoveToRectPopupSize = {}; +#ifdef MOZ_WAYLAND + if (moz_container_wayland_is_waiting_to_show(mContainer)) { + // We need to clear rendering queue, see Bug 1782948. + LOG(" popup failed to show by Wayland compositor, clear rendering " + "queue."); + moz_container_wayland_clear_waiting_to_show_flag(mContainer); + ClearRenderingQueue(); + } +#endif + } +} + +void nsWindow::HideWaylandToplevelWindow() { + LOG("nsWindow::HideWaylandToplevelWindow: [%p]\n", this); + if (mWaylandPopupNext) { + nsWindow* popup = WaylandPopupFindLast(mWaylandPopupNext); + while (popup->mWaylandToplevel != nullptr) { + nsWindow* prev = popup->mWaylandPopupPrev; + popup->HideWaylandPopupWindow(/* aTemporaryHide */ false, + /* aRemoveFromPopupList */ true); + popup = prev; + } + } + WaylandStopVsync(); + gtk_widget_hide(mShell); +} + +void nsWindow::ShowWaylandToplevelWindow() { + MOZ_ASSERT(!IsWaylandPopup()); + LOG("nsWindow::ShowWaylandToplevelWindow"); + gtk_widget_show(mShell); +} + +void nsWindow::WaylandPopupRemoveClosedPopups() { + LOG("nsWindow::WaylandPopupRemoveClosedPopups()"); + nsWindow* popup = this; + while (popup) { + nsWindow* next = popup->mWaylandPopupNext; + if (popup->mPopupClosed) { + popup->HideWaylandPopupWindow(/* aTemporaryHide */ false, + /* aRemoveFromPopupList */ true); + } + popup = next; + } +} + +// Hide all tooltips except the latest one. +void nsWindow::WaylandPopupHideTooltips() { + LOG("nsWindow::WaylandPopupHideTooltips"); + MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!"); + + nsWindow* popup = mWaylandPopupNext; + while (popup && popup->mWaylandPopupNext) { + if (popup->mPopupType == PopupType::Tooltip) { + LOG(" hidding tooltip [%p]", popup); + popup->WaylandPopupMarkAsClosed(); + } + popup = popup->mWaylandPopupNext; + } +} + +void nsWindow::WaylandPopupCloseOrphanedPopups() { +#ifdef MOZ_WAYLAND + LOG("nsWindow::WaylandPopupCloseOrphanedPopups"); + MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!"); + + nsWindow* popup = mWaylandPopupNext; + bool dangling = false; + while (popup) { + if (!dangling && + moz_container_wayland_is_waiting_to_show(popup->GetMozContainer())) { + LOG(" popup [%p] is waiting to show, close all child popups", popup); + dangling = true; + } else if (dangling) { + popup->WaylandPopupMarkAsClosed(); + } + popup = popup->mWaylandPopupNext; + } +#endif +} + +// We can't show popups with remote content or overflow popups +// on top of regular ones. +// If there's any remote popup opened, close all parent popups of it. +void nsWindow::CloseAllPopupsBeforeRemotePopup() { + LOG("nsWindow::CloseAllPopupsBeforeRemotePopup"); + MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!"); + + // Don't waste time when there's only one popup opened. + if (!mWaylandPopupNext || mWaylandPopupNext->mWaylandPopupNext == nullptr) { + return; + } + + // Find the first opened remote content popup + nsWindow* remotePopup = mWaylandPopupNext; + while (remotePopup) { + if (remotePopup->HasRemoteContent() || + remotePopup->IsWidgetOverflowWindow()) { + LOG(" remote popup [%p]", remotePopup); + break; + } + remotePopup = remotePopup->mWaylandPopupNext; + } + + if (!remotePopup) { + return; + } + + // ...hide opened popups before the remote one. + nsWindow* popup = mWaylandPopupNext; + while (popup && popup != remotePopup) { + LOG(" hidding popup [%p]", popup); + popup->WaylandPopupMarkAsClosed(); + popup = popup->mWaylandPopupNext; + } +} + +static void GetLayoutPopupWidgetChain( + nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + pm->GetSubmenuWidgetChain(aLayoutWidgetHierarchy); + aLayoutWidgetHierarchy->Reverse(); +} + +// Compare 'this' popup position in Wayland widget hierarchy +// (mWaylandPopupPrev/mWaylandPopupNext) with +// 'this' popup position in layout hierarchy. +// +// When aMustMatchParent is true we also request +// 'this' parents match, i.e. 'this' has the same parent in +// both layout and widget hierarchy. +bool nsWindow::IsPopupInLayoutPopupChain( + nsTArray<nsIWidget*>* aLayoutWidgetHierarchy, bool aMustMatchParent) { + int len = (int)aLayoutWidgetHierarchy->Length(); + for (int i = 0; i < len; i++) { + if (this == (*aLayoutWidgetHierarchy)[i]) { + if (!aMustMatchParent) { + return true; + } + + // Find correct parent popup for 'this' according to widget + // hierarchy. That means we need to skip closed popups. + nsWindow* parentPopup = nullptr; + if (mWaylandPopupPrev != mWaylandToplevel) { + parentPopup = mWaylandPopupPrev; + while (parentPopup != mWaylandToplevel && parentPopup->mPopupClosed) { + parentPopup = parentPopup->mWaylandPopupPrev; + } + } + + if (i == 0) { + // We found 'this' popups as a first popup in layout hierarchy. + // It matches layout hierarchy if it's first widget also in + // wayland widget hierarchy (i.e. parent is null). + return parentPopup == nullptr; + } + + return parentPopup == (*aLayoutWidgetHierarchy)[i - 1]; + } + } + return false; +} + +// Hide popups which are not in popup chain. +void nsWindow::WaylandPopupHierarchyHideByLayout( + nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) { + LOG("nsWindow::WaylandPopupHierarchyHideByLayout"); + MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!"); + + // Hide all popups which are not in layout popup chain + nsWindow* popup = mWaylandPopupNext; + while (popup) { + // Don't check closed popups and drag source popups and tooltips. + if (!popup->mPopupClosed && popup->mPopupType != PopupType::Tooltip && + !popup->mSourceDragContext) { + if (!popup->IsPopupInLayoutPopupChain(aLayoutWidgetHierarchy, + /* aMustMatchParent */ false)) { + LOG(" hidding popup [%p]", popup); + popup->WaylandPopupMarkAsClosed(); + } + } + popup = popup->mWaylandPopupNext; + } +} + +// Mark popups outside of layout hierarchy +void nsWindow::WaylandPopupHierarchyValidateByLayout( + nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) { + LOG("nsWindow::WaylandPopupHierarchyValidateByLayout"); + nsWindow* popup = mWaylandPopupNext; + while (popup) { + if (popup->mPopupType == PopupType::Tooltip) { + popup->mPopupMatchesLayout = true; + } else if (!popup->mPopupClosed) { + popup->mPopupMatchesLayout = popup->IsPopupInLayoutPopupChain( + aLayoutWidgetHierarchy, /* aMustMatchParent */ true); + LOG(" popup [%p] parent window [%p] matches layout %d\n", (void*)popup, + (void*)popup->mWaylandPopupPrev, popup->mPopupMatchesLayout); + } + popup = popup->mWaylandPopupNext; + } +} + +void nsWindow::WaylandPopupHierarchyHideTemporary() { + LOG("nsWindow::WaylandPopupHierarchyHideTemporary()"); + nsWindow* popup = WaylandPopupFindLast(this); + while (popup && popup != this) { + LOG(" temporary hidding popup [%p]", popup); + nsWindow* prev = popup->mWaylandPopupPrev; + popup->HideWaylandPopupWindow(/* aTemporaryHide */ true, + /* aRemoveFromPopupList */ false); + popup = prev; + } +} + +void nsWindow::WaylandPopupHierarchyShowTemporaryHidden() { + LOG("nsWindow::WaylandPopupHierarchyShowTemporaryHidden()"); + nsWindow* popup = this; + while (popup) { + if (popup->mPopupTemporaryHidden) { + popup->mPopupTemporaryHidden = false; + LOG(" showing temporary hidden popup [%p]", popup); + popup->ShowWaylandPopupWindow(); + } + popup = popup->mWaylandPopupNext; + } +} + +void nsWindow::WaylandPopupHierarchyCalculatePositions() { + LOG("nsWindow::WaylandPopupHierarchyCalculatePositions()"); + + // Set widget hierarchy in Gtk + nsWindow* popup = mWaylandToplevel->mWaylandPopupNext; + while (popup) { + LOG(" popup [%p] set parent window [%p]", (void*)popup, + (void*)popup->mWaylandPopupPrev); + GtkWindowSetTransientFor(GTK_WINDOW(popup->mShell), + GTK_WINDOW(popup->mWaylandPopupPrev->mShell)); + popup = popup->mWaylandPopupNext; + } + + popup = this; + while (popup) { + // Anchored window has mPopupPosition already calculated against + // its parent, no need to recalculate. + LOG(" popup [%p] bounds [%d, %d] -> [%d x %d]", popup, + (int)(popup->mBounds.x / FractionalScaleFactor()), + (int)(popup->mBounds.y / FractionalScaleFactor()), + (int)(popup->mBounds.width / FractionalScaleFactor()), + (int)(popup->mBounds.height / FractionalScaleFactor())); +#ifdef MOZ_LOGGING + if (LOG_ENABLED()) { + if (nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame())) { + auto r = LayoutDeviceRect::FromAppUnitsRounded( + popupFrame->GetRect(), + popupFrame->PresContext()->AppUnitsPerDevPixel()); + LOG(" popup [%p] layout [%d, %d] -> [%d x %d]", popup, r.x, r.y, + r.width, r.height); + } + } +#endif + if (popup->WaylandPopupIsFirst()) { + LOG(" popup [%p] has toplevel as parent", popup); + popup->mRelativePopupPosition = popup->mPopupPosition; + } else { + if (popup->mPopupAnchored) { + LOG(" popup [%p] is anchored", popup); + if (!popup->mPopupMatchesLayout) { + NS_WARNING("Anchored popup does not match layout!"); + } + } + GdkPoint parent = popup->WaylandGetParentPosition(); + + LOG(" popup [%p] uses transformed coordinates\n", popup); + LOG(" parent position [%d, %d]\n", parent.x, parent.y); + LOG(" popup position [%d, %d]\n", popup->mPopupPosition.x, + popup->mPopupPosition.y); + + popup->mRelativePopupPosition.x = popup->mPopupPosition.x - parent.x; + popup->mRelativePopupPosition.y = popup->mPopupPosition.y - parent.y; + } + LOG(" popup [%p] transformed popup coordinates from [%d, %d] to [%d, %d]", + popup, popup->mPopupPosition.x, popup->mPopupPosition.y, + popup->mRelativePopupPosition.x, popup->mRelativePopupPosition.y); + popup = popup->mWaylandPopupNext; + } +} + +// The MenuList popups are used as dropdown menus for example in WebRTC +// microphone/camera chooser or autocomplete widgets. +bool nsWindow::WaylandPopupIsMenu() { + nsMenuPopupFrame* menuPopupFrame = GetMenuPopupFrame(GetFrame()); + if (menuPopupFrame) { + return mPopupType == PopupType::Menu && !menuPopupFrame->IsMenuList(); + } + return false; +} + +bool nsWindow::WaylandPopupIsContextMenu() { + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + if (!popupFrame) { + return false; + } + return popupFrame->IsContextMenu(); +} + +bool nsWindow::WaylandPopupIsPermanent() { + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + if (!popupFrame) { + // We can always hide popups without frames. + return false; + } + return popupFrame->IsNoAutoHide(); +} + +bool nsWindow::WaylandPopupIsAnchored() { + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + if (!popupFrame) { + // We can always hide popups without frames. + return false; + } + return !!popupFrame->GetAnchor(); +} + +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; +} + +bool nsWindow::WaylandPopupIsFirst() { + return !mWaylandPopupPrev || !mWaylandPopupPrev->mWaylandToplevel; +} + +nsWindow* nsWindow::GetEffectiveParent() { + GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) { + return nullptr; + } + return get_window_for_gtk_widget(GTK_WIDGET(parentGtkWindow)); +} + +GdkPoint nsWindow::WaylandGetParentPosition() { + GdkPoint topLeft = {0, 0}; + nsWindow* window = GetEffectiveParent(); + if (window->IsPopup()) { + topLeft = DevicePixelsToGdkPointRoundDown(window->mBounds.TopLeft()); + } + LOG("nsWindow::WaylandGetParentPosition() [%d, %d]\n", topLeft.x, topLeft.y); + return topLeft; +} + +#ifdef MOZ_LOGGING +void nsWindow::LogPopupHierarchy() { + if (!LOG_ENABLED()) { + return; + } + + LOG("Widget Popup Hierarchy:\n"); + if (!mWaylandToplevel->mWaylandPopupNext) { + LOG(" Empty\n"); + } else { + int indent = 4; + nsWindow* popup = mWaylandToplevel->mWaylandPopupNext; + while (popup) { + nsPrintfCString indentString("%*s", indent, " "); + LOG("%s %s %s nsWindow [%p] Menu %d Permanent %d ContextMenu %d " + "Anchored %d Visible %d MovedByRect %d\n", + indentString.get(), popup->GetFrameTag().get(), + popup->GetPopupTypeName().get(), popup, popup->WaylandPopupIsMenu(), + popup->WaylandPopupIsPermanent(), popup->mPopupContextMenu, + popup->mPopupAnchored, gtk_widget_is_visible(popup->mShell), + popup->mPopupUseMoveToRect); + indent += 4; + popup = popup->mWaylandPopupNext; + } + } + + LOG("Layout Popup Hierarchy:\n"); + AutoTArray<nsIWidget*, 5> widgetChain; + GetLayoutPopupWidgetChain(&widgetChain); + if (widgetChain.Length() == 0) { + LOG(" Empty\n"); + } else { + for (unsigned long i = 0; i < widgetChain.Length(); i++) { + nsWindow* window = static_cast<nsWindow*>(widgetChain[i]); + nsPrintfCString indentString("%*s", (int)(i + 1) * 4, " "); + if (window) { + LOG("%s %s %s nsWindow [%p] Menu %d Permanent %d ContextMenu %d " + "Anchored %d Visible %d MovedByRect %d\n", + indentString.get(), window->GetFrameTag().get(), + window->GetPopupTypeName().get(), window, + window->WaylandPopupIsMenu(), window->WaylandPopupIsPermanent(), + window->mPopupContextMenu, window->mPopupAnchored, + gtk_widget_is_visible(window->mShell), window->mPopupUseMoveToRect); + } else { + LOG("%s null window\n", indentString.get()); + } + } + } +} +#endif + +nsWindow* nsWindow::WaylandPopupGetTopmostWindow() { + 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 parentnsWindow; + } + } + } + return nullptr; +} + +// Configure Wayland popup. If true is returned we need to track popup +// in popup hierarchy. Otherwise we just show it as is. +bool nsWindow::WaylandPopupConfigure() { + if (mIsDragPopup) { + return false; + } + + // Don't track popups without frame + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + if (!popupFrame) { + return false; + } + + // Popup state can be changed, see Bug 1728952. + bool permanentStateMatches = + mPopupTrackInHierarchy == !WaylandPopupIsPermanent(); + + // Popup permanent state (noautohide attribute) can change during popup life. + if (mPopupTrackInHierarchyConfigured && permanentStateMatches) { + return mPopupTrackInHierarchy; + } + + // Configure persistent popup params only once. + // WaylandPopupIsAnchored() can give it wrong value after + // nsMenuPopupFrame::MoveTo() call which we use in move-to-rect callback + // to position popup after wayland position change. + if (!mPopupTrackInHierarchyConfigured) { + mPopupAnchored = WaylandPopupIsAnchored(); + mPopupContextMenu = WaylandPopupIsContextMenu(); + } + + LOG("nsWindow::WaylandPopupConfigure tracked %d anchored %d hint %d\n", + mPopupTrackInHierarchy, mPopupAnchored, int(mPopupHint)); + + // Permanent state changed and popup is mapped. + // We need to switch popup type but that's done when popup is mapped + // by Gtk so we need to unmap the popup here. + // It will be mapped again by gtk_widget_show(). + if (!permanentStateMatches && mIsMapped) { + LOG(" permanent state change from %d to %d, unmapping", + mPopupTrackInHierarchy, !WaylandPopupIsPermanent()); + gtk_widget_unmap(mShell); + } + + mPopupTrackInHierarchy = !WaylandPopupIsPermanent(); + LOG(" tracked in hierarchy %d\n", mPopupTrackInHierarchy); + + // See gdkwindow-wayland.c and + // should_map_as_popup()/should_map_as_subsurface() + GdkWindowTypeHint gtkTypeHint; + switch (mPopupHint) { + case PopupType::Menu: + // GDK_WINDOW_TYPE_HINT_POPUP_MENU is mapped as xdg_popup by default. + // We use this type for all menu popups. + gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU; + LOG(" popup type Menu"); + break; + case PopupType::Tooltip: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP; + LOG(" popup type Tooltip"); + break; + default: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY; + LOG(" popup type Utility"); + break; + } + + if (!mPopupTrackInHierarchy) { + // GDK_WINDOW_TYPE_HINT_UTILITY is mapped as wl_subsurface + // by default. + LOG(" not tracked in popup hierarchy, switch to Utility"); + gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY; + } + gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint); + + mPopupTrackInHierarchyConfigured = true; + return mPopupTrackInHierarchy; +} + +bool nsWindow::IsInPopupHierarchy() { + return mPopupTrackInHierarchy && mWaylandToplevel && mWaylandPopupPrev; +} + +void nsWindow::AddWindowToPopupHierarchy() { + LOG("nsWindow::AddWindowToPopupHierarchy\n"); + if (!GetFrame()) { + LOG(" Window without frame cannot be added as popup!\n"); + return; + } + + // Check if we're already in the hierarchy + if (!IsInPopupHierarchy()) { + mWaylandToplevel = WaylandPopupGetTopmostWindow(); + AppendPopupToHierarchyList(mWaylandToplevel); + } +} + +// 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. +void nsWindow::UpdateWaylandPopupHierarchy() { + LOG("nsWindow::UpdateWaylandPopupHierarchy\n"); + + // This popup hasn't been added to popup hierarchy yet so no need to + // do any configurations. + if (!IsInPopupHierarchy()) { + LOG(" popup isn't in hierarchy\n"); + return; + } + +#ifdef MOZ_LOGGING + LogPopupHierarchy(); + auto printPopupHierarchy = MakeScopeExit([&] { LogPopupHierarchy(); }); +#endif + + // Hide all tooltips without the last one. Tooltip can't be popup parent. + mWaylandToplevel->WaylandPopupHideTooltips(); + + // See Bug 1709254 / https://gitlab.gnome.org/GNOME/gtk/-/issues/5092 + // It's possible that Wayland compositor refuses to show + // a popup although Gtk claims it's visible. + // We don't know if the popup is shown or not. + // To avoid application crash refuse to create any child of such invisible + // popup and close any child of it now. + mWaylandToplevel->WaylandPopupCloseOrphanedPopups(); + + // Check if we have any remote content / overflow window in hierarchy. + // We can't attach such widget on top of other popup. + mWaylandToplevel->CloseAllPopupsBeforeRemotePopup(); + + // Check if your popup hierarchy matches layout hierarchy. + // For instance we should not connect hamburger menu on top + // of context menu. + // Close all popups from different layout chains if possible. + AutoTArray<nsIWidget*, 5> layoutPopupWidgetChain; + GetLayoutPopupWidgetChain(&layoutPopupWidgetChain); + + mWaylandToplevel->WaylandPopupHierarchyHideByLayout(&layoutPopupWidgetChain); + mWaylandToplevel->WaylandPopupHierarchyValidateByLayout( + &layoutPopupWidgetChain); + + // Now we have Popup hierarchy complete. + // Find first unchanged (and still open) popup to start with hierarchy + // changes. + nsWindow* changedPopup = mWaylandToplevel->mWaylandPopupNext; + while (changedPopup) { + // Stop when parent of this popup was changed and we need to recalc + // popup position. + if (changedPopup->mPopupChanged) { + break; + } + // Stop when this popup is closed. + if (changedPopup->mPopupClosed) { + break; + } + changedPopup = changedPopup->mWaylandPopupNext; + } + + // We don't need to recompute popup positions, quit now. + if (!changedPopup) { + LOG(" changed Popup is null, quit.\n"); + return; + } + + LOG(" first changed popup [%p]\n", (void*)changedPopup); + + // Hide parent popups if necessary (there are layout discontinuity) + // reposition the popup and show them again. + changedPopup->WaylandPopupHierarchyHideTemporary(); + + nsWindow* parentOfchangedPopup = nullptr; + if (changedPopup->mPopupClosed) { + parentOfchangedPopup = changedPopup->mWaylandPopupPrev; + } + changedPopup->WaylandPopupRemoveClosedPopups(); + + // It's possible that changedPopup was removed from widget hierarchy, + // in such case use child popup of the removed one if there's any. + if (!changedPopup->IsInPopupHierarchy()) { + if (!parentOfchangedPopup || !parentOfchangedPopup->mWaylandPopupNext) { + LOG(" last popup was removed, quit.\n"); + return; + } + changedPopup = parentOfchangedPopup->mWaylandPopupNext; + } + + GetLayoutPopupWidgetChain(&layoutPopupWidgetChain); + mWaylandToplevel->WaylandPopupHierarchyValidateByLayout( + &layoutPopupWidgetChain); + + changedPopup->WaylandPopupHierarchyCalculatePositions(); + + nsWindow* popup = changedPopup; + while (popup) { + const bool useMoveToRect = [&] { + if (!StaticPrefs::widget_wayland_use_move_to_rect_AtStartup()) { + return false; // Not available. + } + if (!popup->mPopupMatchesLayout) { + // We can use move_to_rect only when popups in popup hierarchy matches + // layout hierarchy as move_to_rect request that parent/child + // popups are adjacent. + return false; + } + if (popup->mPopupHint == PopupType::Panel && + popup->WaylandPopupIsFirst() && + popup->WaylandPopupFitsToplevelWindow(/* aMove */ true)) { + // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/1986 + // + // PopupType::Panel types are used for extension popups which may be + // resized. If such popup uses move-to-rect, we need to hide it before + // resize and show it again. That leads to massive flickering + // so use plain move if possible to avoid it. + // + // Bug 1760276 - don't use move-to-rect when popup is inside main + // Firefox window. + // + // Use it for first popups only due to another mutter bug + // https://gitlab.gnome.org/GNOME/gtk/-/issues/5089 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1784873 + return false; + } + if (!popup->WaylandPopupIsFirst() && + !popup->mWaylandPopupPrev->WaylandPopupIsFirst() && + !popup->mWaylandPopupPrev->mPopupUseMoveToRect) { + // We can't use move-to-rect if there are more parents of + // wl_subsurface popups types. + // + // It's because wl_subsurface is ignored by xgd_popup + // (created by move-to-rect) so our popup scenario: + // + // toplevel -> xgd_popup(1) -> wl_subsurface(2) -> xgd_popup(3) + // + // looks for Wayland compositor as: + // + // toplevel -> xgd_popup(1) -> xgd_popup(3) + // + // If xgd_popup(1) and xgd_popup(3) are not connected + // move-to-rect applied to xgd_popup(3) fails and we get missing popup. + return false; + } + return true; + }(); + + LOG(" popup [%p] matches layout [%d] anchored [%d] first popup [%d] use " + "move-to-rect %d\n", + popup, popup->mPopupMatchesLayout, popup->mPopupAnchored, + popup->WaylandPopupIsFirst(), useMoveToRect); + + popup->mPopupUseMoveToRect = useMoveToRect; + popup->WaylandPopupMoveImpl(); + popup->mPopupChanged = false; + popup = popup->mWaylandPopupNext; + } + + changedPopup->WaylandPopupHierarchyShowTemporaryHidden(); +} + +static void NativeMoveResizeCallback(GdkWindow* window, + const GdkRectangle* flipped_rect, + const GdkRectangle* final_rect, + gboolean flipped_x, gboolean flipped_y, + void* aWindow) { + LOG_POPUP("[%p] NativeMoveResizeCallback flipped_x %d flipped_y %d\n", + aWindow, flipped_x, flipped_y); + LOG_POPUP("[%p] new position [%d, %d] -> [%d x %d]", aWindow, + final_rect->x, final_rect->y, final_rect->width, + final_rect->height); + nsWindow* wnd = get_window_for_gdk_window(window); + + wnd->NativeMoveResizeWaylandPopupCallback(final_rect, flipped_x, flipped_y); +} + +// When popup is repositioned by widget code, we need to notify +// layout about it. It's because we control popup placement +// on widget on Wayland so layout may have old popup size/coordinates. +void nsWindow::WaylandPopupPropagateChangesToLayout(bool aMove, bool aResize) { + LOG("nsWindow::WaylandPopupPropagateChangesToLayout()"); + + if (aResize) { + LOG(" needSizeUpdate\n"); + if (nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame())) { + RefPtr<PresShell> presShell = popupFrame->PresShell(); + presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::None, + NS_FRAME_IS_DIRTY); + } + } + if (aMove) { + LOG(" needPositionUpdate, bounds [%d, %d]", mBounds.x, mBounds.y); + NotifyWindowMoved(mBounds.x, mBounds.y, ByMoveToRect::Yes); + } +} + +void nsWindow::NativeMoveResizeWaylandPopupCallback( + const GdkRectangle* aFinalSize, bool aFlippedX, bool aFlippedY) { + // We're getting move-to-rect callback without move-to-rect call. + // That indicates a compositor bug. It happens when a window is hidden and + // shown again before move-to-rect callback is fired. + // It may lead to incorrect popup placement as we may call + // gtk_window_move() between hide & show. + // See Bug 1777919, 1789581. +#if MOZ_LOGGING + if (!mWaitingForMoveToRectCallback) { + LOG(" Bogus move-to-rect callback! Expect wrong popup coordinates."); + } +#endif + + mWaitingForMoveToRectCallback = false; + + bool movedByLayout = mMovedAfterMoveToRect; + bool resizedByLayout = mResizedAfterMoveToRect; + + // Popup was moved between move-to-rect call and move-to-rect callback + // and the coordinates from move-to-rect callback are outdated. + if (movedByLayout || resizedByLayout) { + LOG(" Another move/resize called during waiting for callback\n"); + mMovedAfterMoveToRect = false; + mResizedAfterMoveToRect = false; + // Fire another round of move/resize to reflect latest request + // from layout. + NativeMoveResize(movedByLayout, resizedByLayout); + return; + } + + LOG(" orig mBounds [%d, %d] -> [%d x %d]\n", mBounds.x, mBounds.y, + mBounds.width, mBounds.height); + + LayoutDeviceIntRect newBounds = [&] { + GdkRectangle finalRect = *aFinalSize; + GdkPoint parent = WaylandGetParentPosition(); + finalRect.x += parent.x; + finalRect.y += parent.y; + return GdkRectToDevicePixels(finalRect); + }(); + + LOG(" new mBounds [%d, %d] -> [%d x %d]", newBounds.x, newBounds.y, + newBounds.width, newBounds.height); + + bool needsPositionUpdate = newBounds.TopLeft() != mBounds.TopLeft(); + bool needsSizeUpdate = newBounds.Size() != mLastSizeRequest; + + if (needsSizeUpdate) { + // Wayland compositor changed popup size request from layout. + // Set the constraints to use them in nsMenuPopupFrame::SetPopupPosition(). + // Beware that gtk_window_resize() requests sizes asynchronously and so + // newBounds might not have the size from the most recent + // gtk_window_resize(). + if (newBounds.width < mLastSizeRequest.width) { + mMoveToRectPopupSize.width = newBounds.width; + } + if (newBounds.height < mLastSizeRequest.height) { + mMoveToRectPopupSize.height = newBounds.height; + } + LOG(" mMoveToRectPopupSize set to [%d, %d]", mMoveToRectPopupSize.width, + mMoveToRectPopupSize.height); + } + mBounds = newBounds; + // Check mBounds size + if (mCompositorSession && + !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) { + gfxCriticalNoteOnce << "Invalid mBounds in PopupCallback " << mBounds + << " size state " << mSizeMode; + } + WaylandPopupPropagateChangesToLayout(needsPositionUpdate, needsSizeUpdate); +} + +static GdkGravity PopupAlignmentToGdkGravity(int8_t aAlignment) { + switch (aAlignment) { + case POPUPALIGNMENT_NONE: + return GDK_GRAVITY_NORTH_WEST; + case POPUPALIGNMENT_TOPLEFT: + return GDK_GRAVITY_NORTH_WEST; + case POPUPALIGNMENT_TOPRIGHT: + return GDK_GRAVITY_NORTH_EAST; + case POPUPALIGNMENT_BOTTOMLEFT: + return GDK_GRAVITY_SOUTH_WEST; + case POPUPALIGNMENT_BOTTOMRIGHT: + return GDK_GRAVITY_SOUTH_EAST; + case POPUPALIGNMENT_LEFTCENTER: + return GDK_GRAVITY_WEST; + case POPUPALIGNMENT_RIGHTCENTER: + return GDK_GRAVITY_EAST; + case POPUPALIGNMENT_TOPCENTER: + return GDK_GRAVITY_NORTH; + case POPUPALIGNMENT_BOTTOMCENTER: + return GDK_GRAVITY_SOUTH; + } + return GDK_GRAVITY_STATIC; +} + +bool nsWindow::IsPopupDirectionRTL() { + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + return popupFrame && popupFrame->IsDirectionRTL(); +} + +// Position the popup directly by gtk_window_move() and try to keep it +// on screen by just moving it in scope of it's parent window. +// +// It's used when we position noautihode popup and we don't use xdg_positioner. +// See Bug 1718867 +void nsWindow::WaylandPopupSetDirectPosition() { + GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft()); + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest); + + LOG("nsWindow::WaylandPopupSetDirectPosition %d,%d -> %d x %d\n", topLeft.x, + topLeft.y, size.width, size.height); + + mPopupPosition = {topLeft.x, topLeft.y}; + + if (mIsDragPopup) { + gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + // DND window is placed inside container so we need to make hard size + // request to ensure parent container is resized too. + gtk_widget_set_size_request(GTK_WIDGET(mShell), size.width, size.height); + return; + } + + GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + nsWindow* window = get_window_for_gtk_widget(GTK_WIDGET(parentGtkWindow)); + if (!window) { + return; + } + GdkWindow* gdkWindow = + gtk_widget_get_window(GTK_WIDGET(window->GetMozContainer())); + + int parentWidth = gdk_window_get_width(gdkWindow); + int popupWidth = size.width; + + int x; + gdk_window_get_position(gdkWindow, &x, nullptr); + + // If popup is bigger than main window just center it. + if (popupWidth > parentWidth) { + mPopupPosition.x = -(parentWidth - popupWidth) / 2 + x; + } else { + if (IsPopupDirectionRTL()) { + // Stick with right window edge + if (mPopupPosition.x < x) { + mPopupPosition.x = x; + } + } else { + // Stick with left window edge + if (mPopupPosition.x + popupWidth > parentWidth + x) { + mPopupPosition.x = parentWidth + x - popupWidth; + } + } + } + + LOG(" set position [%d, %d]\n", mPopupPosition.x, mPopupPosition.y); + gtk_window_move(GTK_WINDOW(mShell), mPopupPosition.x, mPopupPosition.y); + + LOG(" set size [%d, %d]\n", size.width, size.height); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + + if (mPopupPosition.x != topLeft.x) { + mBounds.MoveTo(GdkPointToDevicePixels(mPopupPosition)); + LOG(" setting new bounds [%d, %d]\n", mBounds.x, mBounds.y); + WaylandPopupPropagateChangesToLayout(/* move */ true, /* resize */ false); + } +} + +bool nsWindow::WaylandPopupFitsToplevelWindow(bool aMove) { + LOG("nsWindow::WaylandPopupFitsToplevelWindow() move %d", aMove); + + GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + GtkWindow* tmp = parent; + while ((tmp = gtk_window_get_transient_for(GTK_WINDOW(parent)))) { + parent = tmp; + } + GdkWindow* toplevelGdkWindow = gtk_widget_get_window(GTK_WIDGET(parent)); + + if (!toplevelGdkWindow) { + NS_WARNING("Toplevel widget without GdkWindow?"); + return false; + } + + int parentWidth = gdk_window_get_width(toplevelGdkWindow); + int parentHeight = gdk_window_get_height(toplevelGdkWindow); + LOG(" parent size %d x %d", parentWidth, parentHeight); + + GdkPoint topLeft = aMove ? mPopupPosition + : DevicePixelsToGdkPointRoundDown(mBounds.TopLeft()); + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest); + LOG(" popup topleft %d, %d size %d x %d", topLeft.x, topLeft.y, size.width, + size.height); + int fits = topLeft.x >= 0 && topLeft.y >= 0 && + topLeft.x + size.width <= parentWidth && + topLeft.y + size.height <= parentHeight; + + LOG(" fits %d", fits); + return fits; +} + +void nsWindow::NativeMoveResizeWaylandPopup(bool aMove, bool aResize) { + GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft()); + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest); + + LOG("nsWindow::NativeMoveResizeWaylandPopup Bounds %d,%d -> %d x %d move %d " + "resize %d\n", + topLeft.x, topLeft.y, size.width, size.height, aMove, aResize); + + // 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", + mLastSizeRequest.width, mLastSizeRequest.height); + return; + } + + if (mWaitingForMoveToRectCallback) { + LOG(" waiting for move to rect, scheduling"); + // mBounds position must not be overwritten before it is applied. + // OnConfigureEvent() will not set mBounds to an old position for + // GTK_WINDOW_POPUP. + MOZ_ASSERT(gtk_window_get_window_type(GTK_WINDOW(mShell)) == + GTK_WINDOW_POPUP); + mMovedAfterMoveToRect = aMove; + mResizedAfterMoveToRect = aResize; + return; + } + + mMovedAfterMoveToRect = false; + mResizedAfterMoveToRect = false; + + bool trackedInHierarchy = WaylandPopupConfigure(); + + // Read popup position from layout if it was moved or newly created. + // This position is used by move-to-rect method as we need anchor and other + // info to place popup correctly. + // We need WaylandPopupConfigure() to be called before to have all needed + // popup info in place (mainly the anchored flag). + if (aMove) { + mPopupMoveToRectParams = WaylandPopupGetPositionFromLayout(); + } + + if (!trackedInHierarchy) { + WaylandPopupSetDirectPosition(); + return; + } + + if (aResize) { + LOG(" set size [%d, %d]\n", size.width, size.height); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + } + + if (!aMove && WaylandPopupFitsToplevelWindow(aMove)) { + // Popup position has not been changed and its position/size fits + // parent window so no need to reposition the window. + LOG(" fits parent window size, just resize\n"); + return; + } + + // Mark popup as changed as we're updating position/size. + mPopupChanged = true; + + // Save popup position for former re-calculations when popup hierarchy + // is changed. + LOG(" popup position changed from [%d, %d] to [%d, %d]\n", mPopupPosition.x, + mPopupPosition.y, topLeft.x, topLeft.y); + mPopupPosition = {topLeft.x, topLeft.y}; + + UpdateWaylandPopupHierarchy(); +} + +struct PopupSides { + Maybe<Side> mVertical; + Maybe<Side> mHorizontal; +}; + +static PopupSides SidesForPopupAlignment(int8_t aAlignment) { + switch (aAlignment) { + case POPUPALIGNMENT_NONE: + break; + case POPUPALIGNMENT_TOPLEFT: + return {Some(eSideTop), Some(eSideLeft)}; + case POPUPALIGNMENT_TOPRIGHT: + return {Some(eSideTop), Some(eSideRight)}; + case POPUPALIGNMENT_BOTTOMLEFT: + return {Some(eSideBottom), Some(eSideLeft)}; + case POPUPALIGNMENT_BOTTOMRIGHT: + return {Some(eSideBottom), Some(eSideRight)}; + case POPUPALIGNMENT_LEFTCENTER: + return {Nothing(), Some(eSideLeft)}; + case POPUPALIGNMENT_RIGHTCENTER: + return {Nothing(), Some(eSideRight)}; + case POPUPALIGNMENT_TOPCENTER: + return {Some(eSideTop), Nothing()}; + case POPUPALIGNMENT_BOTTOMCENTER: + return {Some(eSideBottom), Nothing()}; + } + return {}; +} + +// We want to apply margins based on popup alignment (which would generally be +// just an offset to apply to the popup). However, to deal with flipping +// correctly, we apply the margin to the anchor when possible. +struct ResolvedPopupMargin { + // A margin to be applied to the anchor. + nsMargin mAnchorMargin; + // An offset in app units to be applied to the popup for when we need to tell + // GTK to center inside the anchor precisely (so we can't really do better in + // presence of flips). + nsPoint mPopupOffset; +}; + +static ResolvedPopupMargin ResolveMargin(nsMenuPopupFrame* aFrame, + int8_t aPopupAlign, + int8_t aAnchorAlign, + bool aAnchoredToPoint, + bool aIsContextMenu) { + nsMargin margin = aFrame->GetMargin(); + nsPoint offset; + + if (aAnchoredToPoint) { + // Since GTK doesn't allow us to specify margins itself, when anchored to a + // point we can just assume we'll be aligned correctly... This is kind of + // annoying but alas. + // + // This calculation must match the relevant unanchored popup calculation in + // nsMenuPopupFrame::SetPopupPosition(), which should itself be the inverse + // inverse of nsMenuPopupFrame::MoveTo(). + if (aIsContextMenu && aFrame->IsDirectionRTL()) { + offset.x = -margin.right; + } else { + offset.x = margin.left; + } + offset.y = margin.top; + return {nsMargin(), offset}; + } + + auto popupSides = SidesForPopupAlignment(aPopupAlign); + auto anchorSides = SidesForPopupAlignment(aAnchorAlign); + // Matched sides: Invert the margin, so that we pull in the right direction. + // Popup not aligned to any anchor side: We give up and use the offset, + // applying the margin from the popup side. + // Mismatched sides: We swap the margins so that we pull in the right + // direction, e.g. margin-left: -10px should shrink 10px the _right_ of the + // box, not the left of the box. + if (popupSides.mHorizontal == anchorSides.mHorizontal) { + margin.left = -margin.left; + margin.right = -margin.right; + } else if (!anchorSides.mHorizontal) { + auto popupSide = *popupSides.mHorizontal; + offset.x += popupSide == eSideRight ? -margin.Side(popupSide) + : margin.Side(popupSide); + margin.left = margin.right = 0; + } else { + std::swap(margin.left, margin.right); + } + + // Same logic as above, but in the vertical direction. + if (popupSides.mVertical == anchorSides.mVertical) { + margin.top = -margin.top; + margin.bottom = -margin.bottom; + } else if (!anchorSides.mVertical) { + auto popupSide = *popupSides.mVertical; + offset.y += popupSide == eSideBottom ? -margin.Side(popupSide) + : margin.Side(popupSide); + margin.top = margin.bottom = 0; + } else { + std::swap(margin.top, margin.bottom); + } + + return {margin, offset}; +} + +#ifdef MOZ_LOGGING +void nsWindow::LogPopupAnchorHints(int aHints) { + static struct hints_ { + int hint; + char name[100]; + } hints[] = { + {GDK_ANCHOR_FLIP_X, "GDK_ANCHOR_FLIP_X"}, + {GDK_ANCHOR_FLIP_Y, "GDK_ANCHOR_FLIP_Y"}, + {GDK_ANCHOR_SLIDE_X, "GDK_ANCHOR_SLIDE_X"}, + {GDK_ANCHOR_SLIDE_Y, "GDK_ANCHOR_SLIDE_Y"}, + {GDK_ANCHOR_RESIZE_X, "GDK_ANCHOR_RESIZE_X"}, + {GDK_ANCHOR_RESIZE_Y, "GDK_ANCHOR_RESIZE_X"}, + }; + + LOG(" PopupAnchorHints"); + for (const auto& hint : hints) { + if (hint.hint & aHints) { + LOG(" %s", hint.name); + } + } +} + +void nsWindow::LogPopupGravity(GdkGravity aGravity) { + static char gravity[][100]{"NONE", + "GDK_GRAVITY_NORTH_WEST", + "GDK_GRAVITY_NORTH", + "GDK_GRAVITY_NORTH_EAST", + "GDK_GRAVITY_WEST", + "GDK_GRAVITY_CENTER", + "GDK_GRAVITY_EAST", + "GDK_GRAVITY_SOUTH_WEST", + "GDK_GRAVITY_SOUTH", + "GDK_GRAVITY_SOUTH_EAST", + "GDK_GRAVITY_STATIC"}; + LOG(" %s", gravity[aGravity]); +} +#endif + +const nsWindow::WaylandPopupMoveToRectParams +nsWindow::WaylandPopupGetPositionFromLayout() { + LOG("nsWindow::WaylandPopupGetPositionFromLayout\n"); + + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + + const bool isTopContextMenu = mPopupContextMenu && !mPopupAnchored; + const bool isRTL = IsPopupDirectionRTL(); + const bool anchored = popupFrame->IsAnchored(); + int8_t popupAlign = POPUPALIGNMENT_TOPLEFT; + int8_t anchorAlign = POPUPALIGNMENT_BOTTOMRIGHT; + if (anchored) { + // See nsMenuPopupFrame::AdjustPositionForAnchorAlign. + popupAlign = popupFrame->GetPopupAlignment(); + anchorAlign = popupFrame->GetPopupAnchor(); + } + if (isRTL && (anchored || isTopContextMenu)) { + popupAlign = -popupAlign; + anchorAlign = -anchorAlign; + } + + // Although we have mPopupPosition / mRelativePopupPosition here + // we can't use it. move-to-rect needs anchor rectangle to position a popup + // but we have only a point from Resize(). + // + // So we need to extract popup position from nsMenuPopupFrame() and duplicate + // the layout work here. + LayoutDeviceIntRect anchorRect; + ResolvedPopupMargin popupMargin; + { + nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect(); + // This is a somewhat hacky way of applying the popup margin. We don't know + // if GTK will end up flipping the popup, in which case the offset we + // compute is just wrong / applied to the wrong side. + // + // Instead, we tell it to anchor us at a smaller or bigger rect depending on + // the margin, which achieves the same result if the popup is positioned + // correctly, but doesn't misposition the popup when flipped across the + // anchor. + popupMargin = ResolveMargin(popupFrame, popupAlign, anchorAlign, + anchorRectAppUnits.IsEmpty(), isTopContextMenu); + LOG(" layout popup CSS anchor (%d, %d) %s, margin %s offset %s\n", + popupAlign, anchorAlign, ToString(anchorRectAppUnits).c_str(), + ToString(popupMargin.mAnchorMargin).c_str(), + ToString(popupMargin.mPopupOffset).c_str()); + anchorRectAppUnits.Inflate(popupMargin.mAnchorMargin); + LOG(" after margins %s\n", ToString(anchorRectAppUnits).c_str()); + nscoord auPerDev = popupFrame->PresContext()->AppUnitsPerDevPixel(); + anchorRect = LayoutDeviceIntRect::FromAppUnitsToNearest(anchorRectAppUnits, + auPerDev); + if (anchorRect.width < 0) { + auto w = -anchorRect.width; + anchorRect.width += w + 1; + anchorRect.x += w; + } + LOG(" final %s\n", ToString(anchorRect).c_str()); + } + + LOG(" relative popup rect position [%d, %d] -> [%d x %d]\n", anchorRect.x, + anchorRect.y, anchorRect.width, anchorRect.height); + + // Get gravity and flip type + GdkGravity rectAnchor = PopupAlignmentToGdkGravity(anchorAlign); + GdkGravity menuAnchor = PopupAlignmentToGdkGravity(popupAlign); + + LOG(" parentRect gravity: %d anchor gravity: %d\n", rectAnchor, menuAnchor); + + // Gtk default is: GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE | GDK_ANCHOR_RESIZE. + // We want to SLIDE_X menu on the dual monitor setup rather than resize it + // on the other monitor. + GdkAnchorHints hints = + GdkAnchorHints(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_RESIZE); + + // slideHorizontal from nsMenuPopupFrame::SetPopupPosition + int8_t position = popupFrame->GetAlignmentPosition(); + 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); + } + + FlipType flipType = popupFrame->GetFlipType(); + if (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 (!WaylandPopupIsMenu()) { + // we don't want to slide menus to fit the screen rather resize them + hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE); + } + + // We want tooltips to flip verticaly or slide only. + // See nsMenuPopupFrame::SetPopupPosition(). + // https://searchfox.org/mozilla-central/rev/d0f5bc50aff3462c9d1546b88d60c5cb020eb15c/layout/xul/nsMenuPopupFrame.cpp#1603 + if (mPopupType == PopupType::Tooltip) { + hints = GdkAnchorHints(GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE); + } + + return { + anchorRect, + rectAnchor, + menuAnchor, + hints, + DevicePixelsToGdkPointRoundDown(LayoutDevicePoint::FromAppUnitsToNearest( + popupMargin.mPopupOffset, + popupFrame->PresContext()->AppUnitsPerDevPixel())), + true}; +} + +bool nsWindow::WaylandPopupAnchorAdjustForParentPopup( + GdkRectangle* aPopupAnchor, GdkPoint* aOffset) { + LOG("nsWindow::WaylandPopupAnchorAdjustForParentPopup"); + + GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell)); + if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) { + NS_WARNING("Popup has no parent!"); + return false; + } + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(parentGtkWindow)); + if (!window) { + NS_WARNING("Popup parrent is not mapped!"); + return false; + } + + GdkRectangle parentWindowRect = {0, 0, gdk_window_get_width(window), + gdk_window_get_height(window)}; + LOG(" parent window size %d x %d", parentWindowRect.width, + parentWindowRect.height); + + // We can't have rectangle anchor with zero width/height. + if (!aPopupAnchor->width) { + aPopupAnchor->width = 1; + } + if (!aPopupAnchor->height) { + aPopupAnchor->height = 1; + } + + GdkRectangle finalRect; + if (!gdk_rectangle_intersect(aPopupAnchor, &parentWindowRect, &finalRect)) { + return false; + } + *aPopupAnchor = finalRect; + LOG(" anchor is correct %d,%d -> %d x %d", finalRect.x, finalRect.y, + finalRect.width, finalRect.height); + + *aOffset = mPopupMoveToRectParams.mOffset; + LOG(" anchor offset %d, %d", aOffset->x, aOffset->y); + return true; +} + +bool nsWindow::WaylandPopupCheckAndGetAnchor(GdkRectangle* aPopupAnchor, + GdkPoint* aOffset) { + LOG("nsWindow::WaylandPopupCheckAndGetAnchor"); + + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(mShell)); + nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); + if (!gdkWindow || !popupFrame) { + LOG(" can't use move-to-rect due missing gdkWindow or popupFrame"); + return false; + } + + if (popupFrame->IsFlippedByLayout()) { + LOG(" can't use move-to-rect, flipped / constrained by layout"); + return false; + } + + if (!mPopupMoveToRectParams.mAnchorSet) { + LOG(" can't use move-to-rect due missing anchor"); + return false; + } + // Update popup layout coordinates from layout by recent popup hierarchy + // (calculate correct position according to parent window) + // and convert to Gtk coordinates. + LayoutDeviceIntRect anchorRect = mPopupMoveToRectParams.mAnchorRect; + if (!WaylandPopupIsFirst()) { + GdkPoint parent = WaylandGetParentPosition(); + LOG(" subtract parent position from anchor [%d, %d]\n", parent.x, + parent.y); + anchorRect.MoveBy(-GdkPointToDevicePixels(parent)); + } + + *aPopupAnchor = DevicePixelsToGdkRectRoundOut(anchorRect); + LOG(" anchored to rectangle [%d, %d] -> [%d x %d]", aPopupAnchor->x, + aPopupAnchor->y, aPopupAnchor->width, aPopupAnchor->height); + + if (!WaylandPopupAnchorAdjustForParentPopup(aPopupAnchor, aOffset)) { + LOG(" can't use move-to-rect, anchor is not placed inside of parent " + "window"); + return false; + } + + return true; +} + +void nsWindow::WaylandPopupPrepareForMove() { + LOG("nsWindow::WaylandPopupPrepareForMove()"); + + if (mPopupHint == PopupType::Tooltip) { + // Don't fiddle with tooltips type, just hide it before move-to-rect + if (mPopupUseMoveToRect && gtk_widget_is_visible(mShell)) { + HideWaylandPopupWindow(/* aTemporaryHide */ true, + /* aRemoveFromPopupList */ false); + } + LOG(" it's tooltip, quit"); + return; + } + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1785185#c8 + // gtk_window_move() needs GDK_WINDOW_TYPE_HINT_UTILITY popup type. + // move-to-rect requires GDK_WINDOW_TYPE_HINT_POPUP_MENU popups type. + // We need to set it before map event when popup is hidden. + const GdkWindowTypeHint currentType = + gtk_window_get_type_hint(GTK_WINDOW(mShell)); + const GdkWindowTypeHint requiredType = mPopupUseMoveToRect + ? GDK_WINDOW_TYPE_HINT_POPUP_MENU + : GDK_WINDOW_TYPE_HINT_UTILITY; + + if (!mPopupUseMoveToRect && currentType == requiredType) { + LOG(" type matches and we're not forced to hide it, quit."); + return; + } + + if (gtk_widget_is_visible(mShell)) { + HideWaylandPopupWindow(/* aTemporaryHide */ true, + /* aRemoveFromPopupList */ false); + } + + if (currentType != requiredType) { + LOG(" set type %s", + requiredType == GDK_WINDOW_TYPE_HINT_POPUP_MENU ? "MENU" : "UTILITY"); + gtk_window_set_type_hint(GTK_WINDOW(mShell), requiredType); + } +} + +// Plain popup move on Wayland - simply place popup on given location. +// We can't just call gtk_window_move() as it's not effective on visible +// popups. +void nsWindow::WaylandPopupMovePlain(int aX, int aY) { + LOG("nsWindow::WaylandPopupMovePlain(%d, %d)", aX, aY); + + // We can directly move only popups based on wl_subsurface type. + MOZ_DIAGNOSTIC_ASSERT(gtk_window_get_type_hint(GTK_WINDOW(mShell)) == + GDK_WINDOW_TYPE_HINT_UTILITY || + gtk_window_get_type_hint(GTK_WINDOW(mShell)) == + GDK_WINDOW_TYPE_HINT_TOOLTIP); + + gtk_window_move(GTK_WINDOW(mShell), aX, aY); + + // gtk_window_move() can trick us. When widget is hidden gtk_window_move() + // does not move the widget but sets new widget coordinates when widget + // is mapped again. + // + // If popup used move-to-rect before + // (GdkWindow has POSITION_METHOD_MOVE_TO_RECT set), popup will use + // move-to-rect again when it's mapped and we'll get bogus move-to-rect + // callback. + // + // gdk_window_move() sets position_method to POSITION_METHOD_MOVE_RESIZE + // so we'll use plain move when popup is shown. + if (!gtk_widget_get_mapped(mShell)) { + GdkWindow* window = gtk_widget_get_window(mShell); + if (window) { + gdk_window_move(window, aX, aY); + } + } +} + +void nsWindow::WaylandPopupMoveImpl() { + // 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"); + + if (mPopupUseMoveToRect && !sGdkWindowMoveToRect) { + LOG("can't use move-to-rect due missing gdk_window_move_to_rect()"); + mPopupUseMoveToRect = false; + } + + GdkRectangle gtkAnchorRect; + GdkPoint offset; + if (mPopupUseMoveToRect) { + mPopupUseMoveToRect = + WaylandPopupCheckAndGetAnchor(>kAnchorRect, &offset); + } + + LOG("nsWindow::WaylandPopupMove"); + LOG(" original widget popup position [%d, %d]\n", mPopupPosition.x, + mPopupPosition.y); + LOG(" relative widget popup position [%d, %d]\n", mRelativePopupPosition.x, + mRelativePopupPosition.y); + LOG(" popup use move to rect %d", mPopupUseMoveToRect); + + WaylandPopupPrepareForMove(); + + if (!mPopupUseMoveToRect) { + WaylandPopupMovePlain(mRelativePopupPosition.x, mRelativePopupPosition.y); + // Layout already should be aware of our bounds, since we didn't change it + // from the widget side for flipping or so. + return; + } + + // Correct popup position now. It will be updated by gdk_window_move_to_rect() + // anyway but we need to set it now to avoid a race condition here. + WaylandPopupRemoveNegativePosition(); + + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(mShell)); + if (!g_signal_handler_find(gdkWindow, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + FuncToGpointer(NativeMoveResizeCallback), this)) { + g_signal_connect(gdkWindow, "moved-to-rect", + G_CALLBACK(NativeMoveResizeCallback), this); + } + mWaitingForMoveToRectCallback = true; + +#ifdef MOZ_LOGGING + if (LOG_ENABLED()) { + LOG(" Call move-to-rect"); + LOG(" Anchor rect [%d, %d] -> [%d x %d]", gtkAnchorRect.x, gtkAnchorRect.y, + gtkAnchorRect.width, gtkAnchorRect.height); + LOG(" Offset [%d, %d]", offset.x, offset.y); + LOG(" AnchorType"); + LogPopupGravity(mPopupMoveToRectParams.mAnchorRectType); + LOG(" PopupAnchorType"); + LogPopupGravity(mPopupMoveToRectParams.mPopupAnchorType); + LogPopupAnchorHints(mPopupMoveToRectParams.mHints); + } +#endif + + sGdkWindowMoveToRect(gdkWindow, >kAnchorRect, + mPopupMoveToRectParams.mAnchorRectType, + mPopupMoveToRectParams.mPopupAnchorType, + mPopupMoveToRectParams.mHints, offset.x, offset.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 (!mGdkWindow) { + return; + } + + 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 %d\n", aMode); + + // Return if there's no shell or our current state is the same as the mode we + // were just set to. + if (!mShell) { + LOG(" no shell"); + return; + } + + if (mSizeMode == aMode && mLastSizeModeRequest == aMode) { + LOG(" already set"); + return; + } + + // It is tempting to try to optimize calls below based only on current + // mSizeMode, but that wouldn't work if there's a size-request in flight + // (specially before show). See bug 1789823. + const auto SizeModeMightBe = [&](nsSizeMode aModeToTest) { + if (mSizeMode != mLastSizeModeRequest) { + // Arbitrary size mode requests might be ongoing. + return true; + } + return mSizeMode == aModeToTest; + }; + + if (aMode != nsSizeMode_Fullscreen && aMode != nsSizeMode_Minimized) { + // Fullscreen and minimized are compatible. + if (SizeModeMightBe(nsSizeMode_Fullscreen)) { + MakeFullScreen(false); + } + } + + 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: + MOZ_FALLTHROUGH_ASSERT("Unknown size mode"); + case nsSizeMode_Normal: + LOG(" set normal"); + if (SizeModeMightBe(nsSizeMode_Maximized)) { + gtk_window_unmaximize(GTK_WINDOW(mShell)); + } + if (SizeModeMightBe(nsSizeMode_Minimized)) { + gtk_window_deiconify(GTK_WINDOW(mShell)); + // We need this for actual deiconification on mutter. + gtk_window_present(GTK_WINDOW(mShell)); + } + break; + } + mLastSizeModeRequest = aMode; +} + +#define kDesktopMutterSchema "org.gnome.mutter"_ns +#define kDesktopDynamicWorkspacesKey "dynamic-workspaces"_ns + +static bool WorkspaceManagementDisabled(GdkScreen* screen) { + if (Preferences::GetBool("widget.disable-workspace-management", false)) { + return true; + } + if (Preferences::HasUserValue("widget.workspace-management")) { + return Preferences::GetBool("widget.workspace-management"); + } + + if (IsGnomeDesktopEnvironment()) { + // 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(kDesktopMutterSchema, + getter_AddRefs(mutterSettings)); + if (mutterSettings) { + mutterSettings->GetBoolean(kDesktopDynamicWorkspacesKey, + &usesDynamicWorkspaces); + } + } + return usesDynamicWorkspaces; + } + + const auto& desktop = GetDesktopEnvironmentIdentifier(); + return desktop.EqualsLiteral("bspwm") || desktop.EqualsLiteral("i3"); +} + +void nsWindow::GetWorkspaceID(nsAString& workspaceID) { + workspaceID.Truncate(); + + if (!GdkIsX11Display() || !mShell) { + return; + } + +#ifdef MOZ_X11 + LOG("nsWindow::GetWorkspaceID()\n"); + + // Get the gdk window for this widget. + GdkWindow* gdk_window = gtk_widget_get_window(mShell); + if (!gdk_window) { + LOG(" missing Gdk window, quit."); + return; + } + + if (WorkspaceManagementDisabled(gdk_window_get_screen(gdk_window))) { + LOG(" WorkspaceManagementDisabled, quit."); + 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)) { + LOG(" gdk_property_get() failed, quit."); + return; + } + + LOG(" got workspace ID %d", (int32_t)wm_desktop[0]); + workspaceID.AppendInt((int32_t)wm_desktop[0]); + g_free(wm_desktop); +#endif +} + +void nsWindow::MoveToWorkspace(const nsAString& workspaceIDStr) { + nsresult rv = NS_OK; + int32_t workspaceID = workspaceIDStr.ToInteger(&rv); + + LOG("nsWindow::MoveToWorkspace() ID %d", workspaceID); + if (NS_FAILED(rv) || !workspaceID || !GdkIsX11Display() || !mShell) { + LOG(" MoveToWorkspace disabled, quit"); + return; + } + +#ifdef MOZ_X11 + // Get the gdk window for this widget. + GdkWindow* gdk_window = gtk_widget_get_window(mShell); + if (!gdk_window) { + LOG(" failed to get GdkWindow, quit."); + 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); + LOG(" moved to workspace"); +#endif +} + +void nsWindow::SetUserTimeAndStartupTokenForActivatedWindow() { + nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit(); + if (!toolkit || MOZ_UNLIKELY(mWindowType == WindowType::Invisible)) { + return; + } + + mWindowActivationTokenFromEnv = toolkit->GetStartupToken(); + if (!mWindowActivationTokenFromEnv.IsEmpty()) { + if (!GdkIsWaylandDisplay()) { + gtk_window_set_startup_id(GTK_WINDOW(mShell), + mWindowActivationTokenFromEnv.get()); + // In the case of X11, the above call is all we need. For wayland we need + // to keep the token around until we take it in + // TransferFocusToWaylandWindow. + mWindowActivationTokenFromEnv.Truncate(); + } + } else if (uint32_t timestamp = toolkit->GetFocusTimestamp()) { + // 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. + gdk_window_focus(gtk_widget_get_window(mShell), timestamp); + } + + // 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 + toolkit->SetFocusTimestamp(0); + toolkit->SetStartupToken(""_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. +#ifdef MOZ_X11 + GdkDisplay* gdkDisplay = gdk_display_get_default(); + guint32 timestamp = GdkIsX11Display(gdkDisplay) + ? gdk_x11_display_get_user_time(gdkDisplay) + : gtk_get_current_event_time(); +#else + guint32 timestamp = gtk_get_current_event_time(); +#endif + + if (sLastUserInputTime != GDK_CURRENT_TIME && + TimestampIsNewerThan(sLastUserInputTime, timestamp)) { + return sLastUserInputTime; + } + + return timestamp; +} + +#ifdef MOZ_WAYLAND +void nsWindow::FocusWaylandWindow(const char* aTokenID) { + MOZ_DIAGNOSTIC_ASSERT(aTokenID); + + LOG("nsWindow::FocusWaylandWindow(%s)", aTokenID); + if (IsDestroyed()) { + LOG(" already destroyed, quit."); + return; + } + wl_surface* surface = + mGdkWindow ? gdk_wayland_window_get_wl_surface(mGdkWindow) : nullptr; + if (!surface) { + LOG(" mGdkWindow is not visible, quit."); + return; + } + + LOG(" requesting xdg-activation, surface ID %d", + wl_proxy_get_id((struct wl_proxy*)surface)); + xdg_activation_v1* xdg_activation = WaylandDisplayGet()->GetXdgActivation(); + if (!xdg_activation) { + return; + } + xdg_activation_v1_activate(xdg_activation, aTokenID, surface); +} + +// Transfer focus from gFocusWindow to aWindow and use xdg_activation +// protocol for it. +void nsWindow::TransferFocusToWaylandWindow(nsWindow* aWindow) { + LOGW("nsWindow::TransferFocusToWaylandWindow(%p) gFocusWindow %p", aWindow, + gFocusWindow); + auto promise = mozilla::widget::RequestWaylandFocusPromise(); + if (NS_WARN_IF(!promise)) { + LOGW(" quit, failed to create TransferFocusToWaylandWindow [%p]", aWindow); + return; + } + promise->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolve */ + [window = RefPtr{aWindow}](nsCString token) { + window->FocusWaylandWindow(token.get()); + }, + /* reject */ + [window = RefPtr{aWindow}](bool state) { + LOGW("TransferFocusToWaylandWindow [%p] failed", window.get()); + }); +} +#endif + +// Request activation of this window or give focus to this widget. +// aRaise means whether we should request activation of this widget's +// toplevel window. +// +// nsWindow::SetFocus(Raise::Yes) - Raise and give focus to toplevel window. +// nsWindow::SetFocus(Raise::No) - Give focus to this window. +void nsWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) { + LOG("nsWindow::SetFocus Raise %d\n", aRaise == Raise::Yes); + + // Raise the window if someone passed in true and the prefs are + // set properly. + GtkWidget* toplevelWidget = gtk_widget_get_toplevel(GTK_WIDGET(mContainer)); + + LOG(" gFocusWindow [%p]\n", gFocusWindow); + LOG(" mContainer [%p]\n", GTK_WIDGET(mContainer)); + LOG(" Toplevel widget [%p]\n", toplevelWidget); + + // 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. + if (StaticPrefs::mozilla_widget_raise_on_setfocus_AtStartup() && + aRaise == Raise::Yes && toplevelWidget && + !gtk_widget_has_focus(toplevelWidget)) { + if (gtk_widget_get_visible(mShell)) { + LOG(" toplevel is not focused"); + gdk_window_show_unraised(gtk_widget_get_window(mShell)); + // Unset the urgency hint if possible. + SetUrgencyHint(mShell, false); + } + } + + RefPtr<nsWindow> toplevelWindow = get_window_for_gtk_widget(toplevelWidget); + if (!toplevelWindow) { + LOG(" missing toplevel nsWindow, quit\n"); + 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 (StaticPrefs::mozilla_widget_raise_on_setfocus_AtStartup() && + toplevelWindow->mIsShown && toplevelWindow->mShell && + !gtk_window_is_active(GTK_WINDOW(toplevelWindow->mShell))) { + LOG(" toplevel is visible but not active, requesting activation [%p]", + toplevelWindow.get()); + + // Take the time here explicitly for the call below. + const uint32_t timestamp = [&] { + if (nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit()) { + if (uint32_t t = toolkit->GetFocusTimestamp()) { + toolkit->SetFocusTimestamp(0); + return t; + } + } + return GetLastUserInputTime(); + }(); + + toplevelWindow->SetUserTimeAndStartupTokenForActivatedWindow(); + gtk_window_present_with_time(GTK_WINDOW(toplevelWindow->mShell), + timestamp); + +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + auto existingToken = + std::move(toplevelWindow->mWindowActivationTokenFromEnv); + if (!existingToken.IsEmpty()) { + LOG(" has existing activation token."); + toplevelWindow->FocusWaylandWindow(existingToken.get()); + } else { + LOG(" missing activation token, try to transfer from focused " + "window"); + TransferFocusToWaylandWindow(toplevelWindow); + } + } +#endif + } + return; + } + + // aRaise == No means that keyboard events should be dispatched from this + // widget. + + // Ensure GTK_WIDGET(mContainer) is the focused GtkWidget within its toplevel + // window. + // + // For WindowType::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(GTK_WIDGET(mContainer))) { + // 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(GTK_WIDGET(mContainer)); + gBlockActivateEvent = false; + } + + // If this is the widget that already has focus, return. + if (gFocusWindow == this) { + LOG(" already have focus"); + return; + } + + // Set this window to be the focused child window + gFocusWindow = this; + + if (mIMContext) { + mIMContext->OnFocusWindow(this); + } + + LOG(" widget now has focus in SetFocus()"); +} + +LayoutDeviceIntRect nsWindow::GetScreenBounds() { + const LayoutDeviceIntPoint origin = [&] { + // XXX Can't we use mGdkWindow here? + // + // Use the point including window decorations. Don't do this for popups, + // because we get wrong coordinates for gtk for override-redirect windows in + // HiDPI screens, and those don't have window decorations anyways. + // + // See https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4820 + if (mContainer && mWindowType != WindowType::Popup) { + gint x, y; + gdk_window_get_root_origin(gtk_widget_get_window(GTK_WIDGET(mContainer)), + &x, &y); + return GdkPointToDevicePixels({x, y}); + } + return 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. + const LayoutDeviceIntRect rect(origin, mBounds.Size()); +#if MOZ_LOGGING + if (MOZ_LOG_TEST(IsPopup() ? gWidgetPopupLog : gWidgetLog, + LogLevel::Verbose)) { + gint scale = GdkCeiledScaleFactor(); + if (mLastLoggedScale != scale || !(mLastLoggedBoundSize == rect)) { + mLastLoggedScale = scale; + mLastLoggedBoundSize = rect; + LOG("GetScreenBounds %d,%d -> %d x %d, unscaled %d,%d -> %d x %d\n", + 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::RecomputeClientOffset(bool aNotify) { + if (mWindowType != WindowType::Dialog && + mWindowType != WindowType::TopLevel) { + return; + } + + auto oldOffset = mClientOffset; + + mClientOffset = WidgetToScreenOffset() - mBounds.TopLeft(); + + if (aNotify && mClientOffset != oldOffset) { + // 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); + } +} + +gboolean nsWindow::OnPropertyNotifyEvent(GtkWidget* aWidget, + GdkEventProperty* aEvent) { + if (aEvent->atom == gdk_atom_intern("_NET_FRAME_EXTENTS", FALSE)) { + RecomputeClientOffset(/* aNotify = */ true); + return FALSE; + } + if (!mGdkWindow) { + return FALSE; + } +#ifdef MOZ_X11 + if (GetCurrentTimeGetter()->PropertyNotifyHandler(aWidget, aEvent)) { + return TRUE; + } +#endif + return FALSE; +} + +static GdkCursor* GetCursorForImage(const nsIWidget::Cursor& aCursor, + int32_t aWidgetScaleFactor) { + if (!aCursor.IsCustom()) { + return nullptr; + } + nsIntSize size = nsIWidget::CustomCursorSize(aCursor); + + // NOTE: GTK only allows integer scale factors, so we ceil to the larger scale + // factor and then tell gtk to scale it down. We ensure to scale at least to + // the GDK scale factor, so that cursors aren't downsized in HiDPI on wayland, + // see bug 1707533. + int32_t gtkScale = std::max( + aWidgetScaleFactor, int32_t(std::ceil(std::max(aCursor.mResolution.mX, + aCursor.mResolution.mY)))); + + // 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 (size.width > 128 || size.height > 128) { + return nullptr; + } + + nsIntSize rasterSize = size * gtkScale; + RefPtr<GdkPixbuf> pixbuf = + nsImageToPixbuf::ImageToPixbuf(aCursor.mContainer, Some(rasterSize)); + if (!pixbuf) { + 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)) { + RefPtr<GdkPixbuf> alphaBuf = + dont_AddRef(gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0)); + pixbuf = std::move(alphaBuf); + if (!pixbuf) { + return nullptr; + } + } + + cairo_surface_t* surface = + gdk_cairo_surface_create_from_pixbuf(pixbuf, gtkScale, nullptr); + if (!surface) { + return nullptr; + } + + auto CleanupSurface = + MakeScopeExit([&]() { cairo_surface_destroy(surface); }); + + return gdk_cursor_new_from_surface(gdk_display_get_default(), surface, + aCursor.mHotspotX, aCursor.mHotspotY); +} + +void nsWindow::SetCursor(const Cursor& aCursor) { + // if we're not the toplevel window pass up the cursor request to + // the toplevel window to handle it. + if (!mContainer && mGdkWindow) { + if (nsWindow* window = GetContainerWindow()) { + window->SetCursor(aCursor); + } + return; + } + + if (mWidgetCursorLocked) { + return; + } + + // Only change cursor if it's actually been changed + if (!mUpdateCursor && mCursor == aCursor) { + return; + } + + mUpdateCursor = false; + mCursor = aCursor; + + // Try to set the cursor image first, and fall back to the numeric cursor. + bool fromImage = true; + GdkCursor* newCursor = GetCursorForImage(aCursor, GdkCeiledScaleFactor()); + if (!newCursor) { + fromImage = false; + newCursor = get_gtk_cursor(aCursor.mDefaultCursor); + } + + auto CleanupCursor = mozilla::MakeScopeExit([&]() { + // get_gtk_cursor returns a weak reference, which we shouldn't unref. + if (fromImage) { + 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); + + LOG("Invalidate (rect): %d %d %d %d\n", rect.x, rect.y, rect.width, + rect.height); +} + +void* nsWindow::GetNativeData(uint32_t aDataType) { + switch (aDataType) { + case NS_NATIVE_WINDOW: + case NS_NATIVE_WIDGET: { + return mGdkWindow; + } + + case NS_NATIVE_SHELLWIDGET: + return GetToplevelWidget(); + + case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID: +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + return (void*)GDK_WINDOW_XID(gdk_window_get_toplevel(mGdkWindow)); + } +#endif + NS_WARNING( + "nsWindow::GetNativeData(): 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: { + void* eglWindow = nullptr; +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + eglWindow = mGdkWindow ? (void*)GDK_WINDOW_XID(mGdkWindow) : nullptr; + } +#endif +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + if (mCompositorWidgetDelegate && + mCompositorWidgetDelegate->AsGtkCompositorWidget() && + mCompositorWidgetDelegate->AsGtkCompositorWidget()->IsHidden()) { + NS_WARNING("Getting OpenGL EGL window for hidden Gtk window!"); + return nullptr; + } + eglWindow = moz_container_wayland_get_egl_window( + mContainer, FractionalScaleFactor()); + } +#endif + LOG("Get NS_NATIVE_EGL_WINDOW mGdkWindow %p returned eglWindow %p", + mGdkWindow, eglWindow); + return eglWindow; + } + 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()); + } +} + +/* 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. + + + This code can be used instead of gdk_window_get_origin() but it cuases + such issues: + + *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; + } +*/ +LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() { + // Don't use gdk_window_get_origin() on wl_subsurface Wayland popups + // https://gitlab.gnome.org/GNOME/gtk/-/issues/5287 + if (IsWaylandPopup() && !mPopupUseMoveToRect) { + return mBounds.TopLeft(); + } + nsIntPoint origin(0, 0); + if (mGdkWindow) { + gdk_window_get_origin(mGdkWindow, &origin.x.value, &origin.y.value); + } + return GdkPointToDevicePixels({origin.x, origin.y}); +} + +void nsWindow::CaptureRollupEvents(bool aDoCapture) { + LOG("CaptureRollupEvents(%d)\n", aDoCapture); + if (mIsDestroyed) { + return; + } + + static constexpr auto kCaptureEventsMask = + GdkEventMask(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_TOUCH_MASK); + + static bool sSystemNeedsPointerGrab = [&] { + if (GdkIsWaylandDisplay()) { + return false; + } + // We only need to grab the pointer for X servers that move the focus with + // the pointer (like twm, sawfish...). Since we roll up popups on focus out, + // not grabbing the pointer triggers rollup when the mouse enters the popup + // and leaves the main window, see bug 1807482. + // + // FVWM is also affected but less severely: the pointer can enter the + // popup, but if it briefly moves out of the popup and over the main window + // then we see a focus change and roll up the popup. + // + // We don't do it for most common desktops, if only because it causes X11 + // crashes like bug 1607713. + const auto& desktop = GetDesktopEnvironmentIdentifier(); + return desktop.EqualsLiteral("twm") || desktop.EqualsLiteral("sawfish") || + StringBeginsWith(desktop, "fvwm"_ns); + }(); + + const bool grabPointer = [] { + switch (StaticPrefs::widget_gtk_grab_pointer()) { + case 0: + return false; + case 1: + return true; + default: + return sSystemNeedsPointerGrab; + } + }(); + + if (!grabPointer) { + return; + } + + mNeedsToRetryCapturingMouse = false; + if (aDoCapture) { + if (mIsDragPopup || DragInProgress()) { + // Don't add a grab if a drag is in progress, or if the widget is a drag + // feedback popup. (panels with type="drag"). + return; + } + + if (!mHasMappedToplevel) { + // On X, capturing an unmapped window is pointless (returns + // GDK_GRAB_NOT_VIEWABLE). Avoid the X server round-trip and just retry + // when we're mapped. + mNeedsToRetryCapturingMouse = true; + return; + } + + // gdk_pointer_grab is deprecated in favor of gdk_device_grab, but that + // causes a strange bug on X11, most obviously with nested popup menus: + // we somehow take the pointer position relative to the top left of the + // outer menu and use it as if it were relative to the submenu. This + // doesn't happen with gdk_pointer_grab even though the code is very + // similar. See the video attached to bug 1750721 for a demonstration, + // and see also bug 1820542 for when the same thing happened with + // another attempt to use gdk_device_grab. + // + // (gdk_device_grab is deprecated in favor of gdk_seat_grab as of 3.20, + // but at the time of this writing we still support older versions of + // GTK 3.) + GdkGrabStatus status = + gdk_pointer_grab(GetToplevelGdkWindow(), + /* owner_events = */ true, kCaptureEventsMask, + /* confine_to = */ nullptr, + /* cursor = */ nullptr, GetLastUserInputTime()); + Unused << NS_WARN_IF(status != GDK_GRAB_SUCCESS); + LOG(" > pointer grab with status %d", int(status)); + gtk_grab_add(GTK_WIDGET(mContainer)); + } else { + // 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)); + gdk_pointer_ungrab(GetLastUserInputTime()); + } +} + +nsresult nsWindow::GetAttention(int32_t aCycleCount) { + LOG("nsWindow::GetAttention"); + + 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 (GdkIsX11Display()) { + 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; +} + +#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((float)r.x, (float)r.y, + (float)r.width, (float)r.height)); + } + + cairo_rectangle_list_destroy(rects); + return true; +} + +#ifdef MOZ_WAYLAND +void nsWindow::CreateCompositorVsyncDispatcher() { + LOG_VSYNC("nsWindow::CreateCompositorVsyncDispatcher()"); + if (!mWaylandVsyncSource) { + LOG_VSYNC( + " mWaylandVsyncSource is missing, create " + "nsBaseWidget::CompositorVsyncDispatcher()"); + nsBaseWidget::CreateCompositorVsyncDispatcher(); + return; + } + if (!mCompositorVsyncDispatcherLock) { + mCompositorVsyncDispatcherLock = + MakeUnique<Mutex>("mCompositorVsyncDispatcherLock"); + } + MutexAutoLock lock(*mCompositorVsyncDispatcherLock); + if (!mCompositorVsyncDispatcher) { + LOG_VSYNC(" create CompositorVsyncDispatcher()"); + mCompositorVsyncDispatcher = + new CompositorVsyncDispatcher(mWaylandVsyncDispatcher); + } +} +#endif + +gboolean nsWindow::OnExposeEvent(cairo_t* cr) { + // This might destroy us. + NotifyOcclusionState(OcclusionState::VISIBLE); + if (mIsDestroyed) { + return FALSE; + } + + // Send any pending resize events so that layout can update. + // May run event loop and destroy us. + 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 (GdkIsWaylandDisplay() && !moz_container_wayland_can_draw(mContainer)) { + return FALSE; + } +#endif + + nsIWidgetListener* listener = GetListener(); + if (!listener) return FALSE; + + LOG("nsWindow::OnExposeEvent GdkWindow [%p] XWindow [0x%lx]", mGdkWindow, + GetX11Window()); + + LayoutDeviceIntRegion exposeRegion; + if (!ExtractExposeRegion(exposeRegion, cr)) { + LOG(" no rects, quit"); + return FALSE; + } + + gint scale = GdkCeiledScaleFactor(); + LayoutDeviceIntRegion region = exposeRegion; + region.ScaleRoundOut(scale, scale); + + WindowRenderer* renderer = GetWindowRenderer(); + WebRenderLayerManager* layerManager = renderer->AsWebRender(); + KnowsCompositor* knowsCompositor = renderer->AsKnowsCompositor(); + + if (knowsCompositor && layerManager && mCompositorSession) { + if (!mConfiguredClearColor && !IsPopup()) { + layerManager->WrBridge()->SendSetDefaultClearColor(LookAndFeel::Color( + LookAndFeel::ColorID::Window, LookAndFeel::ColorSchemeForChrome(), + LookAndFeel::UseStandins::No)); + mConfiguredClearColor = true; + } + + // We need to paint to the screen even if nothing changed, since if we + // don't have a compositing window manager, our pixels could be stale. + layerManager->SetNeedsComposite(true); + layerManager->SendInvalidRegion(region.ToUnknownRegion()); + } + + RefPtr<nsWindow> strongThis(this); + + // 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 (knowsCompositor && layerManager && layerManager->NeedsComposite()) { + layerManager->ScheduleComposite(wr::RenderReasons::WIDGET); + layerManager->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 (TransparencyMode::Transparent == GetTransparencyMode()) { + auto* window = static_cast<nsWindow*>(GetTopLevelWidget()); + if (mTransparencyBitmapForTitlebar) { + if (mSizeMode == 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 (region.IsEmpty()) { + return TRUE; + } + + // If this widget uses OMTC... + if (renderer->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; + } + Maybe<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.emplace(destDT, /* aPreserveTransform */ true); + } else { + gfxUtils::ClipToRegion(dt, region.ToUnknownRegion()); + ctx.emplace(dt, /* aPreserveTransform */ true); + } + +# 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 (renderer->GetBackendType() == LayersBackend::LAYERS_NONE) { + if (GetTransparencyMode() == TransparencyMode::Transparent && + 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.isNothing() ? nullptr : &ctx.ref(), 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.reset(); + 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 %d,%d -> %d x %d scale %d\n", aEvent->x, aEvent->y, + aEvent->width, aEvent->height, + mGdkWindow ? gdk_window_get_scale_factor(mGdkWindow) : -1); + + if (mPendingConfigures > 0) { + mPendingConfigures--; + } + + // Don't fire configure event for scale changes, we handle that + // OnScaleChanged event. Skip that for toplevel windows only. + if (mGdkWindow && (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog)) { + if (mWindowScaleFactor != gdk_window_get_scale_factor(mGdkWindow)) { + LOG(" scale factor changed to %d,return early", + gdk_window_get_scale_factor(mGdkWindow)); + return FALSE; + } + } + + LayoutDeviceIntRect screenBounds = GetScreenBounds(); + + if (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog) { + // This check avoids unwanted rollup on spurious configure events from + // Cygwin/X (bug 672103). + if (mBounds.x != screenBounds.x || mBounds.y != screenBounds.y) { + RollupAllMenus(); + } + } + + NS_ASSERTION(GTK_IS_WINDOW(aWidget), + "Configure event on widget that is not a GtkWindow"); + if (mGdkWindow && + 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. + GetWindowRenderer()->FlushRendering(wr::RenderReasons::WIDGET); + return FALSE; + } + + mBounds.MoveTo(screenBounds.TopLeft()); + RecomputeClientOffset(/* aNotify = */ false); + + // XXX mozilla will invalidate the entire window after this move + // complete. wtf? + NotifyWindowMoved(mBounds.x, mBounds.y); + + return FALSE; +} + +void nsWindow::OnMap() { + LOG("nsWindow::OnMap"); + // Gtk mapped widget to screen. Configure underlying GdkWindow properly + // as our rendering target. + // This call means we have X11 (or Wayland) window we can render to by GL + // so we need to notify compositor about it. + mIsMapped = true; + ConfigureGdkWindow(); +} + +void nsWindow::OnUnmap() { + LOG("nsWindow::OnUnmap"); + + // Mark window as unmapped. It can be still used for rendering on X11 + // untill OnUnrealize is called. + mIsMapped = false; + + if (mSourceDragContext) { + static auto sGtkDragCancel = + (void (*)(GdkDragContext*))dlsym(RTLD_DEFAULT, "gtk_drag_cancel"); + if (sGtkDragCancel) { + sGtkDragCancel(mSourceDragContext); + mSourceDragContext = nullptr; + } + } + +#ifdef MOZ_WAYLAND + // wl_surface owned by mContainer is going to be deleted. + // Make sure we don't paint to it on Wayland. + if (GdkIsWaylandDisplay()) { + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->DisableRendering(); + } + if (moz_container_wayland_has_egl_window(mContainer)) { + // Widget is backed by OpenGL EGLSurface created over wl_surface + // owned by mContainer. + // RenderCompositorEGL::Resume() deletes recent EGLSurface based on + // wl_surface owned by mContainer and creates a new fallback EGLSurface. + // Then we can delete wl_surface in moz_container_wayland_unmap(). + // We don't want to pause compositor as it may lead to whole + // browser freeze (Bug 1777664). + if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) { + remoteRenderer->SendResume(); + } + } + if (GdkIsWaylandDisplay()) { + moz_container_wayland_unmap(GTK_WIDGET(mContainer)); + } + } +#endif + moz_container_unmap(GTK_WIDGET(mContainer)); +} + +void nsWindow::OnUnrealize() { + // 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. + // This call means we *don't* have X11 (or Wayland) window we can render to. + LOG("nsWindow::OnUnrealize GdkWindow %p", mGdkWindow); + ReleaseGdkWindow(); +} + +void nsWindow::OnSizeAllocate(GtkAllocation* aAllocation) { + LOG("nsWindow::OnSizeAllocate %d,%d -> %d x %d\n", 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. + RecomputeClientOffset(/* aNotify = */ true); + + mHasReceivedSizeAllocate = true; + + LayoutDeviceIntSize size = GdkRectToDevicePixels(*aAllocation).Size(); + + // Sometimes the window manager gives us garbage sizes (way past the maximum + // texture size) causing crashes if we don't enforce size constraints again + // here. + ConstrainSize(&size.width, &size.height); + + if (mBounds.Size() == size) { + LOG(" Already the same size"); + // mBounds was set at Create() or Resize(). + if (mNeedsDispatchSize != LayoutDeviceIntSize(-1, -1)) { + LOG(" No longer needs to dispatch %dx%d", mNeedsDispatchSize.width, + mNeedsDispatchSize.height); + mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1); + } + 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 (mGdkWindow) { + 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); + } + } + + // If we update mBounds here, then inner/outerHeight are out of sync until + // we call WindowResized. + mNeedsDispatchSize = size; + + // 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. + NS_DispatchToCurrentThread(NewRunnableMethod( + "nsWindow::MaybeDispatchResized", this, &nsWindow::MaybeDispatchResized)); +} + +void nsWindow::OnDeleteEvent() { + if (mWidgetListener) mWidgetListener->RequestWindowClose(this); +} + +void nsWindow::OnEnterNotifyEvent(GdkEventCrossing* aEvent) { + LOG("enter notify (win=%p, sub=%p): %f, %f mode %d, detail %d\n", + aEvent->window, aEvent->subwindow, aEvent->x, aEvent->y, aEvent->mode, + aEvent->detail); + // 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) { + return; + } + + // Check before checking for ungrab as the button state may have + // changed while a non-Gecko ancestor window had a pointer grab. + DispatchMissedButtonReleases(aEvent); + + WidgetMouseEvent event(true, eMouseEnterIntoWidget, this, + WidgetMouseEvent::eReal); + + event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); + + LOG("OnEnterNotify"); + + DispatchInputEvent(&event); +} + +// Some window managers send a bogus top-level leave-notify event on every +// click. That confuses our event handling code in ways that can break websites, +// see bug 1805939 for details. +// +// Make sure to only check this on bogus environments, since for environments +// with CSD, gdk_device_get_window_at_position could return the window even when +// the pointer is in the decoration area. +static bool IsBogusLeaveNotifyEvent(GdkWindow* aWindow, + GdkEventCrossing* aEvent) { + static bool sBogusWm = [] { + if (GdkIsWaylandDisplay()) { + return false; + } + const auto& desktopEnv = GetDesktopEnvironmentIdentifier(); + return desktopEnv.EqualsLiteral("fluxbox") || // Bug 1805939 comment 0. + desktopEnv.EqualsLiteral("blackbox") || // Bug 1805939 comment 32. + desktopEnv.EqualsLiteral("lg3d") || // Bug 1820405. + desktopEnv.EqualsLiteral("pekwm") || // Bug 1822911. + StringBeginsWith(desktopEnv, "fvwm"_ns); + }(); + + const bool shouldCheck = [] { + switch (StaticPrefs::widget_gtk_ignore_bogus_leave_notify()) { + case 0: + return false; + case 1: + return true; + default: + return sBogusWm; + } + }(); + + if (!shouldCheck) { + return false; + } + GdkDevice* pointer = GdkGetPointer(); + GdkWindow* winAtPt = + gdk_device_get_window_at_position(pointer, nullptr, nullptr); + if (!winAtPt) { + return false; + } + // We're still in the same top level window, ignore this leave notify event. + GdkWindow* topLevelAtPt = gdk_window_get_toplevel(winAtPt); + GdkWindow* topLevelWidget = gdk_window_get_toplevel(aWindow); + return topLevelAtPt == topLevelWidget; +} + +void nsWindow::OnLeaveNotifyEvent(GdkEventCrossing* aEvent) { + LOG("leave notify (win=%p, sub=%p): %f, %f mode %d, detail %d\n", + aEvent->window, aEvent->subwindow, aEvent->x, aEvent->y, aEvent->mode, + aEvent->detail); + + // 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) { + return; + } + + // The filter out for subwindows should make sure that this is targeted to + // this nsWindow. + const bool leavingTopLevel = + mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog; + + if (leavingTopLevel && IsBogusLeaveNotifyEvent(mGdkWindow, aEvent)) { + return; + } + + WidgetMouseEvent event(true, eMouseExitFromWidget, this, + WidgetMouseEvent::eReal); + + event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y); + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); + event.mExitFrom = Some(leavingTopLevel ? WidgetMouseEvent::ePlatformTopLevel + : WidgetMouseEvent::ePlatformChild); + + LOG("OnLeaveNotify"); + + DispatchInputEvent(&event); +} + +Maybe<GdkWindowEdge> nsWindow::CheckResizerEdge( + const LayoutDeviceIntPoint& aPoint) { + const bool canResize = [&] { + // Don't allow resizing maximized/fullscreen windows. + if (mSizeMode != nsSizeMode_Normal) { + return false; + } + if (mIsPIPWindow) { + // Note that since we do show resizers on left/right sides on PIP windows, + // we still want the resizers there, even when tiled. + return true; + } + if (!mDrawInTitlebar) { + return false; + } + // On KDE, allow for 1 extra pixel at the top of regular windows when + // drawing to the titlebar. This matches the native titlebar behavior on + // that environment. See bug 1813554. + // + // Don't do that on GNOME (see bug 1822764). If we wanted to do this on + // GNOME we'd need an extra check for mIsTiled, since the window is "stuck" + // to the top and bottom. + // + // Other DEs are untested. + return mDrawInTitlebar && IsKdeDesktopEnvironment(); + }(); + + if (!canResize) { + return {}; + } + + // If we're not in a PiP window, allow 1px resizer edge from the top edge, + // and nothing else. + // This is to allow resizes of tiled windows on KDE, see bug 1813554. + const int resizerSize = (mIsPIPWindow ? 15 : 1) * GdkCeiledScaleFactor(); + + const int topDist = aPoint.y; + const int leftDist = aPoint.x; + const int rightDist = mBounds.width - aPoint.x; + const int bottomDist = mBounds.height - aPoint.y; + + if (topDist <= resizerSize) { + if (rightDist <= resizerSize) { + return Some(GDK_WINDOW_EDGE_NORTH_EAST); + } + if (leftDist <= resizerSize) { + return Some(GDK_WINDOW_EDGE_NORTH_WEST); + } + return Some(GDK_WINDOW_EDGE_NORTH); + } + + if (!mIsPIPWindow) { + return {}; + } + + if (bottomDist <= resizerSize) { + if (leftDist <= resizerSize) { + return Some(GDK_WINDOW_EDGE_SOUTH_WEST); + } + if (rightDist <= resizerSize) { + return Some(GDK_WINDOW_EDGE_SOUTH_EAST); + } + return Some(GDK_WINDOW_EDGE_SOUTH); + } + + if (leftDist <= resizerSize) { + return Some(GDK_WINDOW_EDGE_WEST); + } + if (rightDist <= resizerSize) { + return Some(GDK_WINDOW_EDGE_EAST); + } + return {}; +} + +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 (!mGdkWindow) { + return; + } + + 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; +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + // 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; + } + } +#endif + + if (canDrag) { + gdk_window_begin_move_drag(gdk_window, 1, aEvent->x_root, aEvent->y_root, + aEvent->time); + return; + } + } + + mWidgetCursorLocked = false; + const auto refPoint = GetRefPoint(this, aEvent); + if (auto edge = CheckResizerEdge(refPoint)) { + nsCursor cursor = eCursor_none; + switch (*edge) { + case GDK_WINDOW_EDGE_SOUTH: + case GDK_WINDOW_EDGE_NORTH: + cursor = eCursor_ns_resize; + break; + case GDK_WINDOW_EDGE_WEST: + case GDK_WINDOW_EDGE_EAST: + cursor = eCursor_ew_resize; + break; + case GDK_WINDOW_EDGE_NORTH_WEST: + case GDK_WINDOW_EDGE_SOUTH_EAST: + cursor = eCursor_nwse_resize; + break; + case GDK_WINDOW_EDGE_NORTH_EAST: + case GDK_WINDOW_EDGE_SOUTH_WEST: + cursor = eCursor_nesw_resize; + break; + } + SetCursor(Cursor{cursor}); + // If we set resize cursor on widget level keep it locked and prevent layout + // to switch it back to default (by synthetic mouse events for instance) + // until resize is finished. + mWidgetCursorLocked = true; + return; + } + + 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; + event.mRefPoint = refPoint; + event.AssignEventTime(GetWidgetEventTime(aEvent->time)); + + KeymapWrapper::InitInputEvent(event, aEvent->state); + + 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", guint(buttonType + 1)); + + // 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, + const LayoutDeviceIntPoint& aRefPoint) { + aEvent.mRefPoint = aRefPoint; + + 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, + const LayoutDeviceIntPoint& aRefPoint) { + if (domButton == MouseButton::eSecondary && MOZ_LIKELY(!mIsDestroyed)) { + WidgetMouseEvent contextMenuEvent(true, eContextMenu, this, + WidgetMouseEvent::eReal); + InitButtonEvent(contextMenuEvent, aEvent, aRefPoint); + contextMenuEvent.mPressure = mLastMotionPressure; + DispatchInputEvent(&contextMenuEvent); + } +} + +void nsWindow::TryToShowNativeWindowMenu(GdkEventButton* aEvent) { + if (!gdk_window_show_window_menu(GetToplevelGdkWindow(), (GdkEvent*)aEvent)) { + NS_WARNING("Native context menu wasn't shown"); + } +} + +void nsWindow::OnButtonPressEvent(GdkEventButton* aEvent) { + LOG("Button %u press\n", aEvent->button); + + SetLastMousePressEvent((GdkEvent*)aEvent); + + // 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. + GUniquePtr<GdkEvent> peekedEvent(gdk_event_peek()); + if (peekedEvent) { + GdkEventType type = peekedEvent->any.type; + if (type == GDK_2BUTTON_PRESS || type == GDK_3BUTTON_PRESS) { + return; + } + } + + nsWindow* containerWindow = GetContainerWindow(); + if (!gFocusWindow && containerWindow) { + containerWindow->DispatchActivateEvent(); + } + + const auto refPoint = GetRefPoint(this, aEvent); + + // check to see if we should rollup + if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) { + if (aEvent->button == 3 && mDraggableRegion.Contains(refPoint)) { + GUniquePtr<GdkEvent> eventCopy; + if (aEvent->type != GDK_BUTTON_PRESS) { + // If the user double-clicks too fast we'll get a 2BUTTON_PRESS event + // instead, and that isn't handled by open_window_menu, so coerce it + // into a regular press. + eventCopy.reset(gdk_event_copy((GdkEvent*)aEvent)); + eventCopy->type = GDK_BUTTON_PRESS; + } + TryToShowNativeWindowMenu(eventCopy ? &eventCopy->button : aEvent); + } + return; + } + + // Check to see if the event is within our window's resize region + if (auto edge = CheckResizerEdge(refPoint)) { + gdk_window_begin_resize_drag(gtk_widget_get_window(mShell), *edge, + aEvent->button, aEvent->x_root, aEvent->y_root, + aEvent->time); + 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, refPoint); + event.mPressure = mLastMotionPressure; + + nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event); + + if ((mIsWaylandPanelWindow || mDraggableRegion.Contains(refPoint)) && + domButton == MouseButton::ePrimary && + eventStatus.mContentStatus != nsEventStatus_eConsumeNoDefault) { + mWindowShouldStartDragging = true; + } + + // right menu click on linux should also pop up a context menu + if (!StaticPrefs::ui_context_menus_after_mouseup() && + eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) { + DispatchContextMenuEventFromMouseEvent(domButton, aEvent, refPoint); + } +} + +void nsWindow::OnButtonReleaseEvent(GdkEventButton* aEvent) { + LOG("Button %u release\n", aEvent->button); + + SetLastMousePressEvent(nullptr); + + if (!mGdkWindow) { + return; + } + + 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); + + const auto refPoint = GetRefPoint(this, aEvent); + + WidgetMouseEvent event(true, eMouseUp, this, WidgetMouseEvent::eReal); + event.mButton = domButton; + InitButtonEvent(event, aEvent, refPoint); + gdouble pressure = 0; + gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure); + event.mPressure = pressure ? (float)pressure : (float)mLastMotionPressure; + + // The mRefPoint is manipulated in DispatchInputEvent, we're saving it + // to use it for the doubleclick position check. + const LayoutDeviceIntPoint pos = event.mRefPoint; + + nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event); + + const bool defaultPrevented = + eventStatus.mContentStatus == 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)) { + if (mSizeMode == nsSizeMode_Maximized) { + SetSizeMode(nsSizeMode_Normal); + } else if (mSizeMode == nsSizeMode_Normal) { + SetSizeMode(nsSizeMode_Maximized); + } + } + mLastMotionPressure = pressure; + + // right menu click on linux should also pop up a context menu + if (StaticPrefs::ui_context_menus_after_mouseup() && + eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) { + DispatchContextMenuEventFromMouseEvent(domButton, aEvent, refPoint); + } + + // Open window manager menu on PIP window to allow user + // to place it on top / all workspaces. + if (mAlwaysOnTop && aEvent->button == 3) { + TryToShowNativeWindowMenu(aEvent); + } +} + +void nsWindow::OnContainerFocusInEvent(GdkEventFocus* aEvent) { + LOG("OnContainerFocusInEvent"); + + // 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) { + LOG("activated notification is blocked"); + 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; + } + + LOG("Events sent from focus in event"); +} + +void nsWindow::OnContainerFocusOutEvent(GdkEventFocus* aEvent) { + LOG("OnContainerFocusOutEvent"); + + if (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog) { + // Rollup menus 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. + const bool shouldRollupMenus = [&] { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + nsCOMPtr<nsIDragSession> dragSession; + dragService->GetCurrentSession(getter_AddRefs(dragSession)); + if (!dragSession) { + return true; + } + // We also roll up when a drag is from a different application + nsCOMPtr<nsINode> sourceNode; + dragSession->GetSourceNode(getter_AddRefs(sourceNode)); + return !sourceNode; + }(); + + if (shouldRollupMenus) { + RollupAllMenus(); + } + + if (RefPtr pm = nsXULPopupManager::GetInstance()) { + pm->RollupTooltips(); + } + } + + 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(); + } + + LOG("Done with container focus out"); +} + +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(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 (GdkIsWaylandDisplay()) { + // 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 { +#ifdef MOZ_X11 + CurrentX11TimeGetter* getCurrentTime = GetCurrentTimeGetter(); + MOZ_ASSERT(getCurrentTime, + "Null current time getter despite having a window"); + eventTimeStamp = + TimeConverter().GetTimeStampFromSystemTime(aEventTime, *getCurrentTime); +#endif + } + return eventTimeStamp; +} + +#ifdef MOZ_X11 +mozilla::CurrentX11TimeGetter* nsWindow::GetCurrentTimeGetter() { + MOZ_ASSERT(mGdkWindow, "Expected mGdkWindow to be set"); + if (MOZ_UNLIKELY(!mCurrentTimeGetter)) { + mCurrentTimeGetter = MakeUnique<CurrentX11TimeGetter>(mGdkWindow); + } + return mCurrentTimeGetter.get(); +} +#endif + +gboolean nsWindow::OnKeyPressEvent(GdkEventKey* aEvent) { + LOG("OnKeyPressEvent"); + + KeymapWrapper::HandleKeyPressEvent(this, aEvent); + return TRUE; +} + +gboolean nsWindow::OnKeyReleaseEvent(GdkEventKey* aEvent) { + LOG("OnKeyReleaseEvent"); + if (NS_WARN_IF(!KeymapWrapper::HandleKeyReleaseEvent(this, 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 || + mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase.isSome()) { + if (StaticPrefs::apz_gtk_pangesture_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; + } else if (mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase + .isSome()) { + switch (*mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase) { + case PHASE_BEGIN: + // we should never hit this because we'll hit the above case + // before this. + MOZ_ASSERT_UNREACHABLE(); + eventType = PanGestureInput::PANGESTURE_START; + mPanInProgress = true; + break; + case PHASE_UPDATE: + // nothing to do here, eventtype should already be set + MOZ_ASSERT(mPanInProgress); + MOZ_ASSERT(eventType == PanGestureInput::PANGESTURE_PAN); + eventType = PanGestureInput::PANGESTURE_PAN; + break; + case PHASE_END: + MOZ_ASSERT(mPanInProgress); + eventType = PanGestureInput::PANGESTURE_END; + mPanInProgress = false; + break; + default: + MOZ_ASSERT_UNREACHABLE(); + break; + } + } + + mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase.reset(); + + const bool isPageMode = + StaticPrefs::apz_gtk_pangesture_delta_mode() != 2; + const double multiplier = + isPageMode + ? StaticPrefs::apz_gtk_pangesture_page_delta_mode_multiplier() + : StaticPrefs:: + apz_gtk_pangesture_pixel_delta_mode_multiplier() * + FractionalScaleFactor(); + + ScreenPoint deltas(float(aEvent->delta_x * multiplier), + float(aEvent->delta_y * multiplier)); + + LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent); + PanGestureInput panEvent( + eventType, GetEventTimeStamp(aEvent->time), + ScreenPoint(touchPoint.x, touchPoint.y), deltas, + KeymapWrapper::ComputeKeyModifiers(aEvent->state)); + panEvent.mDeltaType = isPageMode ? PanGestureInput::PANDELTA_PAGE + : PanGestureInput::PANDELTA_PIXEL; + panEvent.mSimulateMomentum = + StaticPrefs::apz_gtk_kinetic_scroll_enabled(); + + DispatchPanGesture(panEvent); + + if (mCurrentSynthesizedTouchpadPan.mSavedObserver != 0) { + mozilla::widget::AutoObserverNotifier::NotifySavedObserver( + mCurrentSynthesizedTouchpadPan.mSavedObserver, + "touchpadpanevent"); + mCurrentSynthesizedTouchpadPan.mSavedObserver = 0; + } + + return; + } + + // Older GTK doesn't support stop events, so we can't support fling + wheelEvent.mScrollType = WidgetWheelEvent::SCROLL_ASYNCHRONOUSLY; + } + + // 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.mWheelTicksX = aEvent->delta_x; + wheelEvent.mWheelTicksY = aEvent->delta_y; + wheelEvent.mIsNoLineOrPageDelta = true; + + break; + } + case GDK_SCROLL_UP: + wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = -3; + wheelEvent.mWheelTicksY = -1; + break; + case GDK_SCROLL_DOWN: + wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = 3; + wheelEvent.mWheelTicksY = 1; + break; + case GDK_SCROLL_LEFT: + wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = -1; + wheelEvent.mWheelTicksX = -1; + break; + case GDK_SCROLL_RIGHT: + wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = 1; + wheelEvent.mWheelTicksX = 1; + break; + } + + wheelEvent.mRefPoint = GetRefPoint(this, aEvent); + + KeymapWrapper::InitInputEvent(wheelEvent, aEvent->state); + + wheelEvent.AssignEventTime(GetWidgetEventTime(aEvent->time)); + + DispatchInputEvent(&wheelEvent); +} + +void nsWindow::DispatchPanGesture(PanGestureInput& aPanInput) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mSwipeTracker) { + // Give the swipe tracker a first pass at the event. If a new pan gesture + // has been started since the beginning of the swipe, the swipe tracker + // will know to ignore the event. + nsEventStatus status = mSwipeTracker->ProcessEvent(aPanInput); + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + + APZEventResult result; + if (mAPZC) { + MOZ_ASSERT(APZThreadUtils::IsControllerThread()); + + result = mAPZC->InputBridge()->ReceiveInputEvent(aPanInput); + if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) { + return; + } + } + + WidgetWheelEvent event = aPanInput.ToWidgetEvent(this); + if (!mAPZC) { + if (MayStartSwipeForNonAPZ(aPanInput)) { + return; + } + } else { + event = MayStartSwipeForAPZ(aPanInput, result); + } + + ProcessUntransformedAPZEvent(&event, result); +} + +void nsWindow::OnWindowStateEvent(GtkWidget* aWidget, + GdkEventWindowState* aEvent) { + LOG("nsWindow::OnWindowStateEvent for %p changed 0x%x new_window_state " + "0x%x\n", + 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)); + 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 it's still live and kicking + // (Bug 1791779). + 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 + // mSizeMode and obtaining a focus. + bool waylandWasIconified = + (GdkIsWaylandDisplay() && + aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED && + aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED && + mSizeMode == 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; + } + + auto oldSizeMode = mSizeMode; + if (aEvent->new_window_state & GDK_WINDOW_STATE_ICONIFIED) { + LOG("\tIconified\n"); + mSizeMode = nsSizeMode_Minimized; +#ifdef ACCESSIBILITY + DispatchMinimizeEventAccessible(); +#endif // ACCESSIBILITY + } else if (aEvent->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) { + LOG("\tFullscreen\n"); + mSizeMode = nsSizeMode_Fullscreen; + } else if (aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) { + LOG("\tMaximized\n"); + mSizeMode = nsSizeMode_Maximized; +#ifdef ACCESSIBILITY + DispatchMaximizeEventAccessible(); +#endif // ACCESSIBILITY + } else { + LOG("\tNormal\n"); + mSizeMode = nsSizeMode_Normal; +#ifdef ACCESSIBILITY + DispatchRestoreEventAccessible(); +#endif // ACCESSIBILITY + } + + mIsTiled = aEvent->new_window_state & GDK_WINDOW_STATE_TILED; + LOG("\tTiled: %d\n", int(mIsTiled)); + + if (mWidgetListener && mSizeMode != oldSizeMode) { + mWidgetListener->SizeModeChanged(mSizeMode); + } + + if (mDrawInTitlebar && mTransparencyBitmapForTitlebar) { + if (mSizeMode == nsSizeMode_Normal && !mIsTiled) { + UpdateTitlebarTransparencyBitmap(); + } else { + ClearTransparencyBitmap(); + } + } +} + +void nsWindow::OnDPIChanged() { + // Update menu's font size etc. + // This affects style / layout because it affects system font sizes. + if (mWidgetListener) { + if (PresShell* presShell = mWidgetListener->GetPresShell()) { + presShell->BackingScaleFactorChanged(); + } + mWidgetListener->UIResolutionChanged(); + } + NotifyThemeChanged(ThemeChangeKind::StyleAndLayout); +} + +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() { + // Force scale factor recalculation + if (!mGdkWindow) { + mWindowScaleFactorChanged = true; + return; + } + LOG("OnScaleChanged -> %d\n", gdk_window_get_scale_factor(mGdkWindow)); + + // Gtk supply us sometimes with doubled events so stay calm in such case. + if (gdk_window_get_scale_factor(mGdkWindow) == mWindowScaleFactor) { + return; + } + + // We pause compositor to avoid rendering of obsoleted remote content which + // produces flickering. + // Re-enable compositor again when remote content is updated or + // timeout happens. + PauseCompositorFlickering(); + + // Force scale factor recalculation + mWindowScaleFactorChanged = true; + + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(mContainer), &allocation); + LayoutDeviceIntSize size = GdkRectToDevicePixels(allocation).Size(); + mBounds.SizeTo(size); + // Check mBounds size + if (mCompositorSession && + !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) { + gfxCriticalNoteOnce << "Invalid mBounds in OnScaleChanged " << mBounds; + } + + if (mWidgetListener) { + if (PresShell* presShell = mWidgetListener->GetPresShell()) { + presShell->BackingScaleFactorChanged(); + } + } + // This affects style / layout because it affects system font sizes. + // Update menu's font size etc. + NotifyThemeChanged(ThemeChangeKind::StyleAndLayout); + + DispatchResized(); + + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize()); + } + + if (mCursor.IsCustom()) { + mUpdateCursor = true; + SetCursor(Cursor{mCursor}); + } +} + +void nsWindow::DispatchDragEvent(EventMessage aMsg, + const LayoutDeviceIntPoint& aRefPoint, + guint aTime) { + LOGDRAG("nsWindow::DispatchDragEvent"); + 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"); + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + nsDragService::AutoEventLoop loop(dragService); + dragService->TargetDataReceived(aWidget, aDragContext, aX, aY, aSelectionData, + aInfo, aTime); +} + +nsWindow* nsWindow::GetTransientForWindowIfPopup() { + if (mWindowType != WindowType::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()) { + return TRUE; + } + // Do not respond to pinch gestures involving more than two fingers + // unless specifically preffed on. These are sometimes hooked up to other + // actions at the desktop environment level and having the browser also + // pinch can be undesirable. + if (aEvent->n_fingers > 2 && + !StaticPrefs::apz_gtk_touchpad_pinch_three_fingers_enabled()) { + return FALSE; + } + auto pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE; + ScreenCoord currentSpan; + ScreenCoord previousSpan; + + switch (aEvent->phase) { + case GDK_TOUCHPAD_GESTURE_PHASE_BEGIN: + pinchGestureType = PinchGestureInput::PINCHGESTURE_START; + currentSpan = aEvent->scale; + mCurrentTouchpadFocus = ViewAs<ScreenPixel>( + GetRefPoint(this, aEvent), + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + + // 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; + break; + + case GDK_TOUCHPAD_GESTURE_PHASE_UPDATE: + pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE; + mCurrentTouchpadFocus += ScreenPoint(aEvent->dx, aEvent->dy); + if (aEvent->scale == mLastPinchEventSpan) { + return FALSE; + } + currentSpan = aEvent->scale; + previousSpan = mLastPinchEventSpan; + break; + + case GDK_TOUCHPAD_GESTURE_PHASE_END: + pinchGestureType = PinchGestureInput::PINCHGESTURE_END; + currentSpan = aEvent->scale; + previousSpan = mLastPinchEventSpan; + break; + + default: + return FALSE; + } + + PinchGestureInput event( + pinchGestureType, PinchGestureInput::TRACKPAD, + GetEventTimeStamp(aEvent->time), ExternalPoint(0, 0), + mCurrentTouchpadFocus, + 100.0 * ((aEvent->phase == GDK_TOUCHPAD_GESTURE_PHASE_END) + ? ScreenCoord(1.f) + : currentSpan), + 100.0 * ((aEvent->phase == GDK_TOUCHPAD_GESTURE_PHASE_END) + ? ScreenCoord(1.f) + : previousSpan), + KeymapWrapper::ComputeKeyModifiers(aEvent->state)); + + if (!event.SetLineOrPageDeltaY(this)) { + return FALSE; + } + + mLastPinchEventSpan = aEvent->scale; + DispatchPinchGestureInput(event); + return TRUE; +} + +gboolean nsWindow::OnTouchEvent(GdkEventTouch* aEvent) { + LOG("OnTouchEvent: x=%f y=%f type=%d\n", aEvent->x, aEvent->y, aEvent->type); + 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: + // check to see if we should rollup + if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) { + return FALSE; + } + msg = eTouchStart; + break; + case GDK_TOUCH_UPDATE: + msg = eTouchMove; + // Start dragging when motion events happens in the dragging area + if (mWindowShouldStartDragging) { + mWindowShouldStartDragging = false; + GdkWindow* gdk_window = gdk_window_get_toplevel(mGdkWindow); + MOZ_ASSERT(gdk_window, + "gdk_window_get_toplevel should not return null"); + + LOG(" start window dragging window\n"); + gdk_window_begin_move_drag(gdk_window, 1, aEvent->x_root, + aEvent->y_root, aEvent->time); + + // Cancel the event sequence. gdk will steal all subsequent events + // (including TOUCH_END). + msg = eTouchCancel; + } + break; + case GDK_TOUCH_END: + msg = eTouchEnd; + if (mWindowShouldStartDragging) { + LOG(" end of window dragging window\n"); + mWindowShouldStartDragging = false; + } + break; + case GDK_TOUCH_CANCEL: + msg = eTouchCancel; + break; + default: + return FALSE; + } + + const 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); + + if (msg == eTouchStart || msg == eTouchMove) { + mTouches.InsertOrUpdate(aEvent->sequence, std::move(touch)); + // add all touch points to event object + for (const auto& data : mTouches.Values()) { + event.mTouches.AppendElement(new dom::Touch(*data)); + } + } else if (msg == eTouchEnd || msg == eTouchCancel) { + *event.mTouches.AppendElement() = std::move(touch); + } + + nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event); + + // There's a chance that we are in drag area and the event is not consumed + // by something on it. + if (msg == eTouchStart && mDraggableRegion.Contains(touchPoint) && + eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) { + mWindowShouldStartDragging = true; + } + 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 = + GetSystemGtkWindowDecoration() != GTK_DECORATION_NONE; + } + } + transparencyConfigured = true; + } + + return sTransparentMainWindow; +} + +#ifdef MOZ_X11 +// Configure GL visual on X11. +bool nsWindow::ConfigureX11GLVisual() { + auto* screen = gtk_widget_get_screen(mShell); + int visualId = 0; + bool haveVisual = false; + + if (gfxVars::UseEGL()) { + haveVisual = GLContextEGL::FindVisual(&visualId); + } + + // We are on GLX or use it as a fallback on Mesa, see + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/149 + if (!haveVisual) { + auto* display = GDK_DISPLAY_XDISPLAY(gtk_widget_get_display(mShell)); + int screenNumber = GDK_SCREEN_XNUMBER(screen); + haveVisual = GLContextGLX::FindVisual(display, screenNumber, &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!"); + // We try to use a fallback alpha visual + GdkScreen* screen = gtk_widget_get_screen(mShell); + gdkVisual = gdk_screen_get_rgba_visual(screen); + } + if (gdkVisual) { + gtk_widget_set_visual(mShell, gdkVisual); + mHasAlphaVisual = true; + return true; + } + + return false; +} +#endif + +nsAutoCString nsWindow::GetFrameTag() const { + if (nsIFrame* frame = GetFrame()) { +#ifdef DEBUG_FRAME_DUMP + return frame->ListTag(); +#else + nsAutoCString buf; + buf.AppendPrintf("Frame(%p)", frame); + if (nsIContent* content = frame->GetContent()) { + buf.Append(' '); + AppendUTF16toUTF8(content->NodeName(), buf); + } + return buf; +#endif + } + return nsAutoCString("(no frame)"); +} + +nsCString nsWindow::GetPopupTypeName() { + switch (mPopupHint) { + case PopupType::Menu: + return nsCString("Menu"); + case PopupType::Tooltip: + return nsCString("Tooltip"); + case PopupType::Panel: + return nsCString("Panel/Utility"); + default: + return nsCString("Unknown"); + } +} + +// Disables all rendering of GtkWidget from Gtk side. +// We do our best to persuade Gtk/Gdk to ignore all painting +// to the widget. +static void GtkWidgetDisableUpdates(GtkWidget* aWidget) { + // Clear exposure mask - it disabled synthesized events. + GdkWindow* window = gtk_widget_get_window(aWidget); + gdk_window_set_events(window, (GdkEventMask)(gdk_window_get_events(window) & + (~GDK_EXPOSURE_MASK))); + + // Remove before/after paint handles from frame clock. + // It disables widget content updates. + GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window); + g_signal_handlers_disconnect_by_data(frame_clock, window); +} + +Window nsWindow::GetX11Window() { +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + return mGdkWindow ? gdk_x11_window_get_xid(mGdkWindow) : X11None; + } +#endif + return (Window) nullptr; +} + +void nsWindow::EnsureGdkWindow() { + if (!mGdkWindow) { + mGdkWindow = gtk_widget_get_window(mDrawToContainer ? GTK_WIDGET(mContainer) + : mShell); + g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", this); + } +} + +bool nsWindow::GetShapedState() { + return mIsTransparent && !mHasAlphaVisual && !mTransparencyBitmapForTitlebar; +} + +void nsWindow::ConfigureCompositor() { + MOZ_DIAGNOSTIC_ASSERT(mCompositorState == COMPOSITOR_ENABLED); + MOZ_DIAGNOSTIC_ASSERT(mIsMapped); + + LOG("nsWindow::ConfigureCompositor()"); + auto startCompositing = [self = RefPtr{this}, this]() -> void { + LOG(" moz_container_wayland_add_or_fire_initial_draw_callback " + "ConfigureCompositor"); + + // too late + if (mIsDestroyed || !mIsMapped) { + LOG(" quit, mIsDestroyed = %d mIsMapped = %d", !!mIsDestroyed, + mIsMapped); + return; + } + // Compositor will be resumed later by ResumeCompositorFlickering(). + if (mCompositorState == COMPOSITOR_PAUSED_FLICKERING) { + LOG(" quit, will be resumed by ResumeCompositorFlickering."); + return; + } + // Compositor will be resumed at nsWindow::SetCompositorWidgetDelegate(). + if (!mCompositorWidgetDelegate) { + LOG(" quit, missing mCompositorWidgetDelegate"); + return; + } + + ResumeCompositorImpl(); + }; + + if (GdkIsWaylandDisplay()) { +#ifdef MOZ_WAYLAND + moz_container_wayland_add_or_fire_initial_draw_callback(mContainer, + startCompositing); +#endif + } else { + startCompositing(); + } +} + +void nsWindow::ConfigureGdkWindow() { + LOG("nsWindow::ConfigureGdkWindow()"); + + EnsureGdkWindow(); + +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + GdkVisual* gdkVisual = gdk_window_get_visual(mGdkWindow); + Visual* visual = gdk_x11_visual_get_xvisual(gdkVisual); + int depth = gdk_visual_get_depth(gdkVisual); + mSurfaceProvider.Initialize(GetX11Window(), visual, depth, + GetShapedState()); + + // 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); + } +#endif +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + mSurfaceProvider.Initialize(this); + } +#endif + + if (mIsDragPopup) { + if (GdkIsWaylandDisplay()) { + // Disable painting to the widget on Wayland as we paint directly to the + // widget. Wayland compositors does not paint wl_subsurface + // of D&D widget. + if (GtkWidget* parent = gtk_widget_get_parent(mShell)) { + GtkWidgetDisableUpdates(parent); + } + GtkWidgetDisableUpdates(mShell); + GtkWidgetDisableUpdates(GTK_WIDGET(mContainer)); + } else { + // Disable rendering of parent container on X11 to avoid flickering. + if (GtkWidget* parent = gtk_widget_get_parent(mShell)) { + gtk_widget_set_opacity(parent, 0.0); + } + } + } + + if (mWindowType == WindowType::Popup) { + if (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. + SetInputRegion(mInputRegion); + } + + RefreshWindowClass(); + + // We're not mapped yet but we have already created compositor. + if (mCompositorWidgetDelegate) { + ConfigureCompositor(); + } + + LOG(" finished, new GdkWindow %p XID 0x%lx\n", mGdkWindow, GetX11Window()); +} + +void nsWindow::ReleaseGdkWindow() { + LOG("nsWindow::ReleaseGdkWindow()"); + + DestroyChildWindows(); + + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->DisableRendering(); + } + + if (mGdkWindow) { + g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", nullptr); + mGdkWindow = nullptr; + } + + mSurfaceProvider.CleanupResources(); +} + +nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + widget::InitData* aInitData) { + LOG("nsWindow::Create\n"); + + // only set the base parent if we're going to be a dialog or a + // toplevel + nsIWidget* baseParent = + aInitData && (aInitData->mWindowType == WindowType::Dialog || + aInitData->mWindowType == WindowType::TopLevel || + aInitData->mWindowType == WindowType::Invisible) + ? nullptr + : aParent; + +#ifdef ACCESSIBILITY + // Send a DBus message to check whether a11y is enabled + a11y::PreInit(); +#endif + +#ifdef MOZ_WAYLAND + // Ensure that KeymapWrapper is created on Wayland as we need it for + // keyboard focus tracking. + if (GdkIsWaylandDisplay()) { + KeymapWrapper::EnsureInstance(); + } +#endif + + // Ensure that the toolkit is created. + nsGTKToolkit::GetToolkit(); + + // initialize all the common bits of this class + BaseCreate(baseParent, aInitData); + + // and do our common creation + mParent = aParent; + mCreated = true; + // save our bounds + mBounds = aRect; + LOG(" mBounds: x:%d y:%d w:%d h:%d\n", mBounds.x, mBounds.y, mBounds.width, + mBounds.height); + + ConstrainSize(&mBounds.width, &mBounds.height); + mLastSizeRequest = mBounds.Size(); + + GtkWidget* eventWidget = nullptr; + bool popupNeedsAlphaVisual = mWindowType == WindowType::Popup && + (aInitData && aInitData->mTransparencyMode == + TransparencyMode::Transparent); + + // Figure out our parent window - only used for WindowType::Child + 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; + } + } + + if (mWindowType == WindowType::Child) { + // We don't support WindowType::Child directly but emulate it by popup + // windows. + mWindowType = WindowType::Popup; + if (!parentnsWindow) { + if (aNativeParent && GTK_IS_CONTAINER(aNativeParent)) { + parentnsWindow = get_window_for_gtk_widget(GTK_WIDGET(aNativeParent)); + } + } + mIsChildWindow = true; + LOG(" child widget, switch to popup. parent nsWindow %p", parentnsWindow); + } + + if (mWindowType == WindowType::Popup && !parentnsWindow) { + LOG(" popup window without parent!"); + if (GdkIsWaylandDisplay()) { + LOG(" switch to toplevel on Wayland."); + // Wayland does not allow to create popup without parent so switch to + // toplevel and mark as wayland panel. + mIsWaylandPanelWindow = true; + mWindowType = WindowType::TopLevel; + } + } + + if (mWindowType != WindowType::Dialog && mWindowType != WindowType::Popup && + mWindowType != WindowType::TopLevel && + mWindowType != WindowType::Invisible) { + MOZ_ASSERT_UNREACHABLE("Unexpected eWindowType"); + return NS_ERROR_FAILURE; + } + + mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop; + mIsPIPWindow = aInitData && aInitData->mPIPWindow; + // mNoAutoHide seems to be always false here. + // The mNoAutoHide state is set later on nsMenuPopupFrame level + // and can be changed so we use WaylandPopupIsPermanent() to get + // recent popup config (Bug 1728952). + mNoAutoHide = aInitData && aInitData->mNoAutoHide; + + // 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 == WindowType::Popup) { + MOZ_ASSERT(aInitData); + type = GTK_WINDOW_POPUP; + if (GdkIsX11Display() && mNoAutoHide) { + type = GTK_WINDOW_TOPLEVEL; + } + } + mShell = gtk_window_new(type); + + // Ensure gfxPlatform is initialized, since that is what initializes + // gfxVars, used below. + Unused << gfxPlatform::GetPlatform(); + + if (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog) { + mGtkWindowDecoration = GetSystemGtkWindowDecoration(); + } + + // Don't use transparency for PictureInPicture windows. + bool toplevelNeedsAlphaVisual = false; + if (mWindowType == WindowType::TopLevel && !mIsPIPWindow) { + toplevelNeedsAlphaVisual = IsToplevelWindowTransparent(); + } + + bool isGLVisualSet = false; + mIsAccelerated = ComputeShouldAccelerate(); +#ifdef MOZ_X11 + if (GdkIsX11Display() && mIsAccelerated) { + isGLVisualSet = ConfigureX11GLVisual(); + } +#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 TransparencyMode::Transparent + // from layout, so set mIsTransparent here. + if (mWindowType == WindowType::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. + if (AreBoundsSane()) { + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size()); + LOG("nsWindow::Create() Initial resize to %d x %d\n", size.width, + size.height); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + } + if (mIsPIPWindow) { + LOG(" Is PIP Window\n"); + gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_UTILITY); + } else if (mWindowType == WindowType::Dialog) { + mGtkWindowRoleName = "Dialog"; + + SetDefaultIcon(); + gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_DIALOG); + LOG("nsWindow::Create(): dialog"); + if (parentnsWindow) { + GtkWindowSetTransientFor(GTK_WINDOW(mShell), + GTK_WINDOW(parentnsWindow->GetGtkWidget())); + LOG(" set parent window [%p]\n", parentnsWindow); + } + } else if (mWindowType == WindowType::Popup) { + MOZ_ASSERT(aInitData); + mGtkWindowRoleName = "Popup"; + mPopupHint = aInitData->mPopupHint; + + LOG("nsWindow::Create() Popup"); + + if (mNoAutoHide) { + // ... but the window manager does not decorate this window, + // nor provide a separate taskbar icon. + if (mBorderStyle == BorderStyle::Default) { + gtk_window_set_decorated(GTK_WINDOW(mShell), FALSE); + } else { + bool decorate = bool(mBorderStyle & BorderStyle::Title); + gtk_window_set_decorated(GTK_WINDOW(mShell), decorate); + if (decorate) { + gtk_window_set_deletable(GTK_WINDOW(mShell), + bool(mBorderStyle & BorderStyle::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 (GdkIsX11Display()) { + gtk_widget_realize(mShell); + gdk_window_add_filter(gtk_widget_get_window(mShell), + popup_take_focus_filter, nullptr); + } +#endif + } + + if (aInitData->mIsDragPopup) { + gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_DND); + mIsDragPopup = true; + LOG("nsWindow::Create() Drag popup\n"); + } else if (GdkIsX11Display()) { + // Set the window hints on X11 only. Wayland popups are configured + // at WaylandPopupConfigure(). + GdkWindowTypeHint gtkTypeHint; + switch (mPopupHint) { + case PopupType::Menu: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU; + break; + case PopupType::Tooltip: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP; + break; + default: + gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY; + break; + } + gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint); + LOG("nsWindow::Create() popup type %s", GetPopupTypeName().get()); + } + if (parentnsWindow) { + LOG(" set parent window [%p] %s", parentnsWindow, + parentnsWindow->mGtkWindowRoleName.get()); + GtkWindow* parentWidget = GTK_WINDOW(parentnsWindow->GetGtkWidget()); + GtkWindowSetTransientFor(GTK_WINDOW(mShell), parentWidget); + + // If popup parent is modal, we need to make popup modal too. + if (mPopupHint != PopupType::Tooltip && + gtk_window_get_modal(parentWidget)) { + gtk_window_set_modal(GTK_WINDOW(mShell), true); + } + } + + // We need realized mShell at NativeMoveResize(). + gtk_widget_realize(mShell); + + // With popup windows, we want to set their position. + // Place them immediately on X11 and save initial popup position + // on Wayland as we place Wayland popup on show. + if (GdkIsX11Display()) { + NativeMoveResize(/* move */ true, /* resize */ false); + } else if (AreBoundsSane()) { + GdkRectangle rect = DevicePixelsToGdkRectRoundOut(mBounds); + mPopupPosition = {rect.x, rect.y}; + } + } else { // must be WindowType::TopLevel + mGtkWindowRoleName = "Toplevel"; + SetDefaultIcon(); + + LOG("nsWindow::Create() Toplevel\n"); + + // 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); + } + + // It is important that this happens before the realize() call below, so that + // we don't get bogus CSD margins on Wayland, see bug 1794577. + if (IsAlwaysUndecoratedWindow()) { + LOG(" Is undecorated Window\n"); + gtk_window_set_titlebar(GTK_WINDOW(mShell), gtk_fixed_new()); + gtk_window_set_decorated(GTK_WINDOW(mShell), false); + } + + // Create a container to hold child windows and child GtkWidgets. + GtkWidget* container = moz_container_new(); + mContainer = MOZ_CONTAINER(container); + + // "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 = GdkIsWaylandDisplay() || + mGtkWindowDecoration == GTK_DECORATION_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, StaticPrefs::widget_transparent_windows_AtStartup()); + + gtk_widget_add_events(eventWidget, kEvents); + + if (mDrawToContainer) { + gtk_widget_add_events(mShell, GDK_PROPERTY_CHANGE_MASK); + gtk_widget_set_app_paintable( + mShell, StaticPrefs::widget_transparent_windows_AtStartup()); + } + 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); + } + +#ifdef MOZ_WAYLAND + if (mIsDragPopup && GdkIsWaylandDisplay()) { + LOG(" set commit to parent"); + moz_container_wayland_set_commit_to_parent(mContainer); + } +#endif + + if (mWindowType == WindowType::Popup) { + MOZ_ASSERT(aInitData); + // gdk does not automatically set the cursor for "temporary" + // windows, which are what gtk uses for popups. + + // force SetCursor to actually set the cursor, even though our internal + // state indicates that we already have the standard cursor. + mUpdateCursor = true; + SetCursor(Cursor{eCursor_standard}); + } + + if (mIsChildWindow && parentnsWindow) { + GdkWindow* window = GetToplevelGdkWindow(); + GdkWindow* parentWindow = parentnsWindow->GetToplevelGdkWindow(); + LOG(" child GdkWindow %p set parent GdkWindow %p", window, parentWindow); + gdk_window_reparent(window, parentWindow, + DevicePixelsToGdkCoordRoundDown(mBounds.x), + DevicePixelsToGdkCoordRoundDown(mBounds.y)); + } + + 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); + } + + g_object_set_data(G_OBJECT(mContainer), "nsWindow", this); + g_object_set_data(G_OBJECT(mShell), "nsWindow", this); + + // attach listeners for events + 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 == WindowType::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), + nullptr)) { + g_signal_connect(screen, "composited-changed", + G_CALLBACK(screen_composited_changed_cb), nullptr); + } + + gtk_drag_dest_set((GtkWidget*)mShell, (GtkDestDefaults)0, nullptr, 0, + (GdkDragAction)0); + g_signal_connect(mShell, "drag_motion", G_CALLBACK(drag_motion_event_cb), + nullptr); + g_signal_connect(mShell, "drag_leave", G_CALLBACK(drag_leave_event_cb), + nullptr); + g_signal_connect(mShell, "drag_drop", G_CALLBACK(drag_drop_event_cb), + nullptr); + g_signal_connect(mShell, "drag_data_received", + G_CALLBACK(drag_data_received_event_cb), nullptr); + + GtkSettings* default_settings = gtk_settings_get_default(); + g_signal_connect_after(default_settings, "notify::gtk-xft-dpi", + G_CALLBACK(settings_xft_dpi_changed_cb), this); + + // Widget signals + g_signal_connect(mContainer, "map", G_CALLBACK(widget_map_cb), nullptr); + g_signal_connect(mContainer, "unmap", G_CALLBACK(widget_unmap_cb), nullptr); + g_signal_connect(mContainer, "unrealize", G_CALLBACK(widget_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); + +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + 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 +#ifdef MOZ_WAYLAND + // Initialize the window specific VsyncSource early in order to avoid races + // with BrowserParent::UpdateVsyncParentVsyncDispatcher(). + // Only use for toplevel windows for now, see bug 1619246. + if (GdkIsWaylandDisplay() && + StaticPrefs::widget_wayland_vsync_enabled_AtStartup() && + mWindowType == WindowType::TopLevel) { + mWaylandVsyncSource = new WaylandVsyncSource(this); + mWaylandVsyncDispatcher = new VsyncDispatcher(mWaylandVsyncSource); + LOG_VSYNC(" created WaylandVsyncSource"); + } +#endif + + // We create input contexts for all containers, except for + // toplevel popup windows + if (mWindowType != WindowType::Popup) { + mIMContext = new IMContextWrapper(this); + } + + // 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 type %d %s\n", int(mWindowType), + mIsPIPWindow ? "PIP window" : ""); + LOG(" mShell %p mContainer %p mGdkWindow %p XID 0x%lx\n", mShell, mContainer, + mGdkWindow, GetX11Window()); + + // Set default application name when it's empty. + if (mGtkWindowAppName.IsEmpty()) { + mGtkWindowAppName = gAppData->name; + } + + 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 (GdkIsX11Display()) { + XClassHint* class_hint = XAllocClassHint(); + if (!class_hint) return; + + const char* res_name = + !mGtkWindowAppName.IsEmpty() ? mGtkWindowAppName.get() : gAppData->name; + + const char* res_class = !mGtkWindowAppClass.IsEmpty() + ? mGtkWindowAppClass.get() + : gdk_get_program_class(); + + if (!res_name || !res_class) { + XFree(class_hint); + return; + } + + class_hint->res_name = const_cast<char*>(res_name); + 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, + const nsAString& xulWinClass, + const nsAString& xulWinName) { + if (!mShell) return; + + // If window type attribute is set, parse it into name and role + if (!xulWinType.IsEmpty()) { + char* res_name = ToNewCString(xulWinType, mozilla::fallible); + const char* role = nullptr; + + if (res_name) { + // 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] = (char)toupper(res_name[0]); + if (!role) role = res_name; + + mGtkWindowAppName = res_name; + mGtkWindowRoleName = role; + free(res_name); + } + } + + // If window class attribute is set, store it as app class + // If this attribute is not set, reset app class to default + if (!xulWinClass.IsEmpty()) { + CopyUTF16toUTF8(xulWinClass, mGtkWindowAppClass); + } else { + mGtkWindowAppClass = nullptr; + } + + // If window class attribute is set, store it as app name + // If both name and type are not set, reset app name to default + if (!xulWinName.IsEmpty()) { + CopyUTF16toUTF8(xulWinName, mGtkWindowAppName); + } else if (xulWinType.IsEmpty()) { + mGtkWindowAppClass = nullptr; + } + + RefreshWindowClass(); +} + +nsAutoCString nsWindow::GetDebugTag() const { + nsAutoCString tag; + tag.AppendPrintf("[%p]", this); + return tag; +} + +void nsWindow::NativeMoveResize(bool aMoved, bool aResized) { + GdkPoint topLeft = [&] { + auto target = mBounds.TopLeft(); + // gtk_window_move will undo the csd offset, but nothing else, so only add + // the client offset if drawing to the csd titlebar. + if (DrawsToCSDTitlebar()) { + target += mClientOffset; + } + return DevicePixelsToGdkPointRoundDown(target); + }(); + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest); + + LOG("nsWindow::NativeMoveResize move %d resize %d to %d,%d -> %d x %d\n", + aMoved, aResized, topLeft.x, topLeft.y, size.width, size.height); + + if (aResized && !AreBoundsSane()) { + LOG(" bounds are insane, hidding the window"); + // We have been resized but to incorrect size. + // 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); + } + if (aMoved) { + LOG(" moving to %d x %d", topLeft.x, topLeft.y); + gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y); + } + return; + } + + // Set position to hidden window on X11 may fail, so save the position + // and move it when it's shown. + if (aMoved && GdkIsX11Display() && IsPopup() && + !gtk_widget_get_visible(GTK_WIDGET(mShell))) { + LOG(" store position of hidden popup window"); + mHiddenPopupPositioned = true; + mPopupPosition = {topLeft.x, topLeft.y}; + } + + if (IsWaylandPopup()) { + NativeMoveResizeWaylandPopup(aMoved, aResized); + } else { + // x and y give the position of the window manager frame top-left. + if (aMoved) { + gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y); + } + if (aResized) { + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + if (mIsDragPopup) { + // DND window is placed inside container so we need to make hard size + // request to ensure parent container is resized too. + gtk_widget_set_size_request(GTK_WIDGET(mShell), size.width, + size.height); + } + } + } + + if (aResized) { + // Recompute the input region, in case the window grew or shrunk. + SetInputRegion(mInputRegion); + } + + // Does it need to be shown because bounds were previously insane? + if (mNeedsShow && mIsShown && aResized) { + NativeShow(true); + } +} + +// We pause compositor to avoid rendering of obsoleted remote content which +// produces flickering. +// Re-enable compositor again when remote content is updated or +// timeout happens. + +// Define maximal compositor pause when it's paused to avoid flickering, +// in milliseconds. +#define COMPOSITOR_PAUSE_TIMEOUT (1000) + +void nsWindow::PauseCompositorFlickering() { + bool pauseCompositor = (mWindowType == WindowType::TopLevel) && + mCompositorState == COMPOSITOR_ENABLED && + mCompositorWidgetDelegate && !mIsDestroyed; + if (!pauseCompositor) { + return; + } + + LOG("nsWindow::PauseCompositorFlickering()"); + + MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove); + + CompositorBridgeChild* remoteRenderer = GetRemoteRenderer(); + if (remoteRenderer) { + remoteRenderer->SendPause(); + mCompositorState = COMPOSITOR_PAUSED_FLICKERING; + mCompositorPauseTimeoutID = (int)g_timeout_add( + COMPOSITOR_PAUSE_TIMEOUT, + [](void* data) -> gint { + nsWindow* window = static_cast<nsWindow*>(data); + if (!window->IsDestroyed()) { + window->ResumeCompositorFlickering(); + } + return true; + }, + this); + } +} + +bool nsWindow::IsWaitingForCompositorResume() { + return mCompositorState == COMPOSITOR_PAUSED_FLICKERING; +} + +void nsWindow::ResumeCompositorFlickering() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + LOG("nsWindow::ResumeCompositorFlickering()\n"); + + if (mIsDestroyed || !IsWaitingForCompositorResume()) { + LOG(" early quit\n"); + return; + } + + MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove); + + // mCompositorWidgetDelegate can be deleted during timeout. + // In such case just flip compositor back to enabled and let + // SetCompositorWidgetDelegate() or Map event resume it. + if (!mCompositorWidgetDelegate) { + mCompositorState = COMPOSITOR_ENABLED; + return; + } + + ResumeCompositorImpl(); +} + +void nsWindow::ResumeCompositorFromCompositorThread() { + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod("nsWindow::ResumeCompositorFlickering", this, + &nsWindow::ResumeCompositorFlickering); + NS_DispatchToMainThread(event.forget()); +} + +void nsWindow::ResumeCompositorImpl() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + LOG("nsWindow::ResumeCompositorImpl()\n"); + + MOZ_DIAGNOSTIC_ASSERT(mCompositorWidgetDelegate); + mCompositorWidgetDelegate->EnableRendering(GetX11Window(), GetShapedState()); + + // As WaylandStartVsync needs mCompositorWidgetDelegate this is the right + // time to start it. + WaylandStartVsync(); + + CompositorBridgeChild* remoteRenderer = GetRemoteRenderer(); + MOZ_RELEASE_ASSERT(remoteRenderer); + remoteRenderer->SendResume(); + remoteRenderer->SendForcePresent(wr::RenderReasons::WIDGET); + mCompositorState = COMPOSITOR_ENABLED; +} + +void nsWindow::WaylandStartVsync() { +#ifdef MOZ_WAYLAND + if (!mWaylandVsyncSource) { + return; + } + + LOG_VSYNC("nsWindow::WaylandStartVsync"); + + MOZ_DIAGNOSTIC_ASSERT(mCompositorWidgetDelegate); + if (mCompositorWidgetDelegate->AsGtkCompositorWidget() && + mCompositorWidgetDelegate->AsGtkCompositorWidget() + ->GetNativeLayerRoot()) { + LOG_VSYNC(" use source NativeLayerRootWayland"); + mWaylandVsyncSource->MaybeUpdateSource( + mCompositorWidgetDelegate->AsGtkCompositorWidget() + ->GetNativeLayerRoot() + ->AsNativeLayerRootWayland()); + } else { + LOG_VSYNC(" use source mContainer"); + mWaylandVsyncSource->MaybeUpdateSource(mContainer); + } + + mWaylandVsyncSource->EnableMonitor(); +#endif +} + +void nsWindow::WaylandStopVsync() { +#ifdef MOZ_WAYLAND + if (!mWaylandVsyncSource) { + return; + } + + LOG_VSYNC("nsWindow::WaylandStopVsync"); + + // The widget is going to be hidden, so clear the surface of our + // vsync source. + mWaylandVsyncSource->DisableMonitor(); + mWaylandVsyncSource->MaybeUpdateSource(nullptr); +#endif +} + +void nsWindow::NativeShow(bool aAction) { + if (aAction) { + // unset our flag now that our window has been shown + mNeedsShow = true; + auto removeShow = MakeScopeExit([&] { mNeedsShow = false; }); + + LOG("nsWindow::NativeShow show\n"); + + if (IsWaylandPopup()) { + mPopupClosed = false; + if (WaylandPopupConfigure()) { + AddWindowToPopupHierarchy(); + UpdateWaylandPopupHierarchy(); + if (mPopupClosed) { + return; + } + } + } + // Set up usertime/startupID metadata for the created window. + // On X11 we use gtk_window_set_startup_id() so we need to call it + // before show. + if (GdkIsX11Display()) { + SetUserTimeAndStartupTokenForActivatedWindow(); + } + if (GdkIsWaylandDisplay()) { + if (IsWaylandPopup()) { + ShowWaylandPopupWindow(); + } else { + ShowWaylandToplevelWindow(); + } + } else { + LOG(" calling gtk_widget_show(mShell)\n"); + gtk_widget_show(mShell); + } + if (GdkIsWaylandDisplay()) { + SetUserTimeAndStartupTokenForActivatedWindow(); +#ifdef MOZ_WAYLAND + auto token = std::move(mWindowActivationTokenFromEnv); + if (!token.IsEmpty()) { + FocusWaylandWindow(token.get()); + } +#endif + } + if (mHiddenPopupPositioned && IsPopup()) { + LOG(" re-position hidden popup window"); + gtk_window_move(GTK_WINDOW(mShell), mPopupPosition.x, mPopupPosition.y); + mHiddenPopupPositioned = false; + } + } else { + LOG("nsWindow::NativeShow hide\n"); + if (GdkIsWaylandDisplay()) { + if (IsWaylandPopup()) { + // We can't close tracked popups directly as they may have visible + // child popups. Just mark is as closed and let + // UpdateWaylandPopupHierarchy() do the job. + if (IsInPopupHierarchy()) { + WaylandPopupMarkAsClosed(); + UpdateWaylandPopupHierarchy(); + } else { + // Close untracked popups directly. + HideWaylandPopupWindow(/* aTemporaryHide */ false, + /* aRemoveFromPopupList */ true); + } + } else { + HideWaylandToplevelWindow(); + } + } else { + // 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 + } + } +} + +void nsWindow::SetHasMappedToplevel(bool aState) { + LOG("nsWindow::SetHasMappedToplevel(%d)", aState); + if (aState == mHasMappedToplevel) { + return; + } + // 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. + mHasMappedToplevel = aState; + if (aState && mNeedsToRetryCapturingMouse) { + CaptureRollupEvents(true); + MOZ_ASSERT(!mNeedsToRetryCapturingMouse); + } +} + +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 (mWindowRenderer && mWindowRenderer->AsKnowsCompositor()) { + maxSize = std::min( + maxSize, mWindowRenderer->AsKnowsCompositor()->GetMaxTextureSize()); + } + if (result.width > maxSize) { + result.width = maxSize; + } + if (result.height > maxSize) { + result.height = maxSize; + } + return result; +} + +void nsWindow::SetTransparencyMode(TransparencyMode aMode) { + bool isTransparent = aMode == TransparencyMode::Transparent; + + if (mIsTransparent == isTransparent) { + return; + } + + if (mWindowType != WindowType::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. + DestroyLayerManager(); + } +} + +TransparencyMode nsWindow::GetTransparencyMode() { + return mIsTransparent ? TransparencyMode::Transparent + : TransparencyMode::Opaque; +} + +gint nsWindow::GetInputRegionMarginInGdkCoords() { + return DevicePixelsToGdkCoordRoundDown(mInputRegion.mMargin); +} + +void nsWindow::SetInputRegion(const InputRegion& aInputRegion) { + mInputRegion = aInputRegion; + + GdkWindow* window = + mDrawToContainer ? gtk_widget_get_window(mShell) : mGdkWindow; + if (!window) { + return; + } + + LOG("nsWindow::SetInputRegion(%d, %d)", aInputRegion.mFullyTransparent, + int(aInputRegion.mMargin)); + + cairo_rectangle_int_t rect = {0, 0, 0, 0}; + cairo_region_t* region = nullptr; + auto releaseRegion = MakeScopeExit([&] { + if (region) { + cairo_region_destroy(region); + } + }); + + if (aInputRegion.mFullyTransparent) { + region = cairo_region_create_rectangle(&rect); + } else if (aInputRegion.mMargin != 0) { + LayoutDeviceIntRect inputRegion(LayoutDeviceIntPoint(), mLastSizeRequest); + inputRegion.Deflate(aInputRegion.mMargin); + GdkRectangle gdkRect = DevicePixelsToGdkRectRoundOut(inputRegion); + rect = {gdkRect.x, gdkRect.y, gdkRect.width, gdkRect.height}; + region = cairo_region_create_rectangle(&rect); + } + + gdk_window_input_shape_combine_region(window, region, 0, 0); + + // On Wayland gdk_window_input_shape_combine_region() call is cached and + // applied to underlying wl_surface when GdkWindow is repainted. + // Force repaint of GdkWindow to apply the change immediately. + if (GdkIsWaylandDisplay()) { + gdk_window_invalidate_rect(window, nullptr, false); + } +} + +// For setting the draggable titlebar region from CSS +// with -moz-window-dragging: drag. +void nsWindow::UpdateWindowDraggingRegion( + const LayoutDeviceIntRegion& aRegion) { + if (mDraggableRegion != aRegion) { + mDraggableRegion = aRegion; + } +} + +LayoutDeviceIntCoord nsWindow::GetTitlebarRadius() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + int32_t cssCoord = LookAndFeel::GetInt(LookAndFeel::IntID::TitlebarRadius); + return GdkCoordToDevicePixels(cssCoord); +} + +// 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, int aTitlebarRadius) { + if (!aTitlebarRadius) { + return; + } + cairo_rectangle_int_t rect = {aX, aY, aTitlebarRadius, aTitlebarRadius}; + cairo_region_subtract_rectangle(aRegion, &rect); + rect = { + aX + aWindowWidth - aTitlebarRadius, + aY, + aTitlebarRadius, + aTitlebarRadius, + }; + 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); + + int radius = DoDrawTilebarCorners() ? int(GetTitlebarRadius()) : 0; + SubtractTitlebarCorners(region, x, y, width, radius); + + gdk_window_set_opaque_region(window, region); + + cairo_region_destroy(region); + +#ifdef MOZ_WAYLAND + if (GdkIsWaylandDisplay()) { + moz_container_wayland_update_opaque_region(mContainer, radius); + } +#endif +} + +bool nsWindow::IsChromeWindowTitlebar() { + return mDrawInTitlebar && !mIsPIPWindow && + mWindowType == WindowType::TopLevel; +} + +bool nsWindow::DoDrawTilebarCorners() { + return IsChromeWindowTitlebar() && mSizeMode == nsSizeMode_Normal && + !mIsTiled; +} + +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); +#endif // MOZ_X11 +} + +void nsWindow::ClearTransparencyBitmap() { + if (!mTransparencyBitmap) return; + + delete[] mTransparencyBitmap; + mTransparencyBitmap = nullptr; + mTransparencyBitmapWidth = 0; + mTransparencyBitmapHeight = 0; + + if (!mShell) return; + +#ifdef MOZ_X11 + if (MOZ_UNLIKELY(!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) { + 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; +} + +#define TITLEBAR_HEIGHT 10 + +LayoutDeviceIntRect nsWindow::GetTitlebarRect() { + if (!mGdkWindow || !mDrawInTitlebar) { + return LayoutDeviceIntRect(); + } + + int height = 0; + if (DoDrawTilebarCorners()) { + height = GdkCeiledScaleFactor() * TITLEBAR_HEIGHT; + } + return LayoutDeviceIntRect(0, 0, mBounds.width, height); +} + +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; + + LayoutDeviceIntCoord radius = GetTitlebarRadius(); + if (maskCreate) { + delete[] mTransparencyBitmap; + int32_t size = GetBitmapStride(mBounds.width) * radius; + 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, radius); + if (!surface) return; + + cairo_t* cr = cairo_create(surface); + + GtkWidgetState state; + memset((void*)&state, 0, sizeof(state)); + GdkRectangle rect = {0, 0, mTransparencyBitmapWidth, radius}; + + 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, radius, + nsIntRect(0, 0, mTransparencyBitmapWidth, radius), + cairo_image_surface_get_data(surface), + cairo_format_stride_for_width(CAIRO_FORMAT_A8, + mTransparencyBitmapWidth)); + + cairo_surface_destroy(surface); + } + +#ifdef MOZ_X11 + if (!mNeedsShow) { + Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow); + Window xDrawable = GDK_WINDOW_XID(mGdkWindow); + + Pixmap maskPixmap = + XCreateBitmapFromData(xDisplay, xDrawable, mTransparencyBitmap, + mTransparencyBitmapWidth, radius); + + XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap, + ShapeSet); + + if (mTransparencyBitmapHeight > radius) { + XRectangle rect = {0, 0, (unsigned short)mTransparencyBitmapWidth, + (unsigned short)(mTransparencyBitmapHeight - radius)}; + XShapeCombineRectangles(xDisplay, xDrawable, ShapeBounding, 0, radius, + &rect, 1, ShapeUnion, 0); + } + + XFreePixmap(xDisplay, maskPixmap); + } +#endif +} + +GtkWidget* nsWindow::GetToplevelWidget() const { return mShell; } + +GdkWindow* nsWindow::GetToplevelGdkWindow() const { + return gtk_widget_get_window(mShell); +} + +nsWindow* nsWindow::GetContainerWindow() const { + GtkWidget* owningWidget = GTK_WIDGET(mContainer); + 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) { + LOG(" nsWindow::SetUrgencyHint widget %p\n", top_window); + + if (!top_window) return; + + // TODO: Use xdg-activation on Wayland? + gdk_window_set_urgency_hint(gtk_widget_get_window(top_window), state); +} + +void nsWindow::SetDefaultIcon(void) { SetIcon(u"default"_ns); } + +gint nsWindow::ConvertBorderStyles(BorderStyle aStyle) { + gint w = 0; + + if (aStyle == BorderStyle::Default) return -1; + + // note that we don't handle BorderStyle::Close yet + if (aStyle & BorderStyle::All) w |= GDK_DECOR_ALL; + if (aStyle & BorderStyle::Border) w |= GDK_DECOR_BORDER; + if (aStyle & BorderStyle::ResizeH) w |= GDK_DECOR_RESIZEH; + if (aStyle & BorderStyle::Title) w |= GDK_DECOR_TITLE; + if (aStyle & BorderStyle::Menu) w |= GDK_DECOR_MENU; + if (aStyle & BorderStyle::Minimize) w |= GDK_DECOR_MINIMIZE; + if (aStyle & BorderStyle::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); + GtkWindowSetTransientFor(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); + + GdkRGBA bgColor; + bgColor.red = bgColor.green = bgColor.blue = 0.0; + bgColor.alpha = 1.0; + gtk_widget_override_background_color(mWindow, GTK_STATE_FLAG_NORMAL, + &bgColor); + + gtk_widget_set_opacity(mWindow, 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_widget_set_opacity(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<widget::Screen> nsWindow::GetWidgetScreen() { + // Wayland can read screen directly + if (GdkIsWaylandDisplay()) { + if (RefPtr<Screen> screen = ScreenHelperGTK::GetScreenForWindow(this)) { + return screen.forget(); + } + } + + // GetScreenBounds() is slow for the GTK port so we override and use + // mBounds directly. + ScreenManager& screenManager = ScreenManager::GetSingleton(); + LayoutDeviceIntRect bounds = mBounds; + DesktopIntRect deskBounds = RoundedToInt(bounds / GetDesktopToDeviceScale()); + return screenManager.ScreenForRect(deskBounds); +} + +RefPtr<VsyncDispatcher> nsWindow::GetVsyncDispatcher() { +#ifdef MOZ_WAYLAND + if (mWaylandVsyncDispatcher) { + return mWaylandVsyncDispatcher; + } +#endif + return nullptr; +} + +bool nsWindow::SynchronouslyRepaintOnResize() { + if (GdkIsWaylandDisplay()) { + // See Bug 1734368 + // Don't request synchronous repaint on HW accelerated backend - mesa can be + // deadlocked when it's missing back buffer and main event loop is blocked. + return false; + } + + // default is synced repaint. + return true; +} + +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); + return gdk_x11_screen_supports_net_wm_hint(screen, atom); +#else + return true; +#endif +} + +nsresult nsWindow::MakeFullScreen(bool aFullScreen) { + LOG("nsWindow::MakeFullScreen aFullScreen %d\n", aFullScreen); + + if (GdkIsX11Display() && !IsFullscreenSupported(mShell)) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (aFullScreen) { + if (mSizeMode != nsSizeMode_Fullscreen && + mSizeMode != nsSizeMode_Minimized) { + mLastSizeModeBeforeFullscreen = mSizeMode; + } + 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 { + 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(); + } + } + } + + MOZ_ASSERT(mLastSizeModeBeforeFullscreen != nsSizeMode_Fullscreen); + return NS_OK; +} + +void nsWindow::SetWindowDecoration(BorderStyle aStyle) { + LOG("nsWindow::SetWindowDecoration() Border style %x\n", int(aStyle)); + + // 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 (GdkIsX11Display()) { + XSync(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), X11False); + } else +#endif /* MOZ_X11 */ + { + gdk_flush(); + } +} + +void nsWindow::HideWindowChrome(bool aShouldHide) { + SetWindowDecoration(aShouldHide ? BorderStyle::None : mBorderStyle); +} + +bool nsWindow::CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel, + bool aAlwaysRollup) { + LOG("nsWindow::CheckForRollup() aAlwaysRollup %d", aAlwaysRollup); + nsIRollupListener* rollupListener = GetActiveRollupListener(); + nsCOMPtr<nsIWidget> rollupWidget; + if (rollupListener) { + rollupWidget = rollupListener->GetRollupWidget(); + } + if (!rollupWidget) { + return false; + } + + auto* currentPopup = + (GdkWindow*)rollupWidget->GetNativeData(NS_NATIVE_WINDOW); + if (!aAlwaysRollup && is_mouse_in_window(currentPopup, aMouseX, aMouseY)) { + return false; + } + bool retVal = false; + if (aIsWheel) { + retVal = rollupListener->ShouldConsumeOnMouseWheelEvent(); + if (!rollupListener->ShouldRollupOnMouseWheelEvent()) { + return retVal; + } + } + LayoutDeviceIntPoint point; + nsIRollupListener::RollupOptions options{0, + nsIRollupListener::FlushViews::Yes}; + // 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 + 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) { + return retVal; + } + options.mCount = sameTypeCount; + break; + } + } // foreach parent menu widget + if (!aIsWheel) { + point = GdkEventCoordsToDevicePixels(aMouseX, aMouseY); + options.mPoint = &point; + } + } + + if (mSizeMode == nsSizeMode_Minimized) { + // When we try to rollup in a minimized window, transitionend events for + // panels might not fire and thus we might not hide the popup after all, + // see bug 1810797. + options.mAllowAnimations = nsIRollupListener::AllowAnimations::No; + } + + if (rollupListener->Rollup(options)) { + retVal = true; + } + return retVal; +} + +/* static */ +bool nsWindow::DragInProgress() { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (!dragService) { + return false; + } + + nsCOMPtr<nsIDragSession> currentDragSession; + dragService->GetCurrentSession(getter_AddRefs(currentDragSession)); + return !!currentDragSession; +} + +// This is an ugly workaround for +// https://bugzilla.mozilla.org/show_bug.cgi?id=1622107 +// We try to detect when Wayland compositor / gtk fails to deliver +// info about finished D&D operations and cancel it on our own. +MOZ_CAN_RUN_SCRIPT static void WaylandDragWorkaround(GdkEventButton* aEvent) { + static int buttonPressCountWithDrag = 0; + + // We track only left button state as Firefox performs D&D on left + // button only. + if (aEvent->button != 1 || aEvent->type != GDK_BUTTON_PRESS) { + return; + } + + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (!dragService) { + return; + } + nsCOMPtr<nsIDragSession> currentDragSession; + dragService->GetCurrentSession(getter_AddRefs(currentDragSession)); + + if (!currentDragSession) { + buttonPressCountWithDrag = 0; + return; + } + + buttonPressCountWithDrag++; + if (buttonPressCountWithDrag > 1) { + NS_WARNING( + "Quit unfinished Wayland Drag and Drop operation. Buggy Wayland " + "compositor?"); + buttonPressCountWithDrag = 0; + dragService->EndDragSession(false, 0); + } +} + +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 bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX, + gdouble aMouseY) { + GdkWindow* window = aWindow; + if (!window) { + return false; + } + + gint x = 0; + gint y = 0; + + { + gint offsetX = 0; + gint offsetY = 0; + + 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); + } + } + + gint margin = 0; + if (nsWindow* w = get_window_for_gdk_window(aWindow)) { + margin = w->GetInputRegionMarginInGdkCoords(); + } + + x += margin; + y += margin; + + gint w = gdk_window_get_width(aWindow) - margin; + gint h = gdk_window_get_height(aWindow) - margin; + + return aMouseX > x && aMouseX < x + w && aMouseY > y && aMouseY < y + h; +} + +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: + gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "vertical-text"); + if (!gdkcursor) { + 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 = (char)*bits++; + char mask = (char)*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_gtk_widget(widget); + 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); + } + } +} + +/* 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); +} + +// Some Gtk widget code may call gtk_widget_unrealize() which destroys +// mGdkWindow. We need to listen on this signal and re-create +// mGdkWindow when we're already mapped. +static void widget_map_cb(GtkWidget* widget) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + window->OnMap(); +} + +static void widget_unmap_cb(GtkWidget* widget) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + window->OnUnmap(); +} + +static void widget_unrealize_cb(GtkWidget* widget) { + RefPtr<nsWindow> window = get_window_for_gtk_widget(widget); + if (!window) { + return; + } + window->OnUnrealize(); +} + +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; + } + + // We have stored leave notify - check if it's the correct one and + // fire it before enter notify in such case. + if (sStoredLeaveNotifyEvent) { + auto clearNofityEvent = + MakeScopeExit([&] { sStoredLeaveNotifyEvent = nullptr; }); + if (event->x_root == sStoredLeaveNotifyEvent->x_root && + event->y_root == sStoredLeaveNotifyEvent->y_root && + window->ApplyEnterLeaveMutterWorkaround()) { + // Enter/Leave notify events has the same coordinates + // and uses know buggy window config. + // Consider it as a bogus one. + return TRUE; + } + RefPtr<nsWindow> leftWindow = + get_window_for_gdk_window(sStoredLeaveNotifyEvent->window); + if (leftWindow) { + leftWindow->OnLeaveNotifyEvent(sStoredLeaveNotifyEvent.get()); + } + } + + window->OnEnterNotifyEvent(event); + return TRUE; +} + +static gboolean leave_notify_event_cb(GtkWidget* widget, + GdkEventCrossing* event) { + RefPtr<nsWindow> window = get_window_for_gdk_window(event->window); + if (!window) { + return TRUE; + } + + if (window->ApplyEnterLeaveMutterWorkaround()) { + // The leave event is potentially wrong, don't fire it now but store + // it for further check at enter_notify_event_cb(). + sStoredLeaveNotifyEvent.reset(reinterpret_cast<GdkEventCrossing*>( + gdk_event_copy(reinterpret_cast<GdkEvent*>(event)))); + } else { + sStoredLeaveNotifyEvent = nullptr; + 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); + + RefPtr<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); + + RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window); + if (!window) return FALSE; + + window->OnButtonPressEvent(event); + + if (GdkIsWaylandDisplay()) { + WaylandDragWorkaround(event); + } + + return TRUE; +} + +static gboolean button_release_event_cb(GtkWidget* widget, + GdkEventButton* event) { + UpdateLastInputEventTime(event); + + RefPtr<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) { + LOGW("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 (GdkIsX11Display(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) { + LOGW("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) { + RefPtr<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_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; + } + + window->OnScaleChanged(); +} + +static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent) { + UpdateLastInputEventTime(aEvent); + + RefPtr<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); + + RefPtr<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); +} + +static LayoutDeviceIntPoint GetWindowDropPosition(nsWindow* aWindow, int aX, + int aY) { + // Workaround for Bug 1710344 + // Caused by Gtk issue https://gitlab.gnome.org/GNOME/gtk/-/issues/4437 + if (aWindow->IsWaylandPopup()) { + int tx = 0, ty = 0; + gdk_window_get_position(aWindow->GetToplevelGdkWindow(), &tx, &ty); + aX += tx; + aY += ty; + } + LOGDRAG("WindowDropPosition [%d, %d]", aX, aY); + return aWindow->GdkPointToDevicePixels({aX, aY}); +} + +gboolean WindowDragMotionHandler(GtkWidget* aWidget, + GdkDragContext* aDragContext, 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("WindowDragMotionHandler target nsWindow [%p]", + innerMostWindow.get()); + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + nsDragService::AutoEventLoop loop(dragService); + if (!dragService->ScheduleMotionEvent( + innerMostWindow, aDragContext, + GetWindowDropPosition(innerMostWindow, retx, rety), aTime)) { + return FALSE; + } + return TRUE; +} + +static gboolean drag_motion_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, guint aTime, gpointer aData) { + return WindowDragMotionHandler(aWidget, aDragContext, aX, aY, aTime); +} + +void WindowDragLeaveHandler(GtkWidget* aWidget) { + LOGDRAG("WindowDragLeaveHandler()\n"); + + RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget); + if (!window) { + LOGDRAG(" Failed - can't find nsWindow!\n"); + return; + } + + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + nsDragService::AutoEventLoop loop(dragService); + + 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. + LOGDRAG(" Failed - GetMostRecentDestWindow()!\n"); + return; + } + + if (aWidget != window->GetGtkWidget()) { + // 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. + LOGDRAG(" Failed - GtkWidget mismatch!\n"); + return; + } + + LOGDRAG("WindowDragLeaveHandler nsWindow %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, + 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("WindowDragDropHandler nsWindow [%p]", innerMostWindow.get()); + RefPtr<nsDragService> dragService = nsDragService::GetInstance(); + nsDragService::AutoEventLoop loop(dragService); + return dragService->ScheduleDropEvent( + innerMostWindow, aDragContext, + GetWindowDropPosition(innerMostWindow, retx, rety), aTime); +} + +static gboolean drag_drop_event_cb(GtkWidget* aWidget, + GdkDragContext* aDragContext, gint aX, + gint aY, guint aTime, gpointer aData) { + return WindowDragDropHandler(aWidget, aDragContext, 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) { + if (Preferences::HasUserValue("widget.use-aspect-ratio")) { + gUseAspectRatio = Preferences::GetBool("widget.use-aspect-ratio", true); + } else { + gUseAspectRatio = IsGnomeDesktopEnvironment() || IsKdeDesktopEnvironment(); + } + return NS_OK; +} + +// TODO: Can we simplify it for mShell/mContainer only scenario? +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; +} + +#ifdef ACCESSIBILITY +void nsWindow::CreateRootAccessible() { + if (!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::LocalAccessible* 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; +} + +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; + } + + Maybe<WritingMode> writingMode; + if (aEvent.NeedsToRemapNavigationKey()) { + if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) { + writingMode = dispatcher->MaybeQueryWritingModeAtSelection(); + } + } + + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + keyBindings->GetEditCommands(aEvent, writingMode, aCommands); + return true; +} + +already_AddRefed<DrawTarget> nsWindow::StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, BufferMode* aBufferMode) { + return mSurfaceProvider.StartRemoteDrawingInRegion(aInvalidRegion, + aBufferMode); +} + +void nsWindow::EndRemoteDrawingInRegion( + DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + mSurfaceProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion); +} + +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; + } + +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + // 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 + // XXXsmaug remove this old hack. gtk should be fixed now. + 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 TimeStamp lastTimeStamp; + if (lastTimeStamp != aMouseEvent->mTimeStamp) { + lastTimeStamp = aMouseEvent->mTimeStamp; + } else { + return false; + } + } + } +#endif + + // 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; +} + +nsIWidget::WindowRenderer* nsWindow::GetWindowRenderer() { + 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 mWindowRenderer; + } + + return nsBaseWidget::GetWindowRenderer(); +} + +void nsWindow::DidGetNonBlankPaint() { + if (mGotNonBlankPaint) { + return; + } + mGotNonBlankPaint = true; + if (!mConfiguredClearColor) { + // Nothing to do, we hadn't overridden the clear color. + mConfiguredClearColor = true; + return; + } + // Reset the clear color set in the expose event to transparent. + GetWindowRenderer()->AsWebRender()->WrBridge()->SendSetDefaultClearColor( + NS_TRANSPARENT); +} + +/* nsWindow::SetCompositorWidgetDelegate() sets remote GtkCompositorWidget + * to render into with compositor. + * + * SetCompositorWidgetDelegate(delegate) is called from + * nsBaseWidget::CreateCompositor(), i.e. nsWindow::GetWindowRenderer(). + * + * SetCompositorWidgetDelegate(null) is called from + * nsBaseWidget::DestroyCompositor(). + */ +void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) { + LOG("nsWindow::SetCompositorWidgetDelegate %p mIsMapped %d " + "mCompositorWidgetDelegate %p\n", + delegate, mIsMapped, mCompositorWidgetDelegate); + + if (delegate) { + mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate(); + MOZ_ASSERT(mCompositorWidgetDelegate, + "nsWindow::SetCompositorWidgetDelegate called with a " + "non-PlatformCompositorWidgetDelegate"); + if (mIsMapped) { + ConfigureCompositor(); + } + } else { + mCompositorWidgetDelegate = nullptr; + } +} + +nsresult nsWindow::SetNonClientMargins(const LayoutDeviceIntMargin& aMargins) { + SetDrawsInTitlebar(aMargins.top == 0); + return NS_OK; +} + +bool nsWindow::IsAlwaysUndecoratedWindow() const { + if (mIsPIPWindow || mIsWaylandPanelWindow) { + return true; + } + if (mWindowType == WindowType::Dialog && + mBorderStyle != BorderStyle::Default && + mBorderStyle != BorderStyle::All && + !(mBorderStyle & BorderStyle::Title) && + !(mBorderStyle & BorderStyle::ResizeH)) { + return true; + } + return false; +} + +void nsWindow::SetDrawsInTitlebar(bool aState) { + LOG("nsWindow::SetDrawsInTitlebar() State %d mGtkWindowDecoration %d\n", + aState, (int)mGtkWindowDecoration); + + if (mGtkWindowDecoration == GTK_DECORATION_NONE || + aState == mDrawInTitlebar) { + LOG(" already set, quit"); + return; + } + + if (IsAlwaysUndecoratedWindow()) { + MOZ_ASSERT(aState, "Unexpected decoration request"); + MOZ_ASSERT(!gtk_window_get_decorated(GTK_WINDOW(mShell))); + return; + } + + if (mGtkWindowDecoration == GTK_DECORATION_SYSTEM) { + SetWindowDecoration(aState ? BorderStyle::Border : mBorderStyle); + } else if (mGtkWindowDecoration == GTK_DECORATION_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. + */ + bool visible = !mNeedsShow && mIsShown; + if (visible) { + 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)); + + // 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); + + if (AreBoundsSane()) { + GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size()); + LOG(" resize to %d x %d\n", size.width, size.height); + gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height); + } + + if (visible) { + mNeedsShow = true; + NativeShow(true); + } + + gtk_widget_destroy(tmpWindow); + } + + mDrawInTitlebar = aState; + + if (mTransparencyBitmapForTitlebar) { + if (mDrawInTitlebar && mSizeMode == nsSizeMode_Normal && !mIsTiled) { + UpdateTitlebarTransparencyBitmap(); + } else { + ClearTransparencyBitmap(); + } + } +} + +GtkWindow* nsWindow::GetCurrentTopmostWindow() const { + GtkWindow* parentWindow = GTK_WINDOW(GetGtkWidget()); + GtkWindow* topmostParentWindow = nullptr; + while (parentWindow) { + topmostParentWindow = parentWindow; + parentWindow = gtk_window_get_transient_for(parentWindow); + } + return topmostParentWindow; +} + +gint nsWindow::GdkCeiledScaleFactor() { + // 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 == WindowType::TopLevel && !mWindowScaleFactorChanged) { + return mWindowScaleFactor; + } + + GdkWindow* scaledGdkWindow = nullptr; + if (GdkIsWaylandDisplay()) { + // 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 == WindowType::Popup || mWindowType == WindowType::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; + } + if (scaledGdkWindow) { + mWindowScaleFactor = gdk_window_get_scale_factor(scaledGdkWindow); + mWindowScaleFactorChanged = false; + } else { + mWindowScaleFactor = ScreenHelperGTK::GetGTKMonitorScaleFactor(); + } + return mWindowScaleFactor; +} + +bool nsWindow::UseFractionalScale() const { +#ifdef MOZ_WAYLAND + return GdkIsWaylandDisplay() && + StaticPrefs::widget_wayland_fractional_buffer_scale_AtStartup() > 0 && + WaylandDisplayGet()->GetViewporter(); +#else + return false; +#endif +} + +double nsWindow::FractionalScaleFactor() { +#ifdef MOZ_WAYLAND + if (UseFractionalScale()) { + double scale = + StaticPrefs::widget_wayland_fractional_buffer_scale_AtStartup(); + scale = std::max(scale, 0.5); + scale = std::min(scale, 8.0); + return scale; + } +#endif + return GdkCeiledScaleFactor(); +} + +gint nsWindow::DevicePixelsToGdkCoordRoundUp(int aPixels) { + double scale = FractionalScaleFactor(); + return ceil(aPixels / scale); +} + +gint nsWindow::DevicePixelsToGdkCoordRoundDown(int aPixels) { + double scale = FractionalScaleFactor(); + return floor(aPixels / scale); +} + +GdkPoint nsWindow::DevicePixelsToGdkPointRoundDown( + const LayoutDeviceIntPoint& aPoint) { + double scale = FractionalScaleFactor(); + return {int(aPoint.x / scale), int(aPoint.y / scale)}; +} + +GdkRectangle nsWindow::DevicePixelsToGdkRectRoundOut( + const LayoutDeviceIntRect& aRect) { + double scale = FractionalScaleFactor(); + int x = floor(aRect.x / scale); + int y = floor(aRect.y / scale); + int right = ceil((aRect.x + aRect.width) / scale); + int bottom = ceil((aRect.y + aRect.height) / scale); + return {x, y, right - x, bottom - y}; +} + +GdkRectangle nsWindow::DevicePixelsToGdkSizeRoundUp( + const LayoutDeviceIntSize& aSize) { + double scale = FractionalScaleFactor(); + gint width = ceil(aSize.width / scale); + gint height = ceil(aSize.height / scale); + return {0, 0, width, height}; +} + +int nsWindow::GdkCoordToDevicePixels(gint aCoord) { + return (int)(aCoord * FractionalScaleFactor()); +} + +LayoutDeviceIntPoint nsWindow::GdkEventCoordsToDevicePixels(gdouble aX, + gdouble aY) { + double scale = FractionalScaleFactor(); + return LayoutDeviceIntPoint::Floor((float)(aX * scale), (float)(aY * scale)); +} + +LayoutDeviceIntPoint nsWindow::GdkPointToDevicePixels(const GdkPoint& aPoint) { + double scale = FractionalScaleFactor(); + return LayoutDeviceIntPoint::Floor((float)(aPoint.x * scale), + (float)(aPoint.y * scale)); +} + +LayoutDeviceIntRect nsWindow::GdkRectToDevicePixels(const GdkRectangle& aRect) { + double scale = FractionalScaleFactor(); + return LayoutDeviceIntRect::RoundIn( + (float)(aRect.x * scale), (float)(aRect.y * scale), + (float)(aRect.width * scale), (float)(aRect.height * scale)); +} + +nsresult nsWindow::SynthesizeNativeMouseEvent( + LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage, + MouseButton aButton, nsIWidget::Modifiers aModifierFlags, + nsIObserver* aObserver) { + LOG("SynthesizeNativeMouseEvent(%d, %d, %d, %d, %d)", aPoint.x.value, + aPoint.y.value, int(aNativeMessage), int(aButton), int(aModifierFlags)); + + AutoObserverNotifier notifier(aObserver, "mouseevent"); + + if (!mGdkWindow) { + return NS_OK; + } + + // 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. + switch (aNativeMessage) { + case NativeMouseMessage::ButtonDown: + case NativeMouseMessage::ButtonUp: { + GdkEvent event; + memset(&event, 0, sizeof(GdkEvent)); + event.type = aNativeMessage == NativeMouseMessage::ButtonDown + ? GDK_BUTTON_PRESS + : GDK_BUTTON_RELEASE; + switch (aButton) { + case MouseButton::ePrimary: + case MouseButton::eMiddle: + case MouseButton::eSecondary: + case MouseButton::eX1: + case MouseButton::eX2: + event.button.button = aButton + 1; + break; + default: + return NS_ERROR_INVALID_ARG; + } + event.button.state = + KeymapWrapper::ConvertWidgetModifierToGdkState(aModifierFlags); + event.button.window = mGdkWindow; + event.button.time = GDK_CURRENT_TIME; + + // Get device for event source + event.button.device = GdkGetPointer(); + + 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); + return NS_OK; + } + case NativeMouseMessage::Move: { + // 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(). + // XXX How to activate native modifier for the other events? +#ifdef MOZ_WAYLAND + // Impossible to warp the pointer on Wayland. + // For pointer lock, pointer-constraints and relative-pointer are used. + if (GdkIsWaylandDisplay()) { + return NS_OK; + } +#endif + GdkScreen* screen = gdk_window_get_screen(mGdkWindow); + GdkPoint point = DevicePixelsToGdkPointRoundDown(aPoint); + gdk_device_warp(GdkGetPointer(), screen, point.x, point.y); + return NS_OK; + } + case NativeMouseMessage::EnterWindow: + case NativeMouseMessage::LeaveWindow: + MOZ_ASSERT_UNREACHABLE("Non supported mouse event on Linux"); + return NS_ERROR_INVALID_ARG; + } + return NS_ERROR_UNEXPECTED; +} + +void nsWindow::CreateAndPutGdkScrollEvent(mozilla::LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY) { + 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); + // See note in nsWindow::SynthesizeNativeTouchpadPan about the device we use + // here. + 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); +} + +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; + } + + CreateAndPutGdkScrollEvent(aPoint, aDeltaX, aDeltaY); + + 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; +} + +nsresult nsWindow::SynthesizeNativeTouchPadPinch( + TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint, + int32_t aModifierFlags) { + if (!mGdkWindow) { + return NS_OK; + } + GdkEvent event; + memset(&event, 0, sizeof(GdkEvent)); + + GdkEventTouchpadPinch* touchpad_event = + reinterpret_cast<GdkEventTouchpadPinch*>(&event); + touchpad_event->type = GDK_TOUCHPAD_PINCH; + + const ScreenIntPoint widgetToScreenOffset = ViewAs<ScreenPixel>( + WidgetToScreenOffset(), + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + + ScreenPoint pointInWindow = + ViewAs<ScreenPixel>( + aPoint, + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent) - + widgetToScreenOffset; + + gdouble dx = 0, dy = 0; + + switch (aEventPhase) { + case PHASE_BEGIN: + touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_BEGIN; + mCurrentSynthesizedTouchpadPinch = {pointInWindow, pointInWindow}; + break; + case PHASE_UPDATE: + dx = pointInWindow.x - mCurrentSynthesizedTouchpadPinch.mCurrentFocus.x; + dy = pointInWindow.y - mCurrentSynthesizedTouchpadPinch.mCurrentFocus.y; + mCurrentSynthesizedTouchpadPinch.mCurrentFocus = pointInWindow; + touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_UPDATE; + break; + case PHASE_END: + touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_END; + break; + + default: + return NS_ERROR_NOT_IMPLEMENTED; + } + + touchpad_event->window = mGdkWindow; + // We only set the fields of GdkEventTouchpadPinch which are + // actually used in OnTouchpadPinchEvent(). + // GdkEventTouchpadPinch has additional fields. + // If OnTouchpadPinchEvent() is changed to use other fields, this function + // will need to change to set them as well. + touchpad_event->time = GDK_CURRENT_TIME; + touchpad_event->scale = aScale; + touchpad_event->x_root = DevicePixelsToGdkCoordRoundDown( + mCurrentSynthesizedTouchpadPinch.mBeginFocus.x + + ScreenCoord(widgetToScreenOffset.x)); + touchpad_event->y_root = DevicePixelsToGdkCoordRoundDown( + mCurrentSynthesizedTouchpadPinch.mBeginFocus.y + + ScreenCoord(widgetToScreenOffset.y)); + + touchpad_event->x = DevicePixelsToGdkCoordRoundDown( + mCurrentSynthesizedTouchpadPinch.mBeginFocus.x); + touchpad_event->y = DevicePixelsToGdkCoordRoundDown( + mCurrentSynthesizedTouchpadPinch.mBeginFocus.y); + + touchpad_event->dx = dx; + touchpad_event->dy = dy; + + touchpad_event->state = aModifierFlags; + + gdk_event_put(&event); + + return NS_OK; +} + +nsresult nsWindow::SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY, + int32_t aModifierFlags, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "touchpadpanevent"); + + if (!mGdkWindow) { + return NS_OK; + } + + // This should/could maybe send GdkEventTouchpadSwipe events, however we don't + // currently consume those (either real user input or testing events). So we + // send gdk scroll events to be more like what we do for real user input. If + // we start consuming GdkEventTouchpadSwipe and get those hooked up to swipe + // to nav, then maybe we should test those too. + + mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase = Some(aEventPhase); + MOZ_ASSERT(mCurrentSynthesizedTouchpadPan.mSavedObserver == 0); + mCurrentSynthesizedTouchpadPan.mSavedObserver = notifier.SaveObserver(); + + // Note that CreateAndPutGdkScrollEvent sets the device source for the created + // event as the "client pointer" (a kind of default device) which will + // probably be of type mouse. We would ideally want to set the device of the + // created event to be a touchpad, but the system might not have a touchpad. + // To get around this we use + // mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase being something to + // indicate that we should treat the source of the event as touchpad in + // OnScrollEvent. + CreateAndPutGdkScrollEvent(aPoint, aDeltaX, aDeltaY); + + return NS_OK; +} + +nsWindow::GtkWindowDecoration nsWindow::GetSystemGtkWindowDecoration() { + static GtkWindowDecoration sGtkWindowDecoration = [] { + // Allow MOZ_GTK_TITLEBAR_DECORATION to override our heuristics + if (const char* decorationOverride = + getenv("MOZ_GTK_TITLEBAR_DECORATION")) { + if (strcmp(decorationOverride, "none") == 0) { + return GTK_DECORATION_NONE; + } + if (strcmp(decorationOverride, "client") == 0) { + return GTK_DECORATION_CLIENT; + } + if (strcmp(decorationOverride, "system") == 0) { + return GTK_DECORATION_SYSTEM; + } + } + + // nsWindow::GetSystemGtkWindowDecoration can be called from various + // threads so we can't use gfxPlatformGtk here. + if (GdkIsWaylandDisplay()) { + return GTK_DECORATION_CLIENT; + } + + // 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 (const char* csdOverride = getenv("GTK_CSD")) { + return *csdOverride == '0' ? GTK_DECORATION_NONE : GTK_DECORATION_CLIENT; + } + + // TODO: Consider switching this to GetDesktopEnvironmentIdentifier(). + const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (!currentDesktop) { + return GTK_DECORATION_NONE; + } + if (strstr(currentDesktop, "i3")) { + return GTK_DECORATION_NONE; + } + + // Tested desktops: pop:GNOME, KDE, Enlightenment, LXDE, openbox, MATE, + // X-Cinnamon, Pantheon, Deepin, GNOME, LXQt, Unity. + return GTK_DECORATION_CLIENT; + }(); + return sGtkWindowDecoration; +} + +bool nsWindow::TitlebarUseShapeMask() { + static int useShapeMask = []() { + // Don't use titlebar shape mask on Wayland + if (!GdkIsX11Display()) { + return false; + } + + // We can't use shape masks on Mutter/X.org as we can't resize Firefox + // window there (Bug 1530252). + if (IsGnomeDesktopEnvironment()) { + return false; + } + + return Preferences::GetBool("widget.titlebar-x11-use-shape-mask", false); + }(); + return useShapeMask; +} + +int32_t nsWindow::RoundsWidgetCoordinatesTo() { return GdkCeiledScaleFactor(); } + +void nsWindow::GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) { + nsCString displayName; + + LOG("nsWindow::GetCompositorWidgetInitData"); + EnsureGdkWindow(); + + *aInitData = mozilla::widget::GtkCompositorWidgetInitData( + GetX11Window(), displayName, GetShapedState(), GdkIsX11Display(), + GetClientSize()); + +#ifdef MOZ_X11 + if (GdkIsX11Display()) { + // Make sure the window XID is propagated to X server, we can fail otherwise + // in GPU process (Bug 1401634). + Display* display = DefaultXDisplay(); + XFlush(display); + displayName = nsCString(XDisplayString(display)); + } +#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 (!GdkIsX11Display()) { + 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 (!GdkIsX11Display()) { + 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 +static void relative_pointer_handle_relative_motion( + void* data, struct zwp_relative_pointer_v1* pointer, uint32_t time_hi, + uint32_t time_lo, wl_fixed_t dx_w, wl_fixed_t dy_w, wl_fixed_t dx_unaccel_w, + wl_fixed_t dy_unaccel_w) { + RefPtr<nsWindow> window(reinterpret_cast<nsWindow*>(data)); + + WidgetMouseEvent event(true, eMouseMove, window, WidgetMouseEvent::eReal); + + event.mRefPoint = window->GetNativePointerLockCenter(); + event.mRefPoint.x += wl_fixed_to_int(dx_w); + event.mRefPoint.y += wl_fixed_to_int(dy_w); + + event.AssignEventTime(window->GetWidgetEventTime(time_lo)); + window->DispatchInputEvent(&event); +} + +static const struct zwp_relative_pointer_v1_listener relative_pointer_listener = + { + relative_pointer_handle_relative_motion, +}; + +void nsWindow::SetNativePointerLockCenter( + const LayoutDeviceIntPoint& aLockCenter) { + mNativePointerLockCenter = aLockCenter; +} + +void nsWindow::LockNativePointer() { + if (!GdkIsWaylandDisplay()) { + return; + } + + auto* waylandDisplay = WaylandDisplayGet(); + + auto* pointerConstraints = waylandDisplay->GetPointerConstraints(); + if (!pointerConstraints) { + return; + } + + auto* relativePointerMgr = waylandDisplay->GetRelativePointerManager(); + if (!relativePointerMgr) { + return; + } + + GdkDisplay* display = gdk_display_get_default(); + + GdkDeviceManager* manager = gdk_display_get_device_manager(display); + MOZ_ASSERT(manager); + + GdkDevice* device = gdk_device_manager_get_client_pointer(manager); + if (!device) { + NS_WARNING("Could not find Wayland pointer to lock"); + return; + } + wl_pointer* pointer = gdk_wayland_device_get_wl_pointer(device); + MOZ_ASSERT(pointer); + + wl_surface* surface = + gdk_wayland_window_get_wl_surface(gtk_widget_get_window(GetGtkWidget())); + if (!surface) { + /* Can be null when the window is hidden. + * Though it's unlikely that a lock request comes in that case, be + * defensive. */ + return; + } + + if (mLockedPointer || mRelativePointer) { + UnlockNativePointer(); + } + + mLockedPointer = zwp_pointer_constraints_v1_lock_pointer( + pointerConstraints, surface, pointer, nullptr, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + if (!mLockedPointer) { + NS_WARNING("Could not lock Wayland pointer"); + return; + } + + mRelativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer( + relativePointerMgr, pointer); + if (!mRelativePointer) { + NS_WARNING("Could not create relative Wayland pointer"); + zwp_locked_pointer_v1_destroy(mLockedPointer); + mLockedPointer = nullptr; + return; + } + + zwp_relative_pointer_v1_add_listener(mRelativePointer, + &relative_pointer_listener, this); +} + +void nsWindow::UnlockNativePointer() { + if (!GdkIsWaylandDisplay()) { + return; + } + if (mRelativePointer) { + zwp_relative_pointer_v1_destroy(mRelativePointer); + mRelativePointer = nullptr; + } + if (mLockedPointer) { + zwp_locked_pointer_v1_destroy(mLockedPointer); + mLockedPointer = nullptr; + } +} +#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; + } + 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() const { + 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() { + 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); + } + } +} + +void nsWindow::LockAspectRatio(bool aShouldLock) { + if (!gUseAspectRatio) { + return; + } + + if (aShouldLock) { + int decWidth = 0, decHeight = 0; + AddCSDDecorationSize(&decWidth, &decHeight); + + float width = + DevicePixelsToGdkCoordRoundDown(mLastSizeRequest.width) + decWidth; + float height = + DevicePixelsToGdkCoordRoundDown(mLastSizeRequest.height) + decHeight; + + mAspectRatio = width / height; + LOG("nsWindow::LockAspectRatio() width %f height %f aspect %f", width, + height, mAspectRatio); + } else { + mAspectRatio = 0.0; + LOG("nsWindow::LockAspectRatio() removed aspect ratio"); + } + + ApplySizeConstraints(); +} + +nsWindow* nsWindow::GetFocusedWindow() { return gFocusWindow; } + +#ifdef MOZ_WAYLAND +void nsWindow::SetEGLNativeWindowSize( + const LayoutDeviceIntSize& aEGLWindowSize) { + if (!mContainer || !GdkIsWaylandDisplay()) { + return; + } + if (moz_container_wayland_egl_window_needs_size_update( + mContainer, aEGLWindowSize.ToUnknownSize(), GdkCeiledScaleFactor())) { + LOG("nsWindow::SetEGLNativeWindowSize() %d x %d", aEGLWindowSize.width, + aEGLWindowSize.height); + moz_container_wayland_egl_window_set_size(mContainer, + aEGLWindowSize.ToUnknownSize()); + moz_container_wayland_set_scale_factor(mContainer); + } +} +#endif + +nsWindow* nsWindow::GetWindow(GdkWindow* window) { + return get_window_for_gdk_window(window); +} + +void nsWindow::ClearRenderingQueue() { + LOG("nsWindow::ClearRenderingQueue()"); + + if (mWidgetListener) { + mWidgetListener->RequestWindowClose(this); + } + DestroyLayerManager(); +} + +// Apply workaround for Mutter compositor bug (mzbz#1777269). +// +// When we open a popup window (tooltip for instance) attached to +// GDK_WINDOW_TYPE_HINT_UTILITY parent popup, Mutter compositor sends bogus +// leave/enter events to the GDK_WINDOW_TYPE_HINT_UTILITY popup. +// That leads to immediate tooltip close. As a workaround ignore these +// bogus events. +// +// We need to check two affected window types: +// +// - toplevel window with at least two child popups where the first one is +// GDK_WINDOW_TYPE_HINT_UTILITY. +// - GDK_WINDOW_TYPE_HINT_UTILITY popup with a child popup +// +// We need to mask two bogus leave/enter sequences: +// 1) Leave (popup) -> Enter (toplevel) +// 2) Leave (toplevel) -> Enter (popup) +// +// TODO: persistent (non-tracked) popups with tooltip/child popups? +// +bool nsWindow::ApplyEnterLeaveMutterWorkaround() { + // Leave (toplevel) case + if (mWindowType == WindowType::TopLevel && mWaylandPopupNext && + mWaylandPopupNext->mWaylandPopupNext && + gtk_window_get_type_hint(GTK_WINDOW(mWaylandPopupNext->GetGtkWidget())) == + GDK_WINDOW_TYPE_HINT_UTILITY) { + LOG("nsWindow::ApplyEnterLeaveMutterWorkaround(): leave toplevel"); + return true; + } + // Leave (popup) case + if (IsWaylandPopup() && mWaylandPopupNext && + gtk_window_get_type_hint(GTK_WINDOW(mShell)) == + GDK_WINDOW_TYPE_HINT_UTILITY) { + LOG("nsWindow::ApplyEnterLeaveMutterWorkaround(): leave popup"); + return true; + } + return false; +} + +void nsWindow::NotifyOcclusionState(mozilla::widget::OcclusionState aState) { + if (mWindowType != WindowType::TopLevel) { + return; + } + + bool isFullyOccluded = aState == mozilla::widget::OcclusionState::OCCLUDED; + if (mIsFullyOccluded == isFullyOccluded) { + return; + } + mIsFullyOccluded = isFullyOccluded; + + LOG("nsWindow::NotifyOcclusionState() mIsFullyOccluded %d", mIsFullyOccluded); + if (mWidgetListener) { + mWidgetListener->OcclusionStateChanged(mIsFullyOccluded); + } +} + +void nsWindow::SetDragSource(GdkDragContext* aSourceDragContext) { + mSourceDragContext = aSourceDragContext; + if (IsPopup() && + (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol())) { + if (auto* menuPopupFrame = GetMenuPopupFrame(GetFrame())) { + menuPopupFrame->SetIsDragSource(!!aSourceDragContext); + } + } +} diff --git a/widget/gtk/nsWindow.h b/widget/gtk/nsWindow.h new file mode 100644 index 0000000000..d80cdde02a --- /dev/null +++ b/widget/gtk/nsWindow.h @@ -0,0 +1,1005 @@ +/* -*- 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 __nsWindow_h__ +#define __nsWindow_h__ + +#include <gdk/gdk.h> +#include <gtk/gtk.h> + +#include "CompositorWidget.h" +#include "MozContainer.h" +#include "VsyncSource.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/widget/WindowSurface.h" +#include "mozilla/widget/WindowSurfaceProvider.h" +#include "nsBaseWidget.h" +#include "nsGkAtoms.h" +#include "nsIDragService.h" +#include "nsRefPtrHashtable.h" +#include "IMContextWrapper.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/LocalAccessible.h" +#endif + +#ifdef MOZ_X11 +# include <gdk/gdkx.h> +# include "X11UndefineNone.h" +#endif +#ifdef MOZ_WAYLAND +# include <gdk/gdkwayland.h> +# include "base/thread.h" +# include "WaylandVsyncSource.h" +# include "nsClipboardWayland.h" +#endif + +#ifdef MOZ_LOGGING + +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" + +extern mozilla::LazyLogModule gWidgetLog; +extern mozilla::LazyLogModule gWidgetDragLog; +extern mozilla::LazyLogModule gWidgetPopupLog; +extern mozilla::LazyLogModule gWidgetVsync; + +# define LOG(str, ...) \ + MOZ_LOG(IsPopup() ? gWidgetPopupLog : gWidgetLog, \ + mozilla::LogLevel::Debug, \ + ("%s: " str, GetDebugTag().get(), ##__VA_ARGS__)) +# define LOGW(...) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define LOGDRAG(...) \ + MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define LOG_POPUP(...) \ + MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define LOG_VSYNC(...) \ + MOZ_LOG(gWidgetVsync, mozilla::LogLevel::Debug, (__VA_ARGS__)) +# define LOG_ENABLED() \ + (MOZ_LOG_TEST(gWidgetPopupLog, mozilla::LogLevel::Debug) || \ + MOZ_LOG_TEST(gWidgetLog, mozilla::LogLevel::Debug)) + +#else + +# define LOG(...) +# define LOGW(...) +# define LOGDRAG(...) +# define LOG_POPUP(...) +# define LOG_ENABLED() false + +#endif /* MOZ_LOGGING */ + +#if defined(MOZ_WAYLAND) && !defined(MOZ_X11) +typedef uintptr_t Window; +#endif + +class gfxPattern; +class nsIFrame; +#if !GTK_CHECK_VERSION(3, 18, 0) +struct _GdkEventTouchpadPinch; +typedef struct _GdkEventTouchpadPinch GdkEventTouchpadPinch; +#endif + +#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 + +namespace mozilla { +enum class NativeKeyBindingsType : uint8_t; + +class TimeStamp; +#ifdef MOZ_X11 +class CurrentX11TimeGetter; +#endif + +namespace widget { +class Screen; +} // namespace widget +} // 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) + + nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override; + + // called when we are destroyed + void OnDestroy() 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]] nsresult Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + InitData* aInitData) override; + void Destroy() override; + nsIWidget* GetParent() override; + float GetDPI() override; + double GetDefaultScaleInternal() override; + mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() override; + mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScaleByScreen() + override; + void SetParent(nsIWidget* aNewParent) override; + void SetModal(bool aModal) override; + bool IsVisible() const override; + bool IsMapped() const override; + void ConstrainPosition(DesktopIntPoint&) override; + void SetSizeConstraints(const SizeConstraints& aConstraints) override; + void LockAspectRatio(bool aShouldLock) override; + void Move(double aX, double aY) override; + void Show(bool aState) override; + void Resize(double aWidth, double aHeight, bool aRepaint) override; + void Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) override; + bool IsEnabled() const override; + + void SetZIndex(int32_t aZIndex) override; + nsSizeMode SizeMode() override { return mSizeMode; } + void SetSizeMode(nsSizeMode aMode) override; + void GetWorkspaceID(nsAString& workspaceID) override; + void MoveToWorkspace(const nsAString& workspaceID) override; + void Enable(bool aState) override; + void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override; + LayoutDeviceIntRect GetScreenBounds() override; + LayoutDeviceIntRect GetClientBounds() override; + LayoutDeviceIntSize GetClientSize() override; + LayoutDeviceIntPoint GetClientOffset() override { return mClientOffset; } + LayoutDeviceIntPoint GetScreenEdgeSlop() override; + + // Recomputes the client offset according to our current window position. + // If aNotify is true, NotifyWindowMoved will be called on client offset + // changes. + // + // NOTE(emilio): It seems that as long any change here update either the size + // or the position of the window, we should be doing fine without notifying, + // but this is done to preserve existing behavior. + void RecomputeClientOffset(bool aNotify); + + void SetCursor(const Cursor&) override; + void Invalidate(const LayoutDeviceIntRect& aRect) override; + void* GetNativeData(uint32_t aDataType) override; + nsresult SetTitle(const nsAString& aTitle) override; + void SetIcon(const nsAString& aIconSpec) override; + void SetWindowClass(const nsAString& xulWinType, const nsAString& xulWinClass, + const nsAString& xulWinName) override; + LayoutDeviceIntPoint WidgetToScreenOffset() override; + void CaptureRollupEvents(bool aDoCapture) override; + [[nodiscard]] nsresult GetAttention(int32_t aCycleCount) override; + bool HasPendingInputEvent() override; + + bool PrepareForFullscreenTransition(nsISupports** aData) override; + void PerformFullscreenTransition(FullscreenTransitionStage aStage, + uint16_t aDuration, nsISupports* aData, + nsIRunnable* aCallback) override; + already_AddRefed<Screen> GetWidgetScreen() override; + nsresult MakeFullScreen(bool aFullScreen) override; + 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(BorderStyle aStyle); + + 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 OnMap(); + void OnUnmap(); + void OnUnrealize(); + 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); + + gint GetInputRegionMarginInGdkCoords(); + + void UpdateTopLevelOpaqueRegion(); + + already_AddRefed<mozilla::gfx::DrawTarget> StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + mozilla::layers::BufferMode* aBufferMode) override; + void EndRemoteDrawingInRegion( + mozilla::gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion) override; + + void SetProgress(unsigned long progressPercent); + + RefPtr<mozilla::VsyncDispatcher> GetVsyncDispatcher() override; + bool SynchronouslyRepaintOnResize() override; + + void OnDPIChanged(void); + void OnCheckResize(void); + void OnCompositedChanged(void); + void OnScaleChanged(); + void DispatchResized(); + + static guint32 sLastButtonPressTime; + + MozContainer* GetMozContainer() { return mContainer; } + GdkWindow* GetGdkWindow() const { return mGdkWindow; }; + GdkWindow* GetToplevelGdkWindow() const; + GtkWidget* GetGtkWidget() const { return mShell; } + nsIFrame* GetFrame() const; + nsWindow* GetEffectiveParent(); + bool IsDestroyed() const { return mIsDestroyed; } + bool IsPopup() const; + bool IsWaylandPopup() const; + bool IsPIPWindow() const { return mIsPIPWindow; }; + bool IsDragPopup() { return mIsDragPopup; }; + + nsAutoCString GetDebugTag() const; + + void DispatchDragEvent(mozilla::EventMessage aMsg, + const LayoutDeviceIntPoint& aRefPoint, guint aTime); + static void UpdateDragStatus(GdkDragContext* aDragContext, + nsIDragService* aDragService); + void SetDragSource(GdkDragContext* aSourceDragContext); + + WidgetEventTime GetWidgetEventTime(guint32 aEventTime); + mozilla::TimeStamp GetEventTimeStamp(guint32 aEventTime); +#ifdef MOZ_X11 + mozilla::CurrentX11TimeGetter* GetCurrentTimeGetter(); +#endif + + void SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) override; + InputContext GetInputContext() override; + TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override; + MOZ_CAN_RUN_SCRIPT bool GetEditCommands( + mozilla::NativeKeyBindingsType aType, + const mozilla::WidgetKeyboardEvent& aEvent, + nsTArray<mozilla::CommandInt>& aCommands) override; + + // These methods are for toplevel windows only. + void ResizeTransparencyBitmap(); + void ApplyTransparencyBitmap(); + void ClearTransparencyBitmap(); + + void SetTransparencyMode(TransparencyMode aMode) override; + TransparencyMode GetTransparencyMode() override; + void SetInputRegion(const InputRegion&) override; + nsresult UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect, + uint8_t* aAlphas, + int32_t aStride); + void ReparentNativeWidget(nsIWidget* aNewParent) override; + + void UpdateTitlebarTransparencyBitmap(); + + nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + NativeMouseMessage aNativeMessage, + mozilla::MouseButton aButton, + nsIWidget::Modifiers aModifierFlags, + nsIObserver* aObserver) override; + + nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint, + nsIObserver* aObserver) override { + return SynthesizeNativeMouseEvent( + aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed, + nsIWidget::Modifiers::NO_MODIFIERS, aObserver); + } + + nsresult SynthesizeNativeMouseScrollEvent( + LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX, + double aDeltaY, double aDeltaZ, uint32_t aModifierFlags, + uint32_t aAdditionalFlags, nsIObserver* aObserver) override; + + nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId, + TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, + double aPointerPressure, + uint32_t aPointerOrientation, + nsIObserver* aObserver) override; + + nsresult SynthesizeNativeTouchPadPinch(TouchpadGesturePhase aEventPhase, + float aScale, + LayoutDeviceIntPoint aPoint, + int32_t aModifierFlags) override; + + nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY, + int32_t aModifierFlags, + nsIObserver* aObserver) override; + + void GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) override; + + nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override; + void SetDrawsInTitlebar(bool aState) override; + mozilla::LayoutDeviceIntCoord GetTitlebarRadius(); + LayoutDeviceIntRect GetTitlebarRect(); + void UpdateWindowDraggingRegion( + const LayoutDeviceIntRegion& aRegion) override; + + // HiDPI scale conversion + gint GdkCeiledScaleFactor(); + bool UseFractionalScale() const; + double FractionalScaleFactor(); + + // To GDK + gint DevicePixelsToGdkCoordRoundUp(int); + gint DevicePixelsToGdkCoordRoundDown(int); + GdkPoint DevicePixelsToGdkPointRoundDown(const LayoutDeviceIntPoint&); + GdkRectangle DevicePixelsToGdkSizeRoundUp(const LayoutDeviceIntSize&); + GdkRectangle DevicePixelsToGdkRectRoundOut(const LayoutDeviceIntRect&); + + // From GDK + int GdkCoordToDevicePixels(gint); + LayoutDeviceIntPoint GdkPointToDevicePixels(const GdkPoint&); + LayoutDeviceIntPoint GdkEventCoordsToDevicePixels(gdouble aX, gdouble aY); + LayoutDeviceIntRect GdkRectToDevicePixels(const GdkRectangle&); + + bool WidgetTypeSupportsAcceleration() override; + + nsresult SetSystemFont(const nsCString& aFontName) override; + nsresult GetSystemFont(nsCString& aFontName) override; + + typedef enum { + GTK_DECORATION_SYSTEM, // CSD including shadows + GTK_DECORATION_CLIENT, // CSD without shadows + GTK_DECORATION_NONE, // WM does not support CSD at all + } GtkWindowDecoration; + /** + * Get the support of Client Side Decoration by checking the desktop + * environment. + */ + static GtkWindowDecoration GetSystemGtkWindowDecoration(); + + static bool GetTopLevelWindowActiveState(nsIFrame* aFrame); + static bool TitlebarUseShapeMask(); + bool IsRemoteContent() { return HasRemoteContent(); } + void NativeMoveResizeWaylandPopupCallback(const GdkRectangle* aFinalSize, + bool aFlippedX, bool aFlippedY); + static bool IsToplevelWindowTransparent(); + + static nsWindow* GetFocusedWindow(); + +#ifdef MOZ_WAYLAND + // Use xdg-activation protocol to transfer focus from gFocusWindow to aWindow. + static void TransferFocusToWaylandWindow(nsWindow* aWindow); + void FocusWaylandWindow(const char* aTokenID); + + bool GetCSDDecorationOffset(int* aDx, int* aDy); + void SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize); + void WaylandDragWorkaround(GdkEventButton* aEvent); + + void CreateCompositorVsyncDispatcher() override; + LayoutDeviceIntPoint GetNativePointerLockCenter() { + return mNativePointerLockCenter; + } + void SetNativePointerLockCenter( + const LayoutDeviceIntPoint& aLockCenter) override; + void LockNativePointer() override; + void UnlockNativePointer() override; + LayoutDeviceIntSize GetMoveToRectPopupSize() const override { + return mMoveToRectPopupSize; + }; +#endif + + typedef enum { + // WebRender compositor is enabled + COMPOSITOR_ENABLED, + // WebRender compositor is paused as we're repainting whole window and + // we're waiting for content process to update page content. + COMPOSITOR_PAUSED_FLICKERING + } WindowCompositorState; + + // Pause compositor to avoid rendering artifacts from content process. + void ResumeCompositorImpl(); + void ResumeCompositorFlickering(); + void ResumeCompositorFromCompositorThread(); + void PauseCompositorFlickering(); + bool IsWaitingForCompositorResume(); + + // Force hide this window, remove compositor etc. to avoid + // rendering queue blocking (see Bug 1782948). + void ClearRenderingQueue(); + + bool ApplyEnterLeaveMutterWorkaround(); + + void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override; + + static nsWindow* GetWindow(GdkWindow* window); + + protected: + virtual ~nsWindow(); + + // event handling code + void DispatchActivateEvent(void); + void DispatchDeactivateEvent(void); + void MaybeDispatchResized(); + void DispatchPanGesture(mozilla::PanGestureInput& aPanInput); + + void RegisterTouchWindow() override; + + nsCOMPtr<nsIWidget> mParent; + PopupType mPopupHint{}; + int mWindowScaleFactor = 1; + + void UpdateAlpha(mozilla::gfx::SourceSurface* aSourceSurface, + nsIntRect aBoundsRect); + + void NativeMoveResize(bool aMoved, bool aResized); + + void NativeShow(bool aAction); + void SetHasMappedToplevel(bool aState); + LayoutDeviceIntSize GetSafeWindowSize(LayoutDeviceIntSize aSize); + + void DispatchContextMenuEventFromMouseEvent( + uint16_t domButton, GdkEventButton* aEvent, + const mozilla::LayoutDeviceIntPoint& aRefPoint); + + void TryToShowNativeWindowMenu(GdkEventButton* aEvent); + + void WaylandStartVsync(); + void WaylandStopVsync(); + void DestroyChildWindows(); + GtkWidget* GetToplevelWidget() const; + nsWindow* GetContainerWindow() const; + Window GetX11Window(); + bool GetShapedState(); + void EnsureGdkWindow(); + void SetUrgencyHint(GtkWidget* top_window, bool state); + void SetDefaultIcon(void); + void SetWindowDecoration(BorderStyle aStyle); + void InitButtonEvent(mozilla::WidgetMouseEvent& aEvent, + GdkEventButton* aGdkEvent, + const mozilla::LayoutDeviceIntPoint& aRefPoint); + bool CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel, + bool aAlwaysRollup); + void RollupAllMenus() { CheckForRollup(0, 0, false, true); } + void CheckForRollupDuringGrab() { RollupAllMenus(); } + + bool GetDragInfo(mozilla::WidgetMouseEvent* aMouseEvent, GdkWindow** aWindow, + gint* aButton, gint* aRootX, gint* aRootY); + nsIWidgetListener* GetListener(); + + nsWindow* GetTransientForWindowIfPopup(); + bool IsHandlingTouchSequence(GdkEventSequence* aSequence); + + void ResizeInt(const mozilla::Maybe<LayoutDeviceIntPoint>& aMove, + LayoutDeviceIntSize aSize); + void NativeMoveResizeWaylandPopup(bool aMove, bool aResize); + + // Returns a window edge if the given point (in device pixels) is within a + // resizer region of the window. + // Only used when drawing decorations client side. + mozilla::Maybe<GdkWindowEdge> CheckResizerEdge(const LayoutDeviceIntPoint&); + + GtkTextDirection GetTextDirection(); + + bool DrawsToCSDTitlebar() const; + void AddCSDDecorationSize(int* aWidth, int* aHeight); + + void CreateAndPutGdkScrollEvent(mozilla::LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY); + + nsCString mGtkWindowAppClass; + nsCString mGtkWindowAppName; + nsCString mGtkWindowRoleName; + void RefreshWindowClass(); + + GtkWidget* mShell = nullptr; + MozContainer* mContainer = nullptr; + GdkWindow* mGdkWindow = nullptr; + PlatformCompositorWidgetDelegate* mCompositorWidgetDelegate = nullptr; + mozilla::Atomic<WindowCompositorState, mozilla::Relaxed> mCompositorState{ + COMPOSITOR_ENABLED}; + // This is used in COMPOSITOR_PAUSED_FLICKERING mode only to resume compositor + // in some reasonable time when page content is not updated. + guint mCompositorPauseTimeoutID = 0; + + // The actual size mode that's in effect. + nsSizeMode mSizeMode = nsSizeMode_Normal; + // The last size mode we've requested. This might not match mSizeMode if + // there's a request to change the size mode in progress. + nsSizeMode mLastSizeModeRequest = nsSizeMode_Normal; + nsSizeMode mLastSizeModeBeforeFullscreen = nsSizeMode_Normal; + + float mAspectRatio = 0.0f; + float mAspectRatioSaved = 0.0f; + + // The size requested, which might not be reflected in mBounds. Used in + // WaylandPopupSetDirectPosition() to remember intended size for popup + // positioning, in LockAspect() to remember the intended aspect ratio, and + // to remember a size requested while waiting for moved-to-rect when + // OnSizeAllocate() might change mBounds.Size(). + LayoutDeviceIntSize mLastSizeRequest; + LayoutDeviceIntPoint mClientOffset; + // Indicates a new size that still needs to be dispatched. + LayoutDeviceIntSize mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1); + + // This field omits duplicate scroll events caused by GNOME bug 726878. + guint32 mLastScrollEventTime = GDK_CURRENT_TIME; + mozilla::ScreenCoord mLastPinchEventSpan; + + struct TouchpadPinchGestureState { + // Focus point of the PHASE_BEGIN event + ScreenPoint mBeginFocus; + + // Focus point of the most recent PHASE_UPDATE event + ScreenPoint mCurrentFocus; + }; + + // Used for handling touchpad pinch gestures + ScreenPoint mCurrentTouchpadFocus; + + // Used for synthesizing touchpad pinch gestures + TouchpadPinchGestureState mCurrentSynthesizedTouchpadPinch; + + // Used for synthesizing touchpad pan gestures + struct TouchpadPanGestureState { + mozilla::Maybe<TouchpadGesturePhase> mTouchpadGesturePhase; + uint64_t mSavedObserver = 0; + }; + + // Used for synthesizing touchpad pan gestures + TouchpadPanGestureState mCurrentSynthesizedTouchpadPan; + + // for touch event handling + nsRefPtrHashtable<nsPtrHashKey<GdkEventSequence>, mozilla::dom::Touch> + mTouches; + + // Upper bound on pending ConfigureNotify events to be dispatched to the + // window. See bug 1225044. + unsigned int mPendingConfigures = 0; + + // Window titlebar rendering mode, GTK_DECORATION_NONE if it's disabled + // for this window. + GtkWindowDecoration mGtkWindowDecoration = GTK_DECORATION_NONE; + + // Draggable titlebar region maintained by UpdateWindowDraggingRegion + LayoutDeviceIntRegion mDraggableRegion; + + // The cursor cache + static GdkCursor* gsGtkCursorCache[eCursorCount]; + + // If true, draw our own window titlebar. + // + // Needs to be atomic because GetTitlebarRect() gets called from non-main + // threads. + // + // FIXME(emilio): GetTitlebarRect() reads other things that TSAN doesn't + // catch because mDrawInTitlebar is false on automation ~always. We should + // probably make GetTitlebarRect() simpler / properly thread-safe. + mozilla::Atomic<bool, mozilla::Relaxed> mDrawInTitlebar{false}; + + // Has this widget been destroyed yet? + bool mIsDestroyed; + // mIsShown tracks requested visible status from browser perspective, i.e. + // if the window should be visible or now. + bool mIsShown : 1; + // mNeedsShow is set when browser requested to show this window but we failed + // to do so for some reason (wrong window size for instance). + // In such case we set mIsShown = true and mNeedsShow = true to indicate + // that the window is not actually visible but we report to browser that + // it is visible (mIsShown == true). + bool mNeedsShow : 1; + // This track real window visibility from OS perspective. + // It's set by OnMap/OnUnrealize which is based on Gtk events. + bool mIsMapped : 1; + // is this widget enabled? + bool mEnabled : 1; + // has the native window for this been created yet? + bool mCreated : 1; + // whether we handle touch event + bool mHandleTouchEvent : 1; + // true if this is a drag and drop feedback popup + bool mIsDragPopup : 1; + bool mWindowScaleFactorChanged : 1; + bool mCompositedScreen : 1; + bool mIsAccelerated : 1; + bool mWindowShouldStartDragging : 1; + bool mHasMappedToplevel : 1; + bool mRetryPointerGrab : 1; + bool mPanInProgress : 1; + // Use dedicated GdkWindow for mContainer + bool mDrawToContainer : 1; + // Draw titlebar with :backdrop css state (inactive/unfocused). + bool mTitlebarBackdropState : 1; + // It's PictureInPicture window. + bool mIsPIPWindow : 1; + // It's undecorated popup utility window, without resizers/titlebar, + // movable by mouse. Used on Wayland for popups without + // parent (for instance WebRTC sharing indicator, notifications). + bool mIsWaylandPanelWindow : 1; + // It's child window, i.e. window which is nested in parent window. + // This is obsoleted and should not be used. + // We use GdkWindow hierarchy for such windows. + bool mIsChildWindow : 1; + bool mAlwaysOnTop : 1; + bool mNoAutoHide : 1; + bool mIsTransparent : 1; + // We can expect at least one size-allocate event after early resizes. + bool mHasReceivedSizeAllocate : 1; + bool mWidgetCursorLocked : 1; + + /* Gkt creates popup in two incarnations - wl_subsurface and xdg_popup. + * Kind of popup is choosen before GdkWindow is mapped so we can change + * it only when GdkWindow is hidden. + * + * Relevant Gtk code is at gdkwindow-wayland.c + * in should_map_as_popup() and should_map_as_subsurface() + * + * wl_subsurface: + * - can't be positioned by move-to-rect + * - can stand outside popup widget hierarchy (has toplevel as parent) + * - don't have child popup widgets + * + * xdg_popup: + * - can be positioned by move-to-rect + * - aligned in popup widget hierarchy, first one is attached to toplevel + * - has child (popup) widgets + * + * Thus we need to map Firefox popup type to desired Gtk one: + * + * wl_subsurface: + * - pernament panels + * + * xdg_popup: + * - menus + * - autohide popups (hamburger menu) + * - extension popups + * - tooltips + * + * We set mPopupTrackInHierarchy = false for pernament panels which + * are always mapped to toplevel and painted as wl_surfaces. + */ + bool mPopupTrackInHierarchy : 1; + bool mPopupTrackInHierarchyConfigured : 1; + + /* On X11 Gtk tends to ignore window position requests when gtk_window + * is hidden. Save the position requests at mPopupPosition and apply + * when the widget is shown. + */ + bool mHiddenPopupPositioned : 1; + + // The transparency bitmap is used instead of ARGB visual for toplevel + // window to draw titlebar. + bool mTransparencyBitmapForTitlebar : 1; + + // True when we're on compositing window manager and this + // window is using visual with alpha channel. + bool mHasAlphaVisual : 1; + + // When popup is anchored, mPopupPosition is relative to its parent popup. + bool mPopupAnchored : 1; + + // When popup is context menu. + bool mPopupContextMenu : 1; + + // Indicates that this popup matches layout setup so we can use parent popup + // coordinates reliably. + bool mPopupMatchesLayout : 1; + + /* Indicates that popup setup was changed and + * we need to recalculate popup coordinates. + */ + bool mPopupChanged : 1; + + // Popup is hidden only as a part of hierarchy tree update. + bool mPopupTemporaryHidden : 1; + + // Popup is going to be closed and removed. + bool mPopupClosed : 1; + + // Popup is positioned by gdk_window_move_to_rect() + bool mPopupUseMoveToRect : 1; + + /* mWaitingForMoveToRectCallback is set when move-to-rect is called + * and we're waiting for move-to-rect callback. + * + * If another position/resize request comes between move-to-rect call and + * move-to-rect callback we set mMovedAfterMoveToRect/mResizedAfterMoveToRect. + */ + bool mWaitingForMoveToRectCallback : 1; + bool mMovedAfterMoveToRect : 1; + bool mResizedAfterMoveToRect : 1; + + // Params used for popup placemend by GdkWindowMoveToRect. + // When popup is only resized and not positioned, + // we need to reuse last GdkWindowMoveToRect params to avoid + // popup movement. + struct WaylandPopupMoveToRectParams { + LayoutDeviceIntRect mAnchorRect = {0, 0, 0, 0}; + GdkGravity mAnchorRectType = GDK_GRAVITY_NORTH_WEST; + GdkGravity mPopupAnchorType = GDK_GRAVITY_NORTH_WEST; + GdkAnchorHints mHints = GDK_ANCHOR_SLIDE; + GdkPoint mOffset = {0, 0}; + bool mAnchorSet = false; + }; + + WaylandPopupMoveToRectParams mPopupMoveToRectParams; + + // Whether we've configured default clear color already. + bool mConfiguredClearColor : 1; + // Whether we've received a non-blank paint in which case we can reset the + // clear color to transparent. + bool mGotNonBlankPaint : 1; + + // Whether we need to retry capturing the mouse because we' re not mapped yet. + bool mNeedsToRetryCapturingMouse : 1; + + // 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 = nullptr; + int32_t mTransparencyBitmapWidth = 0; + int32_t mTransparencyBitmapHeight = 0; + + // all of our DND stuff + void InitDragEvent(mozilla::WidgetDragEvent& aEvent); + + float mLastMotionPressure = 0.0f; + + InputRegion mInputRegion; + + static bool DragInProgress(void); + + void DispatchMissedButtonReleases(GdkEventCrossing* aGdkEvent); + + // When window widget gets mapped/unmapped we need to configure + // underlying GdkWindow properly. Otherwise we'll end up with + // rendering to released window. + void ConfigureGdkWindow(); + void ReleaseGdkWindow(); + void ConfigureCompositor(); + + bool IsAlwaysUndecoratedWindow() const; + + // nsBaseWidget + WindowRenderer* GetWindowRenderer() override; + void DidGetNonBlankPaint() override; + + void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override; + + int32_t RoundsWidgetCoordinatesTo() override; + + void UpdateMozWindowActive(); + + void ForceTitlebarRedraw(); + bool DoDrawTilebarCorners(); + bool IsChromeWindowTitlebar(); + + void SetPopupWindowDecoration(bool aShowOnTaskbar); + + void ApplySizeConstraints(); + + // Wayland Popup section + GdkPoint WaylandGetParentPosition(); + bool WaylandPopupConfigure(); + bool WaylandPopupIsAnchored(); + bool WaylandPopupIsMenu(); + bool WaylandPopupIsContextMenu(); + bool WaylandPopupIsPermanent(); + // First popup means it's attached directly to toplevel window + bool WaylandPopupIsFirst(); + bool IsWidgetOverflowWindow(); + void RemovePopupFromHierarchyList(); + void ShowWaylandPopupWindow(); + void HideWaylandPopupWindow(bool aTemporaryHidden, bool aRemoveFromPopupList); + void ShowWaylandToplevelWindow(); + void HideWaylandToplevelWindow(); + void WaylandPopupHideTooltips(); + void WaylandPopupCloseOrphanedPopups(); + void AppendPopupToHierarchyList(nsWindow* aToplevelWindow); + void WaylandPopupHierarchyHideTemporary(); + void WaylandPopupHierarchyShowTemporaryHidden(); + void WaylandPopupHierarchyCalculatePositions(); + bool IsInPopupHierarchy(); + void AddWindowToPopupHierarchy(); + void UpdateWaylandPopupHierarchy(); + void WaylandPopupHierarchyHideByLayout( + nsTArray<nsIWidget*>* aLayoutWidgetHierarchy); + void WaylandPopupHierarchyValidateByLayout( + nsTArray<nsIWidget*>* aLayoutWidgetHierarchy); + void CloseAllPopupsBeforeRemotePopup(); + void WaylandPopupHideClosedPopups(); + void WaylandPopupPrepareForMove(); + void WaylandPopupMoveImpl(); + void WaylandPopupMovePlain(int aX, int aY); + bool WaylandPopupRemoveNegativePosition(int* aX = nullptr, int* aY = nullptr); + bool WaylandPopupCheckAndGetAnchor(GdkRectangle* aPopupAnchor, + GdkPoint* aOffset); + bool WaylandPopupAnchorAdjustForParentPopup(GdkRectangle* aPopupAnchor, + GdkPoint* aOffset); + nsWindow* WaylandPopupGetTopmostWindow(); + bool IsPopupInLayoutPopupChain(nsTArray<nsIWidget*>* aLayoutWidgetHierarchy, + bool aMustMatchParent); + void WaylandPopupMarkAsClosed(); + void WaylandPopupRemoveClosedPopups(); + void WaylandPopupSetDirectPosition(); + bool WaylandPopupFitsToplevelWindow(bool aMove); + const WaylandPopupMoveToRectParams WaylandPopupGetPositionFromLayout(); + void WaylandPopupPropagateChangesToLayout(bool aMove, bool aResize); + nsWindow* WaylandPopupFindLast(nsWindow* aPopup); + GtkWindow* GetCurrentTopmostWindow() const; + nsAutoCString GetFrameTag() const; + nsCString GetPopupTypeName(); + bool IsPopupDirectionRTL(); + +#ifdef MOZ_LOGGING + void LogPopupHierarchy(); + void LogPopupAnchorHints(int aHints); + void LogPopupGravity(GdkGravity aGravity); +#endif + + // mPopupPosition is the original popup position/size from layout, set by + // nsWindow::Move() or nsWindow::Resize(). + // Popup position is relative to main (toplevel) window. + GdkPoint mPopupPosition{}; + + // mRelativePopupPosition is popup position calculated against + // recent popup parent window. + GdkPoint mRelativePopupPosition{}; + + // Toplevel window (first element) of linked list of Wayland popups. It's null + // if we're the toplevel. + RefPtr<nsWindow> mWaylandToplevel; + + // Next/Previous popups in Wayland popup hierarchy. + RefPtr<nsWindow> mWaylandPopupNext; + RefPtr<nsWindow> mWaylandPopupPrev; + + // When popup is resized by Gtk by move-to-rect callback, + // we store final popup size here. Then we use mMoveToRectPopupSize size + // in following popup operations unless mLayoutPopupSizeCleared is set. + LayoutDeviceIntSize mMoveToRectPopupSize; + + /** + * |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; + +#ifdef MOZ_X11 + mozilla::UniquePtr<mozilla::CurrentX11TimeGetter> mCurrentTimeGetter; +#endif + static GtkWindowDecoration sGtkWindowDecoration; + + static bool sTransparentMainWindow; + +#ifdef ACCESSIBILITY + RefPtr<mozilla::a11y::LocalAccessible> 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 + + void SetUserTimeAndStartupTokenForActivatedWindow(); + +#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); + bool ConfigureX11GLVisual(); +#endif +#ifdef MOZ_WAYLAND + RefPtr<mozilla::WaylandVsyncSource> mWaylandVsyncSource; + RefPtr<mozilla::VsyncDispatcher> mWaylandVsyncDispatcher; + LayoutDeviceIntPoint mNativePointerLockCenter; + zwp_locked_pointer_v1* mLockedPointer = nullptr; + zwp_relative_pointer_v1* mRelativePointer = nullptr; +#endif + // An activation token from our environment (see handling of the + // XDG_ACTIVATION_TOKEN/DESKTOP_STARTUP_ID) env vars. + nsCString mWindowActivationTokenFromEnv; + mozilla::widget::WindowSurfaceProvider mSurfaceProvider; + GdkDragContext* mSourceDragContext = nullptr; +#if MOZ_LOGGING + LayoutDeviceIntRect mLastLoggedBoundSize; + int mLastLoggedScale = -1; +#endif +}; + +#endif /* __nsWindow_h__ */ diff --git a/widget/gtk/vaapitest/moz.build b/widget/gtk/vaapitest/moz.build new file mode 100644 index 0000000000..8667a6637d --- /dev/null +++ b/widget/gtk/vaapitest/moz.build @@ -0,0 +1,25 @@ +# -*- 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 = ("Toolkit", "Startup and Profile System") + +Program("vaapitest") +SOURCES += [ + "vaapitest.cpp", +] +CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] +CXXFLAGS += CONFIG["MOZ_PANGO_CFLAGS"] +OS_LIBS += CONFIG["MOZ_X11_LIBS"] +OS_LIBS += CONFIG["MOZ_GTK3_LIBS"] + +if CONFIG["MOZ_WAYLAND"]: + USE_LIBS += [ + "mozva", + ] + LOCAL_INCLUDES += [ + "/media/mozva", + ] diff --git a/widget/gtk/vaapitest/vaapitest.cpp b/widget/gtk/vaapitest/vaapitest.cpp new file mode 100644 index 0000000000..fdaec3a479 --- /dev/null +++ b/widget/gtk/vaapitest/vaapitest.cpp @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=8 et : + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 <cstdio> +#include <cstdlib> +#include <dlfcn.h> +#include <fcntl.h> +#include <unistd.h> +#include <string.h> +#include <unistd.h> +#include <getopt.h> +#include <stdint.h> + +#if defined(MOZ_ASAN) || defined(FUZZING) +# include <signal.h> +#endif + +#include "mozilla/ScopeExit.h" + +#ifdef __SUNPRO_CC +# include <stdio.h> +#endif + +#include "mozilla/widget/mozwayland.h" +#include "prlink.h" +#include "va/va.h" + +#include "mozilla/GfxInfoUtils.h" + +// Print VA-API test results to stdout and logging to stderr +#define OUTPUT_PIPE 1 + +// bits to use decoding vaapitest() return values. +constexpr int CODEC_HW_H264 = 1 << 4; +constexpr int CODEC_HW_VP8 = 1 << 5; +constexpr int CODEC_HW_VP9 = 1 << 6; +constexpr int CODEC_HW_AV1 = 1 << 7; + +// childgltest is declared inside extern "C" so that the name is not mangled. +// The name is used in build/valgrind/x86_64-pc-linux-gnu.sup to suppress +// memory leak errors because we run it inside a short lived fork and we don't +// care about leaking memory +extern "C" { + +static constexpr struct { + VAProfile mVAProfile; + const char* mName; +} kVAAPiProfileName[] = { +#define MAP(v) \ + { VAProfile##v, #v } + MAP(H264ConstrainedBaseline), + MAP(H264Main), + MAP(H264High), + MAP(VP8Version0_3), + MAP(VP9Profile0), + MAP(VP9Profile2), + MAP(AV1Profile0), + MAP(AV1Profile1), +#undef MAP +}; + +static const char* VAProfileName(VAProfile aVAProfile) { + for (const auto& profile : kVAAPiProfileName) { + if (profile.mVAProfile == aVAProfile) { + return profile.mName; + } + } + return nullptr; +} + +static void vaapitest(const char* aRenderDevicePath) { + int renderDeviceFD = -1; + VAProfile* profiles = nullptr; + VAEntrypoint* entryPoints = nullptr; + VADisplay display = nullptr; + void* libDrm = nullptr; + + log("vaapitest start, device %s\n", aRenderDevicePath); + + auto autoRelease = mozilla::MakeScopeExit([&] { + free(profiles); + free(entryPoints); + if (display) { + vaTerminate(display); + } + if (libDrm) { + dlclose(libDrm); + } + if (renderDeviceFD > -1) { + close(renderDeviceFD); + } + }); + + renderDeviceFD = open(aRenderDevicePath, O_RDWR); + if (renderDeviceFD == -1) { + record_error("VA-API test failed: failed to open renderDeviceFD."); + return; + } + + libDrm = dlopen("libva-drm.so.2", RTLD_LAZY); + if (!libDrm) { + record_error("VA-API test failed: libva-drm.so.2 is missing."); + return; + } + + static auto sVaGetDisplayDRM = + (void* (*)(int fd))dlsym(libDrm, "vaGetDisplayDRM"); + if (!sVaGetDisplayDRM) { + record_error("VA-API test failed: sVaGetDisplayDRM is missing."); + return; + } + + display = sVaGetDisplayDRM(renderDeviceFD); + if (!display) { + record_error("VA-API test failed: sVaGetDisplayDRM failed."); + return; + } + + int major, minor; + VAStatus status = vaInitialize(display, &major, &minor); + if (status != VA_STATUS_SUCCESS) { + log("vaInitialize failed %d\n", status); + record_error("VA-API test failed: failed to initialise VAAPI connection."); + return; + } else { + log("vaInitialize finished\n"); + } + + int maxProfiles = vaMaxNumProfiles(display); + int maxEntryPoints = vaMaxNumEntrypoints(display); + if (maxProfiles <= 0 || maxEntryPoints <= 0) { + record_error("VA-API test failed: wrong VAAPI profiles/entry point nums."); + return; + } + + profiles = (VAProfile*)malloc(sizeof(VAProfile) * maxProfiles); + int numProfiles = 0; + status = vaQueryConfigProfiles(display, profiles, &numProfiles); + if (status != VA_STATUS_SUCCESS) { + record_error("VA-API test failed: vaQueryConfigProfiles() failed."); + return; + } + numProfiles = MIN(numProfiles, maxProfiles); + + entryPoints = (VAEntrypoint*)malloc(sizeof(VAEntrypoint) * maxEntryPoints); + int codecs = 0; + bool foundProfile = false; + for (int p = 0; p < numProfiles; p++) { + VAProfile profile = profiles[p]; + + // Check only supported profiles + if (!VAProfileName(profile)) { + continue; + } + + int numEntryPoints = 0; + status = vaQueryConfigEntrypoints(display, profile, entryPoints, + &numEntryPoints); + if (status != VA_STATUS_SUCCESS) { + continue; + } + numEntryPoints = MIN(numEntryPoints, maxEntryPoints); + for (int entry = 0; entry < numEntryPoints; entry++) { + if (entryPoints[entry] != VAEntrypointVLD) { + continue; + } + VAConfigID config = VA_INVALID_ID; + status = vaCreateConfig(display, profile, entryPoints[entry], nullptr, 0, + &config); + if (status == VA_STATUS_SUCCESS) { + const char* profstr = VAProfileName(profile); + log("Profile: %s\n", profstr); + // VAProfileName returns null on failure, making the below calls safe + if (!strncmp(profstr, "H264", 4)) { + codecs |= CODEC_HW_H264; + } else if (!strncmp(profstr, "VP8", 3)) { + codecs |= CODEC_HW_VP8; + } else if (!strncmp(profstr, "VP9", 3)) { + codecs |= CODEC_HW_VP9; + } else if (!strncmp(profstr, "AV1", 3)) { + codecs |= CODEC_HW_AV1; + } else { + record_warning("VA-API test unknown profile.\n"); + } + vaDestroyConfig(display, config); + foundProfile = true; + } + } + } + if (foundProfile) { + record_value("VAAPI_SUPPORTED\nTRUE\n"); + record_value("VAAPI_HWCODECS\n%d\n", codecs); + } else { + record_value("VAAPI_SUPPORTED\nFALSE\n"); + } + log("vaapitest finished\n"); +} + +} // extern "C" + +static void PrintUsage() { + printf( + "Firefox VA-API probe utility\n" + "\n" + "usage: vaapitest [options]\n" + "\n" + "Options:\n" + "\n" + " -h --help show this message\n" + " -d --drm drm_device probe VA-API on drm_device (may be " + "/dev/dri/renderD128)\n" + "\n"); +} + +int main(int argc, char** argv) { + struct option longOptions[] = {{"help", no_argument, NULL, 'h'}, + {"drm", required_argument, NULL, 'd'}, + {NULL, 0, NULL, 0}}; + const char* shortOptions = "hd:"; + int c; + const char* drmDevice = nullptr; + while ((c = getopt_long(argc, argv, shortOptions, longOptions, NULL)) != -1) { + switch (c) { + case 'd': + drmDevice = optarg; + break; + case 'h': + default: + break; + } + } + if (drmDevice) { +#if defined(MOZ_ASAN) || defined(FUZZING) + // If handle_segv=1 (default), then glxtest crash will print a sanitizer + // report which can confuse the harness in fuzzing automation. + signal(SIGSEGV, SIG_DFL); +#endif + const char* env = getenv("MOZ_GFX_DEBUG"); + enable_logging = env && *env == '1'; + output_pipe = OUTPUT_PIPE; + if (!enable_logging) { + close_logging(); + } + vaapitest(drmDevice); + record_flush(); + return EXIT_SUCCESS; + } + PrintUsage(); + return 0; +} 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/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..695fa46d75 --- /dev/null +++ b/widget/gtk/wayland/moz.build @@ -0,0 +1,37 @@ +# -*- 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 += [ + "idle-inhibit-unstable-v1-protocol.c", + "linux-dmabuf-unstable-v1-protocol.c", + "pointer-constraints-unstable-v1-protocol.c", + "relative-pointer-unstable-v1-protocol.c", + "viewporter-protocol.c", + "xdg-activation-v1-protocol.c", + "xdg-output-unstable-v1-protocol.c", +] + +EXPORTS.mozilla.widget += [ + "gbm.h", + "idle-inhibit-unstable-v1-client-protocol.h", + "linux-dmabuf-unstable-v1-client-protocol.h", + "pointer-constraints-unstable-v1-client-protocol.h", + "relative-pointer-unstable-v1-client-protocol.h", + "va_drmcommon.h", + "viewporter-client-protocol.h", + "xdg-activation-v1-client-protocol.h", + "xdg-output-unstable-v1-client-protocol.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] +CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] diff --git a/widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h b/widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h new file mode 100644 index 0000000000..0f38dee226 --- /dev/null +++ b/widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h @@ -0,0 +1,650 @@ +/* Generated by wayland-scanner 1.18.0 */ + +#ifndef POINTER_CONSTRAINTS_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define POINTER_CONSTRAINTS_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_pointer_constraints_unstable_v1 The + * pointer_constraints_unstable_v1 protocol protocol for constraining pointer + * motions + * + * @section page_desc_pointer_constraints_unstable_v1 Description + * + * This protocol specifies a set of interfaces used for adding constraints to + * the motion of a pointer. Possible constraints include confining pointer + * motions to a given region, or locking it to its current position. + * + * In order to constrain the pointer, a client must first bind the global + * interface "wp_pointer_constraints" which, if a compositor supports pointer + * constraints, is exposed by the registry. Using the bound global object, the + * client uses the request that corresponds to the type of constraint it wants + * to make. See wp_pointer_constraints for more details. + * + * 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_pointer_constraints_unstable_v1 Interfaces + * - @subpage page_iface_zwp_pointer_constraints_v1 - constrain the movement of + * a pointer + * - @subpage page_iface_zwp_locked_pointer_v1 - receive relative pointer motion + * events + * - @subpage page_iface_zwp_confined_pointer_v1 - confined pointer object + * @section page_copyright_pointer_constraints_unstable_v1 Copyright + * <pre> + * + * Copyright © 2014 Jonas Ådahl + * Copyright © 2015 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_pointer; +struct wl_region; +struct wl_surface; +struct zwp_confined_pointer_v1; +struct zwp_locked_pointer_v1; +struct zwp_pointer_constraints_v1; + +/** + * @page page_iface_zwp_pointer_constraints_v1 zwp_pointer_constraints_v1 + * @section page_iface_zwp_pointer_constraints_v1_desc Description + * + * The global interface exposing pointer constraining functionality. It + * exposes two requests: lock_pointer for locking the pointer to its + * position, and confine_pointer for locking the pointer to a region. + * + * The lock_pointer and confine_pointer requests create the objects + * wp_locked_pointer and wp_confined_pointer respectively, and the client can + * use these objects to interact with the lock. + * + * For any surface, only one lock or confinement may be active across all + * wl_pointer objects of the same seat. If a lock or confinement is requested + * when another lock or confinement is active or requested on the same surface + * and with any of the wl_pointer objects of the same seat, an + * 'already_constrained' error will be raised. + * @section page_iface_zwp_pointer_constraints_v1_api API + * See @ref iface_zwp_pointer_constraints_v1. + */ +/** + * @defgroup iface_zwp_pointer_constraints_v1 The zwp_pointer_constraints_v1 + * interface + * + * The global interface exposing pointer constraining functionality. It + * exposes two requests: lock_pointer for locking the pointer to its + * position, and confine_pointer for locking the pointer to a region. + * + * The lock_pointer and confine_pointer requests create the objects + * wp_locked_pointer and wp_confined_pointer respectively, and the client can + * use these objects to interact with the lock. + * + * For any surface, only one lock or confinement may be active across all + * wl_pointer objects of the same seat. If a lock or confinement is requested + * when another lock or confinement is active or requested on the same surface + * and with any of the wl_pointer objects of the same seat, an + * 'already_constrained' error will be raised. + */ +extern const struct wl_interface zwp_pointer_constraints_v1_interface; +/** + * @page page_iface_zwp_locked_pointer_v1 zwp_locked_pointer_v1 + * @section page_iface_zwp_locked_pointer_v1_desc Description + * + * The wp_locked_pointer interface represents a locked pointer state. + * + * While the lock of this object is active, the wl_pointer objects of the + * associated seat will not emit any wl_pointer.motion events. + * + * This object will send the event 'locked' when the lock is activated. + * Whenever the lock is activated, it is guaranteed that the locked surface + * will already have received pointer focus and that the pointer will be + * within the region passed to the request creating this object. + * + * To unlock the pointer, send the destroy request. This will also destroy + * the wp_locked_pointer object. + * + * If the compositor decides to unlock the pointer the unlocked event is + * sent. See wp_locked_pointer.unlock for details. + * + * When unlocking, the compositor may warp the cursor position to the set + * cursor position hint. If it does, it will not result in any relative + * motion events emitted via wp_relative_pointer. + * + * If the surface the lock was requested on is destroyed and the lock is not + * yet activated, the wp_locked_pointer object is now defunct and must be + * destroyed. + * @section page_iface_zwp_locked_pointer_v1_api API + * See @ref iface_zwp_locked_pointer_v1. + */ +/** + * @defgroup iface_zwp_locked_pointer_v1 The zwp_locked_pointer_v1 interface + * + * The wp_locked_pointer interface represents a locked pointer state. + * + * While the lock of this object is active, the wl_pointer objects of the + * associated seat will not emit any wl_pointer.motion events. + * + * This object will send the event 'locked' when the lock is activated. + * Whenever the lock is activated, it is guaranteed that the locked surface + * will already have received pointer focus and that the pointer will be + * within the region passed to the request creating this object. + * + * To unlock the pointer, send the destroy request. This will also destroy + * the wp_locked_pointer object. + * + * If the compositor decides to unlock the pointer the unlocked event is + * sent. See wp_locked_pointer.unlock for details. + * + * When unlocking, the compositor may warp the cursor position to the set + * cursor position hint. If it does, it will not result in any relative + * motion events emitted via wp_relative_pointer. + * + * If the surface the lock was requested on is destroyed and the lock is not + * yet activated, the wp_locked_pointer object is now defunct and must be + * destroyed. + */ +extern const struct wl_interface zwp_locked_pointer_v1_interface; +/** + * @page page_iface_zwp_confined_pointer_v1 zwp_confined_pointer_v1 + * @section page_iface_zwp_confined_pointer_v1_desc Description + * + * The wp_confined_pointer interface represents a confined pointer state. + * + * This object will send the event 'confined' when the confinement is + * activated. Whenever the confinement is activated, it is guaranteed that + * the surface the pointer is confined to will already have received pointer + * focus and that the pointer will be within the region passed to the request + * creating this object. It is up to the compositor to decide whether this + * requires some user interaction and if the pointer will warp to within the + * passed region if outside. + * + * To unconfine the pointer, send the destroy request. This will also destroy + * the wp_confined_pointer object. + * + * If the compositor decides to unconfine the pointer the unconfined event is + * sent. The wp_confined_pointer object is at this point defunct and should + * be destroyed. + * @section page_iface_zwp_confined_pointer_v1_api API + * See @ref iface_zwp_confined_pointer_v1. + */ +/** + * @defgroup iface_zwp_confined_pointer_v1 The zwp_confined_pointer_v1 interface + * + * The wp_confined_pointer interface represents a confined pointer state. + * + * This object will send the event 'confined' when the confinement is + * activated. Whenever the confinement is activated, it is guaranteed that + * the surface the pointer is confined to will already have received pointer + * focus and that the pointer will be within the region passed to the request + * creating this object. It is up to the compositor to decide whether this + * requires some user interaction and if the pointer will warp to within the + * passed region if outside. + * + * To unconfine the pointer, send the destroy request. This will also destroy + * the wp_confined_pointer object. + * + * If the compositor decides to unconfine the pointer the unconfined event is + * sent. The wp_confined_pointer object is at this point defunct and should + * be destroyed. + */ +extern const struct wl_interface zwp_confined_pointer_v1_interface; + +#ifndef ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM +# define ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM +/** + * @ingroup iface_zwp_pointer_constraints_v1 + * wp_pointer_constraints error values + * + * These errors can be emitted in response to wp_pointer_constraints + * requests. + */ +enum zwp_pointer_constraints_v1_error { + /** + * pointer constraint already requested on that surface + */ + ZWP_POINTER_CONSTRAINTS_V1_ERROR_ALREADY_CONSTRAINED = 1, +}; +#endif /* ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM */ + +#ifndef ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM +# define ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM +/** + * @ingroup iface_zwp_pointer_constraints_v1 + * the pointer constraint may reactivate + * + * A persistent pointer constraint may again reactivate once it has + * been deactivated. See the corresponding deactivation event + * (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) for + * details. + */ +enum zwp_pointer_constraints_v1_lifetime { + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT = 1, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT = 2, +}; +#endif /* ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM */ + +#define ZWP_POINTER_CONSTRAINTS_V1_DESTROY 0 +#define ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER 1 +#define ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER 2 + +/** + * @ingroup iface_zwp_pointer_constraints_v1 + */ +#define ZWP_POINTER_CONSTRAINTS_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_pointer_constraints_v1 + */ +#define ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_pointer_constraints_v1 + */ +#define ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER_SINCE_VERSION 1 + +/** @ingroup iface_zwp_pointer_constraints_v1 */ +static inline void zwp_pointer_constraints_v1_set_user_data( + struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1, + void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_pointer_constraints_v1, + user_data); +} + +/** @ingroup iface_zwp_pointer_constraints_v1 */ +static inline void* zwp_pointer_constraints_v1_get_user_data( + struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_pointer_constraints_v1); +} + +static inline uint32_t zwp_pointer_constraints_v1_get_version( + struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_pointer_constraints_v1); +} + +/** + * @ingroup iface_zwp_pointer_constraints_v1 + * + * Used by the client to notify the server that it will no longer use this + * pointer constraints object. + */ +static inline void zwp_pointer_constraints_v1_destroy( + struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_pointer_constraints_v1, + ZWP_POINTER_CONSTRAINTS_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_pointer_constraints_v1); +} + +/** + * @ingroup iface_zwp_pointer_constraints_v1 + * + * The lock_pointer request lets the client request to disable movements of + * the virtual pointer (i.e. the cursor), effectively locking the pointer + * to a position. This request may not take effect immediately; in the + * future, when the compositor deems implementation-specific constraints + * are satisfied, the pointer lock will be activated and the compositor + * sends a locked event. + * + * The protocol provides no guarantee that the constraints are ever + * satisfied, and does not require the compositor to send an error if the + * constraints cannot ever be satisfied. It is thus possible to request a + * lock that will never activate. + * + * There may not be another pointer constraint of any kind requested or + * active on the surface for any of the wl_pointer objects of the seat of + * the passed pointer when requesting a lock. If there is, an error will be + * raised. See general pointer lock documentation for more details. + * + * The intersection of the region passed with this request and the input + * region of the surface is used to determine where the pointer must be + * in order for the lock to activate. It is up to the compositor whether to + * warp the pointer or require some kind of user interaction for the lock + * to activate. If the region is null the surface input region is used. + * + * A surface may receive pointer focus without the lock being activated. + * + * The request creates a new object wp_locked_pointer which is used to + * interact with the lock as well as receive updates about its state. See + * the the description of wp_locked_pointer for further information. + * + * Note that while a pointer is locked, the wl_pointer objects of the + * corresponding seat will not emit any wl_pointer.motion events, but + * relative motion events will still be emitted via wp_relative_pointer + * objects of the same seat. wl_pointer.axis and wl_pointer.button events + * are unaffected. + */ +static inline struct zwp_locked_pointer_v1* +zwp_pointer_constraints_v1_lock_pointer( + struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1, + struct wl_surface* surface, struct wl_pointer* pointer, + struct wl_region* region, uint32_t lifetime) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor( + (struct wl_proxy*)zwp_pointer_constraints_v1, + ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER, &zwp_locked_pointer_v1_interface, + NULL, surface, pointer, region, lifetime); + + return (struct zwp_locked_pointer_v1*)id; +} + +/** + * @ingroup iface_zwp_pointer_constraints_v1 + * + * The confine_pointer request lets the client request to confine the + * pointer cursor to a given region. This request may not take effect + * immediately; in the future, when the compositor deems implementation- + * specific constraints are satisfied, the pointer confinement will be + * activated and the compositor sends a confined event. + * + * The intersection of the region passed with this request and the input + * region of the surface is used to determine where the pointer must be + * in order for the confinement to activate. It is up to the compositor + * whether to warp the pointer or require some kind of user interaction for + * the confinement to activate. If the region is null the surface input + * region is used. + * + * The request will create a new object wp_confined_pointer which is used + * to interact with the confinement as well as receive updates about its + * state. See the the description of wp_confined_pointer for further + * information. + */ +static inline struct zwp_confined_pointer_v1* +zwp_pointer_constraints_v1_confine_pointer( + struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1, + struct wl_surface* surface, struct wl_pointer* pointer, + struct wl_region* region, uint32_t lifetime) { + struct wl_proxy* id; + + id = + wl_proxy_marshal_constructor((struct wl_proxy*)zwp_pointer_constraints_v1, + ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER, + &zwp_confined_pointer_v1_interface, NULL, + surface, pointer, region, lifetime); + + return (struct zwp_confined_pointer_v1*)id; +} + +/** + * @ingroup iface_zwp_locked_pointer_v1 + * @struct zwp_locked_pointer_v1_listener + */ +struct zwp_locked_pointer_v1_listener { + /** + * lock activation event + * + * Notification that the pointer lock of the seat's pointer is + * activated. + */ + void (*locked)(void* data, + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1); + /** + * lock deactivation event + * + * Notification that the pointer lock of the seat's pointer is no + * longer active. If this is a oneshot pointer lock (see + * wp_pointer_constraints.lifetime) this object is now defunct and + * should be destroyed. If this is a persistent pointer lock (see + * wp_pointer_constraints.lifetime) this pointer lock may again + * reactivate in the future. + */ + void (*unlocked)(void* data, + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1); +}; + +/** + * @ingroup iface_zwp_locked_pointer_v1 + */ +static inline int zwp_locked_pointer_v1_add_listener( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1, + const struct zwp_locked_pointer_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)zwp_locked_pointer_v1, + (void (**)(void))listener, data); +} + +#define ZWP_LOCKED_POINTER_V1_DESTROY 0 +#define ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT 1 +#define ZWP_LOCKED_POINTER_V1_SET_REGION 2 + +/** + * @ingroup iface_zwp_locked_pointer_v1 + */ +#define ZWP_LOCKED_POINTER_V1_LOCKED_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_locked_pointer_v1 + */ +#define ZWP_LOCKED_POINTER_V1_UNLOCKED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_locked_pointer_v1 + */ +#define ZWP_LOCKED_POINTER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_locked_pointer_v1 + */ +#define ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_locked_pointer_v1 + */ +#define ZWP_LOCKED_POINTER_V1_SET_REGION_SINCE_VERSION 1 + +/** @ingroup iface_zwp_locked_pointer_v1 */ +static inline void zwp_locked_pointer_v1_set_user_data( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_locked_pointer_v1, user_data); +} + +/** @ingroup iface_zwp_locked_pointer_v1 */ +static inline void* zwp_locked_pointer_v1_get_user_data( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_locked_pointer_v1); +} + +static inline uint32_t zwp_locked_pointer_v1_get_version( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_locked_pointer_v1); +} + +/** + * @ingroup iface_zwp_locked_pointer_v1 + * + * Destroy the locked pointer object. If applicable, the compositor will + * unlock the pointer. + */ +static inline void zwp_locked_pointer_v1_destroy( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_locked_pointer_v1, + ZWP_LOCKED_POINTER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_locked_pointer_v1); +} + +/** + * @ingroup iface_zwp_locked_pointer_v1 + * + * Set the cursor position hint relative to the top left corner of the + * surface. + * + * If the client is drawing its own cursor, it should update the position + * hint to the position of its own cursor. A compositor may use this + * information to warp the pointer upon unlock in order to avoid pointer + * jumps. + * + * The cursor position hint is double buffered. The new hint will only take + * effect when the associated surface gets it pending state applied. See + * wl_surface.commit for details. + */ +static inline void zwp_locked_pointer_v1_set_cursor_position_hint( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1, wl_fixed_t surface_x, + wl_fixed_t surface_y) { + wl_proxy_marshal((struct wl_proxy*)zwp_locked_pointer_v1, + ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT, surface_x, + surface_y); +} + +/** + * @ingroup iface_zwp_locked_pointer_v1 + * + * Set a new region used to lock the pointer. + * + * The new lock region is double-buffered. The new lock region will + * only take effect when the associated surface gets its pending state + * applied. See wl_surface.commit for details. + * + * For details about the lock region, see wp_locked_pointer. + */ +static inline void zwp_locked_pointer_v1_set_region( + struct zwp_locked_pointer_v1* zwp_locked_pointer_v1, + struct wl_region* region) { + wl_proxy_marshal((struct wl_proxy*)zwp_locked_pointer_v1, + ZWP_LOCKED_POINTER_V1_SET_REGION, region); +} + +/** + * @ingroup iface_zwp_confined_pointer_v1 + * @struct zwp_confined_pointer_v1_listener + */ +struct zwp_confined_pointer_v1_listener { + /** + * pointer confined + * + * Notification that the pointer confinement of the seat's + * pointer is activated. + */ + void (*confined)(void* data, + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1); + /** + * pointer unconfined + * + * Notification that the pointer confinement of the seat's + * pointer is no longer active. If this is a oneshot pointer + * confinement (see wp_pointer_constraints.lifetime) this object is + * now defunct and should be destroyed. If this is a persistent + * pointer confinement (see wp_pointer_constraints.lifetime) this + * pointer confinement may again reactivate in the future. + */ + void (*unconfined)(void* data, + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1); +}; + +/** + * @ingroup iface_zwp_confined_pointer_v1 + */ +static inline int zwp_confined_pointer_v1_add_listener( + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1, + const struct zwp_confined_pointer_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)zwp_confined_pointer_v1, + (void (**)(void))listener, data); +} + +#define ZWP_CONFINED_POINTER_V1_DESTROY 0 +#define ZWP_CONFINED_POINTER_V1_SET_REGION 1 + +/** + * @ingroup iface_zwp_confined_pointer_v1 + */ +#define ZWP_CONFINED_POINTER_V1_CONFINED_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_confined_pointer_v1 + */ +#define ZWP_CONFINED_POINTER_V1_UNCONFINED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_confined_pointer_v1 + */ +#define ZWP_CONFINED_POINTER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_confined_pointer_v1 + */ +#define ZWP_CONFINED_POINTER_V1_SET_REGION_SINCE_VERSION 1 + +/** @ingroup iface_zwp_confined_pointer_v1 */ +static inline void zwp_confined_pointer_v1_set_user_data( + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_confined_pointer_v1, user_data); +} + +/** @ingroup iface_zwp_confined_pointer_v1 */ +static inline void* zwp_confined_pointer_v1_get_user_data( + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_confined_pointer_v1); +} + +static inline uint32_t zwp_confined_pointer_v1_get_version( + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_confined_pointer_v1); +} + +/** + * @ingroup iface_zwp_confined_pointer_v1 + * + * Destroy the confined pointer object. If applicable, the compositor will + * unconfine the pointer. + */ +static inline void zwp_confined_pointer_v1_destroy( + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_confined_pointer_v1, + ZWP_CONFINED_POINTER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_confined_pointer_v1); +} + +/** + * @ingroup iface_zwp_confined_pointer_v1 + * + * Set a new region used to confine the pointer. + * + * The new confine region is double-buffered. The new confine region will + * only take effect when the associated surface gets its pending state + * applied. See wl_surface.commit for details. + * + * If the confinement is active when the new confinement region is applied + * and the pointer ends up outside of newly applied region, the pointer may + * warped to a position within the new confinement region. If warped, a + * wl_pointer.motion event will be emitted, but no + * wp_relative_pointer.relative_motion event. + * + * The compositor may also, instead of using the new region, unconfine the + * pointer. + * + * For details about the confine region, see wp_confined_pointer. + */ +static inline void zwp_confined_pointer_v1_set_region( + struct zwp_confined_pointer_v1* zwp_confined_pointer_v1, + struct wl_region* region) { + wl_proxy_marshal((struct wl_proxy*)zwp_confined_pointer_v1, + ZWP_CONFINED_POINTER_V1_SET_REGION, region); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c b/widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c new file mode 100644 index 0000000000..c28ddf6918 --- /dev/null +++ b/widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c @@ -0,0 +1,97 @@ +/* Generated by wayland-scanner 1.18.0 */ + +/* + * Copyright © 2014 Jonas Ådahl + * Copyright © 2015 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 "wayland-util.h" + +#pragma GCC visibility push(default) +extern const struct wl_interface wl_pointer_interface; +extern const struct wl_interface wl_region_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwp_confined_pointer_v1_interface; +extern const struct wl_interface zwp_locked_pointer_v1_interface; +#pragma GCC visibility pop + +static const struct wl_interface* pointer_constraints_unstable_v1_types[] = { + NULL, + NULL, + &zwp_locked_pointer_v1_interface, + &wl_surface_interface, + &wl_pointer_interface, + &wl_region_interface, + NULL, + &zwp_confined_pointer_v1_interface, + &wl_surface_interface, + &wl_pointer_interface, + &wl_region_interface, + NULL, + &wl_region_interface, + &wl_region_interface, +}; + +static const struct wl_message zwp_pointer_constraints_v1_requests[] = { + {"destroy", "", pointer_constraints_unstable_v1_types + 0}, + {"lock_pointer", "noo?ou", pointer_constraints_unstable_v1_types + 2}, + {"confine_pointer", "noo?ou", pointer_constraints_unstable_v1_types + 7}, +}; + +WL_EXPORT const struct wl_interface zwp_pointer_constraints_v1_interface = { + "zwp_pointer_constraints_v1", 1, 3, + zwp_pointer_constraints_v1_requests, 0, NULL, +}; + +static const struct wl_message zwp_locked_pointer_v1_requests[] = { + {"destroy", "", pointer_constraints_unstable_v1_types + 0}, + {"set_cursor_position_hint", "ff", + pointer_constraints_unstable_v1_types + 0}, + {"set_region", "?o", pointer_constraints_unstable_v1_types + 12}, +}; + +static const struct wl_message zwp_locked_pointer_v1_events[] = { + {"locked", "", pointer_constraints_unstable_v1_types + 0}, + {"unlocked", "", pointer_constraints_unstable_v1_types + 0}, +}; + +WL_EXPORT const struct wl_interface zwp_locked_pointer_v1_interface = { + "zwp_locked_pointer_v1", 1, 3, + zwp_locked_pointer_v1_requests, 2, zwp_locked_pointer_v1_events, +}; + +static const struct wl_message zwp_confined_pointer_v1_requests[] = { + {"destroy", "", pointer_constraints_unstable_v1_types + 0}, + {"set_region", "?o", pointer_constraints_unstable_v1_types + 13}, +}; + +static const struct wl_message zwp_confined_pointer_v1_events[] = { + {"confined", "", pointer_constraints_unstable_v1_types + 0}, + {"unconfined", "", pointer_constraints_unstable_v1_types + 0}, +}; + +WL_EXPORT const struct wl_interface zwp_confined_pointer_v1_interface = { + "zwp_confined_pointer_v1", 1, 2, + zwp_confined_pointer_v1_requests, 2, zwp_confined_pointer_v1_events, +}; diff --git a/widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h b/widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h new file mode 100644 index 0000000000..dbae8081f4 --- /dev/null +++ b/widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h @@ -0,0 +1,293 @@ +/* Generated by wayland-scanner 1.18.0 */ + +#ifndef RELATIVE_POINTER_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define RELATIVE_POINTER_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_relative_pointer_unstable_v1 The relative_pointer_unstable_v1 + * protocol protocol for relative pointer motion events + * + * @section page_desc_relative_pointer_unstable_v1 Description + * + * This protocol specifies a set of interfaces used for making clients able to + * receive relative pointer events not obstructed by barriers (such as the + * monitor edge or other pointer barriers). + * + * To start receiving relative pointer events, a client must first bind the + * global interface "wp_relative_pointer_manager" which, if a compositor + * supports relative pointer motion events, is exposed by the registry. After + * having created the relative pointer manager proxy object, the client uses + * it to create the actual relative pointer object using the + * "get_relative_pointer" request given a wl_pointer. The relative pointer + * motion events will then, when applicable, be transmitted via the proxy of + * the newly created relative pointer object. See the documentation of the + * relative pointer interface for more details. + * + * 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_relative_pointer_unstable_v1 Interfaces + * - @subpage page_iface_zwp_relative_pointer_manager_v1 - get relative pointer + * objects + * - @subpage page_iface_zwp_relative_pointer_v1 - relative pointer object + * @section page_copyright_relative_pointer_unstable_v1 Copyright + * <pre> + * + * Copyright © 2014 Jonas Ådahl + * Copyright © 2015 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_pointer; +struct zwp_relative_pointer_manager_v1; +struct zwp_relative_pointer_v1; + +/** + * @page page_iface_zwp_relative_pointer_manager_v1 + * zwp_relative_pointer_manager_v1 + * @section page_iface_zwp_relative_pointer_manager_v1_desc Description + * + * A global interface used for getting the relative pointer object for a + * given pointer. + * @section page_iface_zwp_relative_pointer_manager_v1_api API + * See @ref iface_zwp_relative_pointer_manager_v1. + */ +/** + * @defgroup iface_zwp_relative_pointer_manager_v1 The + * zwp_relative_pointer_manager_v1 interface + * + * A global interface used for getting the relative pointer object for a + * given pointer. + */ +extern const struct wl_interface zwp_relative_pointer_manager_v1_interface; +/** + * @page page_iface_zwp_relative_pointer_v1 zwp_relative_pointer_v1 + * @section page_iface_zwp_relative_pointer_v1_desc Description + * + * A wp_relative_pointer object is an extension to the wl_pointer interface + * used for emitting relative pointer events. It shares the same focus as + * wl_pointer objects of the same seat and will only emit events when it has + * focus. + * @section page_iface_zwp_relative_pointer_v1_api API + * See @ref iface_zwp_relative_pointer_v1. + */ +/** + * @defgroup iface_zwp_relative_pointer_v1 The zwp_relative_pointer_v1 interface + * + * A wp_relative_pointer object is an extension to the wl_pointer interface + * used for emitting relative pointer events. It shares the same focus as + * wl_pointer objects of the same seat and will only emit events when it has + * focus. + */ +extern const struct wl_interface zwp_relative_pointer_v1_interface; + +#define ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY 0 +#define ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER 1 + +/** + * @ingroup iface_zwp_relative_pointer_manager_v1 + */ +#define ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_relative_pointer_manager_v1 + */ +#define ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER_SINCE_VERSION 1 + +/** @ingroup iface_zwp_relative_pointer_manager_v1 */ +static inline void zwp_relative_pointer_manager_v1_set_user_data( + struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1, + void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_relative_pointer_manager_v1, + user_data); +} + +/** @ingroup iface_zwp_relative_pointer_manager_v1 */ +static inline void* zwp_relative_pointer_manager_v1_get_user_data( + struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1) { + return wl_proxy_get_user_data( + (struct wl_proxy*)zwp_relative_pointer_manager_v1); +} + +static inline uint32_t zwp_relative_pointer_manager_v1_get_version( + struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1) { + return wl_proxy_get_version( + (struct wl_proxy*)zwp_relative_pointer_manager_v1); +} + +/** + * @ingroup iface_zwp_relative_pointer_manager_v1 + * + * Used by the client to notify the server that it will no longer use this + * relative pointer manager object. + */ +static inline void zwp_relative_pointer_manager_v1_destroy( + struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_relative_pointer_manager_v1, + ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_relative_pointer_manager_v1); +} + +/** + * @ingroup iface_zwp_relative_pointer_manager_v1 + * + * Create a relative pointer interface given a wl_pointer object. See the + * wp_relative_pointer interface for more details. + */ +static inline struct zwp_relative_pointer_v1* +zwp_relative_pointer_manager_v1_get_relative_pointer( + struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1, + struct wl_pointer* pointer) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor( + (struct wl_proxy*)zwp_relative_pointer_manager_v1, + ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER, + &zwp_relative_pointer_v1_interface, NULL, pointer); + + return (struct zwp_relative_pointer_v1*)id; +} + +/** + * @ingroup iface_zwp_relative_pointer_v1 + * @struct zwp_relative_pointer_v1_listener + */ +struct zwp_relative_pointer_v1_listener { + /** + * relative pointer motion + * + * Relative x/y pointer motion from the pointer of the seat + * associated with this object. + * + * A relative motion is in the same dimension as regular wl_pointer + * motion events, except they do not represent an absolute + * position. For example, moving a pointer from (x, y) to (x', y') + * would have the equivalent relative motion (x' - x, y' - y). If a + * pointer motion caused the absolute pointer position to be + * clipped by for example the edge of the monitor, the relative + * motion is unaffected by the clipping and will represent the + * unclipped motion. + * + * This event also contains non-accelerated motion deltas. The + * non-accelerated delta is, when applicable, the regular pointer + * motion delta as it was before having applied motion acceleration + * and other transformations such as normalization. + * + * Note that the non-accelerated delta does not represent 'raw' + * events as they were read from some device. Pointer motion + * acceleration is device- and configuration-specific and + * non-accelerated deltas and accelerated deltas may have the same + * value on some devices. + * + * Relative motions are not coupled to wl_pointer.motion events, + * and can be sent in combination with such events, but also + * independently. There may also be scenarios where + * wl_pointer.motion is sent, but there is no relative motion. The + * order of an absolute and relative motion event originating from + * the same physical motion is not guaranteed. + * + * If the client needs button events or focus state, it can receive + * them from a wl_pointer object of the same seat that the + * wp_relative_pointer object is associated with. + * @param utime_hi high 32 bits of a 64 bit timestamp with microsecond + * granularity + * @param utime_lo low 32 bits of a 64 bit timestamp with microsecond + * granularity + * @param dx the x component of the motion vector + * @param dy the y component of the motion vector + * @param dx_unaccel the x component of the unaccelerated motion vector + * @param dy_unaccel the y component of the unaccelerated motion vector + */ + void (*relative_motion)( + void* data, struct zwp_relative_pointer_v1* zwp_relative_pointer_v1, + uint32_t utime_hi, uint32_t utime_lo, wl_fixed_t dx, wl_fixed_t dy, + wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel); +}; + +/** + * @ingroup iface_zwp_relative_pointer_v1 + */ +static inline int zwp_relative_pointer_v1_add_listener( + struct zwp_relative_pointer_v1* zwp_relative_pointer_v1, + const struct zwp_relative_pointer_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)zwp_relative_pointer_v1, + (void (**)(void))listener, data); +} + +#define ZWP_RELATIVE_POINTER_V1_DESTROY 0 + +/** + * @ingroup iface_zwp_relative_pointer_v1 + */ +#define ZWP_RELATIVE_POINTER_V1_RELATIVE_MOTION_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_relative_pointer_v1 + */ +#define ZWP_RELATIVE_POINTER_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwp_relative_pointer_v1 */ +static inline void zwp_relative_pointer_v1_set_user_data( + struct zwp_relative_pointer_v1* zwp_relative_pointer_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)zwp_relative_pointer_v1, user_data); +} + +/** @ingroup iface_zwp_relative_pointer_v1 */ +static inline void* zwp_relative_pointer_v1_get_user_data( + struct zwp_relative_pointer_v1* zwp_relative_pointer_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)zwp_relative_pointer_v1); +} + +static inline uint32_t zwp_relative_pointer_v1_get_version( + struct zwp_relative_pointer_v1* zwp_relative_pointer_v1) { + return wl_proxy_get_version((struct wl_proxy*)zwp_relative_pointer_v1); +} + +/** + * @ingroup iface_zwp_relative_pointer_v1 + */ +static inline void zwp_relative_pointer_v1_destroy( + struct zwp_relative_pointer_v1* zwp_relative_pointer_v1) { + wl_proxy_marshal((struct wl_proxy*)zwp_relative_pointer_v1, + ZWP_RELATIVE_POINTER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)zwp_relative_pointer_v1); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c b/widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c new file mode 100644 index 0000000000..3534686a3d --- /dev/null +++ b/widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c @@ -0,0 +1,69 @@ +/* Generated by wayland-scanner 1.18.0 */ + +/* + * Copyright © 2014 Jonas Ådahl + * Copyright © 2015 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 "wayland-util.h" + +#pragma GCC visibility push(default) +extern const struct wl_interface wl_pointer_interface; +extern const struct wl_interface zwp_relative_pointer_v1_interface; +#pragma GCC visibility pop + +static const struct wl_interface* relative_pointer_unstable_v1_types[] = { + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + &zwp_relative_pointer_v1_interface, + &wl_pointer_interface, +}; + +static const struct wl_message zwp_relative_pointer_manager_v1_requests[] = { + {"destroy", "", relative_pointer_unstable_v1_types + 0}, + {"get_relative_pointer", "no", relative_pointer_unstable_v1_types + 6}, +}; + +WL_EXPORT const struct wl_interface zwp_relative_pointer_manager_v1_interface = + { + "zwp_relative_pointer_manager_v1", 1, 2, + zwp_relative_pointer_manager_v1_requests, 0, NULL, +}; + +static const struct wl_message zwp_relative_pointer_v1_requests[] = { + {"destroy", "", relative_pointer_unstable_v1_types + 0}, +}; + +static const struct wl_message zwp_relative_pointer_v1_events[] = { + {"relative_motion", "uuffff", relative_pointer_unstable_v1_types + 0}, +}; + +WL_EXPORT const struct wl_interface zwp_relative_pointer_v1_interface = { + "zwp_relative_pointer_v1", 1, 1, + zwp_relative_pointer_v1_requests, 1, zwp_relative_pointer_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/viewporter-client-protocol.h b/widget/gtk/wayland/viewporter-client-protocol.h new file mode 100644 index 0000000000..4c6c5bd910 --- /dev/null +++ b/widget/gtk/wayland/viewporter-client-protocol.h @@ -0,0 +1,392 @@ +/* Generated by wayland-scanner 1.18.0 */ + +#ifndef VIEWPORTER_CLIENT_PROTOCOL_H +#define VIEWPORTER_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_viewporter The viewporter protocol + * @section page_ifaces_viewporter Interfaces + * - @subpage page_iface_wp_viewporter - surface cropping and scaling + * - @subpage page_iface_wp_viewport - crop and scale interface to a wl_surface + * @section page_copyright_viewporter Copyright + * <pre> + * + * Copyright © 2013-2016 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_surface; +struct wp_viewport; +struct wp_viewporter; + +/** + * @page page_iface_wp_viewporter wp_viewporter + * @section page_iface_wp_viewporter_desc Description + * + * The global interface exposing surface cropping and scaling + * capabilities is used to instantiate an interface extension for a + * wl_surface object. This extended interface will then allow + * cropping and scaling the surface contents, effectively + * disconnecting the direct relationship between the buffer and the + * surface size. + * @section page_iface_wp_viewporter_api API + * See @ref iface_wp_viewporter. + */ +/** + * @defgroup iface_wp_viewporter The wp_viewporter interface + * + * The global interface exposing surface cropping and scaling + * capabilities is used to instantiate an interface extension for a + * wl_surface object. This extended interface will then allow + * cropping and scaling the surface contents, effectively + * disconnecting the direct relationship between the buffer and the + * surface size. + */ +extern const struct wl_interface wp_viewporter_interface; +/** + * @page page_iface_wp_viewport wp_viewport + * @section page_iface_wp_viewport_desc Description + * + * An additional interface to a wl_surface object, which allows the + * client to specify the cropping and scaling of the surface + * contents. + * + * This interface works with two concepts: the source rectangle (src_x, + * src_y, src_width, src_height), and the destination size (dst_width, + * dst_height). The contents of the source rectangle are scaled to the + * destination size, and content outside the source rectangle is ignored. + * This state is double-buffered, and is applied on the next + * wl_surface.commit. + * + * The two parts of crop and scale state are independent: the source + * rectangle, and the destination size. Initially both are unset, that + * is, no scaling is applied. The whole of the current wl_buffer is + * used as the source, and the surface size is as defined in + * wl_surface.attach. + * + * If the destination size is set, it causes the surface size to become + * dst_width, dst_height. The source (rectangle) is scaled to exactly + * this size. This overrides whatever the attached wl_buffer size is, + * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface + * has no content and therefore no size. Otherwise, the size is always + * at least 1x1 in surface local coordinates. + * + * If the source rectangle is set, it defines what area of the wl_buffer is + * taken as the source. If the source rectangle is set and the destination + * size is not set, then src_width and src_height must be integers, and the + * surface size becomes the source rectangle size. This results in cropping + * without scaling. If src_width or src_height are not integers and + * destination size is not set, the bad_size protocol error is raised when + * the surface state is applied. + * + * The coordinate transformations from buffer pixel coordinates up to + * the surface-local coordinates happen in the following order: + * 1. buffer_transform (wl_surface.set_buffer_transform) + * 2. buffer_scale (wl_surface.set_buffer_scale) + * 3. crop and scale (wp_viewport.set*) + * This means, that the source rectangle coordinates of crop and scale + * are given in the coordinates after the buffer transform and scale, + * i.e. in the coordinates that would be the surface-local coordinates + * if the crop and scale was not applied. + * + * If src_x or src_y are negative, the bad_value protocol error is raised. + * Otherwise, if the source rectangle is partially or completely outside of + * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised + * when the surface state is applied. A NULL wl_buffer does not raise the + * out_of_buffer error. + * + * The x, y arguments of wl_surface.attach are applied as normal to + * the surface. They indicate how many pixels to remove from the + * surface size from the left and the top. In other words, they are + * still in the surface-local coordinate system, just like dst_width + * and dst_height are. + * + * If the wl_surface associated with the wp_viewport is destroyed, + * all wp_viewport requests except 'destroy' raise the protocol error + * no_surface. + * + * If the wp_viewport object is destroyed, the crop and scale + * state is removed from the wl_surface. The change will be applied + * on the next wl_surface.commit. + * @section page_iface_wp_viewport_api API + * See @ref iface_wp_viewport. + */ +/** + * @defgroup iface_wp_viewport The wp_viewport interface + * + * An additional interface to a wl_surface object, which allows the + * client to specify the cropping and scaling of the surface + * contents. + * + * This interface works with two concepts: the source rectangle (src_x, + * src_y, src_width, src_height), and the destination size (dst_width, + * dst_height). The contents of the source rectangle are scaled to the + * destination size, and content outside the source rectangle is ignored. + * This state is double-buffered, and is applied on the next + * wl_surface.commit. + * + * The two parts of crop and scale state are independent: the source + * rectangle, and the destination size. Initially both are unset, that + * is, no scaling is applied. The whole of the current wl_buffer is + * used as the source, and the surface size is as defined in + * wl_surface.attach. + * + * If the destination size is set, it causes the surface size to become + * dst_width, dst_height. The source (rectangle) is scaled to exactly + * this size. This overrides whatever the attached wl_buffer size is, + * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface + * has no content and therefore no size. Otherwise, the size is always + * at least 1x1 in surface local coordinates. + * + * If the source rectangle is set, it defines what area of the wl_buffer is + * taken as the source. If the source rectangle is set and the destination + * size is not set, then src_width and src_height must be integers, and the + * surface size becomes the source rectangle size. This results in cropping + * without scaling. If src_width or src_height are not integers and + * destination size is not set, the bad_size protocol error is raised when + * the surface state is applied. + * + * The coordinate transformations from buffer pixel coordinates up to + * the surface-local coordinates happen in the following order: + * 1. buffer_transform (wl_surface.set_buffer_transform) + * 2. buffer_scale (wl_surface.set_buffer_scale) + * 3. crop and scale (wp_viewport.set*) + * This means, that the source rectangle coordinates of crop and scale + * are given in the coordinates after the buffer transform and scale, + * i.e. in the coordinates that would be the surface-local coordinates + * if the crop and scale was not applied. + * + * If src_x or src_y are negative, the bad_value protocol error is raised. + * Otherwise, if the source rectangle is partially or completely outside of + * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised + * when the surface state is applied. A NULL wl_buffer does not raise the + * out_of_buffer error. + * + * The x, y arguments of wl_surface.attach are applied as normal to + * the surface. They indicate how many pixels to remove from the + * surface size from the left and the top. In other words, they are + * still in the surface-local coordinate system, just like dst_width + * and dst_height are. + * + * If the wl_surface associated with the wp_viewport is destroyed, + * all wp_viewport requests except 'destroy' raise the protocol error + * no_surface. + * + * If the wp_viewport object is destroyed, the crop and scale + * state is removed from the wl_surface. The change will be applied + * on the next wl_surface.commit. + */ +extern const struct wl_interface wp_viewport_interface; + +#ifndef WP_VIEWPORTER_ERROR_ENUM +# define WP_VIEWPORTER_ERROR_ENUM +enum wp_viewporter_error { + /** + * the surface already has a viewport object associated + */ + WP_VIEWPORTER_ERROR_VIEWPORT_EXISTS = 0, +}; +#endif /* WP_VIEWPORTER_ERROR_ENUM */ + +#define WP_VIEWPORTER_DESTROY 0 +#define WP_VIEWPORTER_GET_VIEWPORT 1 + +/** + * @ingroup iface_wp_viewporter + */ +#define WP_VIEWPORTER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_wp_viewporter + */ +#define WP_VIEWPORTER_GET_VIEWPORT_SINCE_VERSION 1 + +/** @ingroup iface_wp_viewporter */ +static inline void wp_viewporter_set_user_data( + struct wp_viewporter* wp_viewporter, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)wp_viewporter, user_data); +} + +/** @ingroup iface_wp_viewporter */ +static inline void* wp_viewporter_get_user_data( + struct wp_viewporter* wp_viewporter) { + return wl_proxy_get_user_data((struct wl_proxy*)wp_viewporter); +} + +static inline uint32_t wp_viewporter_get_version( + struct wp_viewporter* wp_viewporter) { + return wl_proxy_get_version((struct wl_proxy*)wp_viewporter); +} + +/** + * @ingroup iface_wp_viewporter + * + * Informs the server that the client will not be using this + * protocol object anymore. This does not affect any other objects, + * wp_viewport objects included. + */ +static inline void wp_viewporter_destroy(struct wp_viewporter* wp_viewporter) { + wl_proxy_marshal((struct wl_proxy*)wp_viewporter, WP_VIEWPORTER_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)wp_viewporter); +} + +/** + * @ingroup iface_wp_viewporter + * + * Instantiate an interface extension for the given wl_surface to + * crop and scale its content. If the given wl_surface already has + * a wp_viewport object associated, the viewport_exists + * protocol error is raised. + */ +static inline struct wp_viewport* wp_viewporter_get_viewport( + struct wp_viewporter* wp_viewporter, struct wl_surface* surface) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor((struct wl_proxy*)wp_viewporter, + WP_VIEWPORTER_GET_VIEWPORT, + &wp_viewport_interface, NULL, surface); + + return (struct wp_viewport*)id; +} + +#ifndef WP_VIEWPORT_ERROR_ENUM +# define WP_VIEWPORT_ERROR_ENUM +enum wp_viewport_error { + /** + * negative or zero values in width or height + */ + WP_VIEWPORT_ERROR_BAD_VALUE = 0, + /** + * destination size is not integer + */ + WP_VIEWPORT_ERROR_BAD_SIZE = 1, + /** + * source rectangle extends outside of the content area + */ + WP_VIEWPORT_ERROR_OUT_OF_BUFFER = 2, + /** + * the wl_surface was destroyed + */ + WP_VIEWPORT_ERROR_NO_SURFACE = 3, +}; +#endif /* WP_VIEWPORT_ERROR_ENUM */ + +#define WP_VIEWPORT_DESTROY 0 +#define WP_VIEWPORT_SET_SOURCE 1 +#define WP_VIEWPORT_SET_DESTINATION 2 + +/** + * @ingroup iface_wp_viewport + */ +#define WP_VIEWPORT_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_wp_viewport + */ +#define WP_VIEWPORT_SET_SOURCE_SINCE_VERSION 1 +/** + * @ingroup iface_wp_viewport + */ +#define WP_VIEWPORT_SET_DESTINATION_SINCE_VERSION 1 + +/** @ingroup iface_wp_viewport */ +static inline void wp_viewport_set_user_data(struct wp_viewport* wp_viewport, + void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)wp_viewport, user_data); +} + +/** @ingroup iface_wp_viewport */ +static inline void* wp_viewport_get_user_data(struct wp_viewport* wp_viewport) { + return wl_proxy_get_user_data((struct wl_proxy*)wp_viewport); +} + +static inline uint32_t wp_viewport_get_version( + struct wp_viewport* wp_viewport) { + return wl_proxy_get_version((struct wl_proxy*)wp_viewport); +} + +/** + * @ingroup iface_wp_viewport + * + * The associated wl_surface's crop and scale state is removed. + * The change is applied on the next wl_surface.commit. + */ +static inline void wp_viewport_destroy(struct wp_viewport* wp_viewport) { + wl_proxy_marshal((struct wl_proxy*)wp_viewport, WP_VIEWPORT_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)wp_viewport); +} + +/** + * @ingroup iface_wp_viewport + * + * Set the source rectangle of the associated wl_surface. See + * wp_viewport for the description, and relation to the wl_buffer + * size. + * + * If all of x, y, width and height are -1.0, the source rectangle is + * unset instead. Any other set of values where width or height are zero + * or negative, or x or y are negative, raise the bad_value protocol + * error. + * + * The crop and scale state is double-buffered state, and will be + * applied on the next wl_surface.commit. + */ +static inline void wp_viewport_set_source(struct wp_viewport* wp_viewport, + wl_fixed_t x, wl_fixed_t y, + wl_fixed_t width, wl_fixed_t height) { + wl_proxy_marshal((struct wl_proxy*)wp_viewport, WP_VIEWPORT_SET_SOURCE, x, y, + width, height); +} + +/** + * @ingroup iface_wp_viewport + * + * Set the destination size of the associated wl_surface. See + * wp_viewport for the description, and relation to the wl_buffer + * size. + * + * If width is -1 and height is -1, the destination size is unset + * instead. Any other pair of values for width and height that + * contains zero or negative values raises the bad_value protocol + * error. + * + * The crop and scale state is double-buffered state, and will be + * applied on the next wl_surface.commit. + */ +static inline void wp_viewport_set_destination(struct wp_viewport* wp_viewport, + int32_t width, int32_t height) { + wl_proxy_marshal((struct wl_proxy*)wp_viewport, WP_VIEWPORT_SET_DESTINATION, + width, height); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/viewporter-protocol.c b/widget/gtk/wayland/viewporter-protocol.c new file mode 100644 index 0000000000..06b7901426 --- /dev/null +++ b/widget/gtk/wayland/viewporter-protocol.c @@ -0,0 +1,56 @@ +/* Generated by wayland-scanner 1.18.0 */ + +/* + * Copyright © 2013-2016 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_surface_interface; +extern const struct wl_interface wp_viewport_interface; +#pragma GCC visibility pop + +static const struct wl_interface* viewporter_types[] = { + NULL, NULL, NULL, NULL, &wp_viewport_interface, &wl_surface_interface, +}; + +static const struct wl_message wp_viewporter_requests[] = { + {"destroy", "", viewporter_types + 0}, + {"get_viewport", "no", viewporter_types + 4}, +}; + +const struct wl_interface wp_viewporter_interface = { + "wp_viewporter", 1, 2, wp_viewporter_requests, 0, NULL, +}; + +static const struct wl_message wp_viewport_requests[] = { + {"destroy", "", viewporter_types + 0}, + {"set_source", "ffff", viewporter_types + 0}, + {"set_destination", "ii", viewporter_types + 0}, +}; + +const struct wl_interface wp_viewport_interface = { + "wp_viewport", 1, 3, wp_viewport_requests, 0, NULL, +}; diff --git a/widget/gtk/wayland/xdg-activation-v1-client-protocol.h b/widget/gtk/wayland/xdg-activation-v1-client-protocol.h new file mode 100644 index 0000000000..9bebcb6ec2 --- /dev/null +++ b/widget/gtk/wayland/xdg-activation-v1-client-protocol.h @@ -0,0 +1,409 @@ +/* Generated by wayland-scanner 1.19.0 */ + +#ifndef XDG_ACTIVATION_V1_CLIENT_PROTOCOL_H +#define XDG_ACTIVATION_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_activation_v1 The xdg_activation_v1 protocol + * Protocol for requesting activation of surfaces + * + * @section page_desc_xdg_activation_v1 Description + * + * The way for a client to pass focus to another toplevel is as follows. + * + * The client that intends to activate another toplevel uses the + * xdg_activation_v1.get_activation_token request to get an activation token. + * This token is then forwarded to the client, which is supposed to activate + * one of its surfaces, through a separate band of communication. + * + * One established way of doing this is through the XDG_ACTIVATION_TOKEN + * environment variable of a newly launched child process. The child process + * should unset the environment variable again right after reading it out in + * order to avoid propagating it to other child processes. + * + * Another established way exists for Applications implementing the D-Bus + * interface org.freedesktop.Application, which should get their token under + * XDG_ACTIVATION_TOKEN on their platform_data. + * + * In general activation tokens may be transferred across clients through + * means not described in this protocol. + * + * The client to be activated will then pass the token + * it received to the xdg_activation_v1.activate request. The compositor can + * then use this token to decide how to react to the activation request. + * + * The token the activating client gets may be ineffective either already at + * the time it receives it, for example if it was not focused, for focus + * stealing prevention. The activating client will have no way to discover + * the validity of the token, and may still forward it to the to be activated + * client. + * + * The created activation token may optionally get information attached to it + * that can be used by the compositor to identify the application that we + * intend to activate. This can for example be used to display a visual hint + * about what application is being started. + * + * Warning! The protocol described in this file is currently in the testing + * phase. Backward compatible changes may be added together with the + * corresponding interface version bump. Backward incompatible changes can + * only be done by creating a new major version of the extension. + * + * @section page_ifaces_xdg_activation_v1 Interfaces + * - @subpage page_iface_xdg_activation_v1 - interface for activating surfaces + * - @subpage page_iface_xdg_activation_token_v1 - an exported activation handle + * @section page_copyright_xdg_activation_v1 Copyright + * <pre> + * + * Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org> + * Copyright © 2020 Carlos Garnacho <carlosg@gnome.org> + * + * 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 wl_surface; +struct xdg_activation_token_v1; +struct xdg_activation_v1; + +#ifndef XDG_ACTIVATION_V1_INTERFACE +# define XDG_ACTIVATION_V1_INTERFACE +/** + * @page page_iface_xdg_activation_v1 xdg_activation_v1 + * @section page_iface_xdg_activation_v1_desc Description + * + * A global interface used for informing the compositor about applications + * being activated or started, or for applications to request to be + * activated. + * @section page_iface_xdg_activation_v1_api API + * See @ref iface_xdg_activation_v1. + */ +/** + * @defgroup iface_xdg_activation_v1 The xdg_activation_v1 interface + * + * A global interface used for informing the compositor about applications + * being activated or started, or for applications to request to be + * activated. + */ +extern const struct wl_interface xdg_activation_v1_interface; +#endif +#ifndef XDG_ACTIVATION_TOKEN_V1_INTERFACE +# define XDG_ACTIVATION_TOKEN_V1_INTERFACE +/** + * @page page_iface_xdg_activation_token_v1 xdg_activation_token_v1 + * @section page_iface_xdg_activation_token_v1_desc Description + * + * An object for setting up a token and receiving a token handle that can + * be passed as an activation token to another client. + * + * The object is created using the xdg_activation_v1.get_activation_token + * request. This object should then be populated with the app_id, surface + * and serial information and committed. The compositor shall then issue a + * done event with the token. In case the request's parameters are invalid, + * the compositor will provide an invalid token. + * @section page_iface_xdg_activation_token_v1_api API + * See @ref iface_xdg_activation_token_v1. + */ +/** + * @defgroup iface_xdg_activation_token_v1 The xdg_activation_token_v1 interface + * + * An object for setting up a token and receiving a token handle that can + * be passed as an activation token to another client. + * + * The object is created using the xdg_activation_v1.get_activation_token + * request. This object should then be populated with the app_id, surface + * and serial information and committed. The compositor shall then issue a + * done event with the token. In case the request's parameters are invalid, + * the compositor will provide an invalid token. + */ +extern const struct wl_interface xdg_activation_token_v1_interface; +#endif + +#define XDG_ACTIVATION_V1_DESTROY 0 +#define XDG_ACTIVATION_V1_GET_ACTIVATION_TOKEN 1 +#define XDG_ACTIVATION_V1_ACTIVATE 2 + +/** + * @ingroup iface_xdg_activation_v1 + */ +#define XDG_ACTIVATION_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_activation_v1 + */ +#define XDG_ACTIVATION_V1_GET_ACTIVATION_TOKEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_activation_v1 + */ +#define XDG_ACTIVATION_V1_ACTIVATE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_activation_v1 */ +static inline void xdg_activation_v1_set_user_data( + struct xdg_activation_v1* xdg_activation_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)xdg_activation_v1, user_data); +} + +/** @ingroup iface_xdg_activation_v1 */ +static inline void* xdg_activation_v1_get_user_data( + struct xdg_activation_v1* xdg_activation_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)xdg_activation_v1); +} + +static inline uint32_t xdg_activation_v1_get_version( + struct xdg_activation_v1* xdg_activation_v1) { + return wl_proxy_get_version((struct wl_proxy*)xdg_activation_v1); +} + +/** + * @ingroup iface_xdg_activation_v1 + * + * Notify the compositor that the xdg_activation object will no longer be + * used. + * + * The child objects created via this interface are unaffected and should + * be destroyed separately. + */ +static inline void xdg_activation_v1_destroy( + struct xdg_activation_v1* xdg_activation_v1) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_v1, + XDG_ACTIVATION_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)xdg_activation_v1); +} + +/** + * @ingroup iface_xdg_activation_v1 + * + * Creates an xdg_activation_token_v1 object that will provide + * the initiating client with a unique token for this activation. This + * token should be offered to the clients to be activated. + */ +static inline struct xdg_activation_token_v1* +xdg_activation_v1_get_activation_token( + struct xdg_activation_v1* xdg_activation_v1) { + struct wl_proxy* id; + + id = wl_proxy_marshal_constructor((struct wl_proxy*)xdg_activation_v1, + XDG_ACTIVATION_V1_GET_ACTIVATION_TOKEN, + &xdg_activation_token_v1_interface, NULL); + + return (struct xdg_activation_token_v1*)id; +} + +/** + * @ingroup iface_xdg_activation_v1 + * + * Requests surface activation. It's up to the compositor to display + * this information as desired, for example by placing the surface above + * the rest. + * + * The compositor may know who requested this by checking the activation + * token and might decide not to follow through with the activation if it's + * considered unwanted. + * + * Compositors can ignore unknown activation tokens when an invalid + * token is passed. + */ +static inline void xdg_activation_v1_activate( + struct xdg_activation_v1* xdg_activation_v1, const char* token, + struct wl_surface* surface) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_v1, + XDG_ACTIVATION_V1_ACTIVATE, token, surface); +} + +#ifndef XDG_ACTIVATION_TOKEN_V1_ERROR_ENUM +# define XDG_ACTIVATION_TOKEN_V1_ERROR_ENUM +enum xdg_activation_token_v1_error { + /** + * The token has already been used previously + */ + XDG_ACTIVATION_TOKEN_V1_ERROR_ALREADY_USED = 0, +}; +#endif /* XDG_ACTIVATION_TOKEN_V1_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_activation_token_v1 + * @struct xdg_activation_token_v1_listener + */ +struct xdg_activation_token_v1_listener { + /** + * the exported activation token + * + * The 'done' event contains the unique token of this activation + * request and notifies that the provider is done. + * @param token the exported activation token + */ + void (*done)(void* data, + struct xdg_activation_token_v1* xdg_activation_token_v1, + const char* token); +}; + +/** + * @ingroup iface_xdg_activation_token_v1 + */ +static inline int xdg_activation_token_v1_add_listener( + struct xdg_activation_token_v1* xdg_activation_token_v1, + const struct xdg_activation_token_v1_listener* listener, void* data) { + return wl_proxy_add_listener((struct wl_proxy*)xdg_activation_token_v1, + (void (**)(void))listener, data); +} + +#define XDG_ACTIVATION_TOKEN_V1_SET_SERIAL 0 +#define XDG_ACTIVATION_TOKEN_V1_SET_APP_ID 1 +#define XDG_ACTIVATION_TOKEN_V1_SET_SURFACE 2 +#define XDG_ACTIVATION_TOKEN_V1_COMMIT 3 +#define XDG_ACTIVATION_TOKEN_V1_DESTROY 4 + +/** + * @ingroup iface_xdg_activation_token_v1 + */ +#define XDG_ACTIVATION_TOKEN_V1_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_activation_token_v1 + */ +#define XDG_ACTIVATION_TOKEN_V1_SET_SERIAL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_activation_token_v1 + */ +#define XDG_ACTIVATION_TOKEN_V1_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_activation_token_v1 + */ +#define XDG_ACTIVATION_TOKEN_V1_SET_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_activation_token_v1 + */ +#define XDG_ACTIVATION_TOKEN_V1_COMMIT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_activation_token_v1 + */ +#define XDG_ACTIVATION_TOKEN_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_xdg_activation_token_v1 */ +static inline void xdg_activation_token_v1_set_user_data( + struct xdg_activation_token_v1* xdg_activation_token_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*)xdg_activation_token_v1, user_data); +} + +/** @ingroup iface_xdg_activation_token_v1 */ +static inline void* xdg_activation_token_v1_get_user_data( + struct xdg_activation_token_v1* xdg_activation_token_v1) { + return wl_proxy_get_user_data((struct wl_proxy*)xdg_activation_token_v1); +} + +static inline uint32_t xdg_activation_token_v1_get_version( + struct xdg_activation_token_v1* xdg_activation_token_v1) { + return wl_proxy_get_version((struct wl_proxy*)xdg_activation_token_v1); +} + +/** + * @ingroup iface_xdg_activation_token_v1 + * + * Provides information about the seat and serial event that requested the + * token. + * + * The serial can come from an input or focus event. For instance, if a + * click triggers the launch of a third-party client, the launcher client + * should send a set_serial request with the serial and seat from the + * wl_pointer.button event. + * + * Some compositors might refuse to activate toplevels when the token + * doesn't have a valid and recent enough event serial. + * + * Must be sent before commit. This information is optional. + */ +static inline void xdg_activation_token_v1_set_serial( + struct xdg_activation_token_v1* xdg_activation_token_v1, uint32_t serial, + struct wl_seat* seat) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1, + XDG_ACTIVATION_TOKEN_V1_SET_SERIAL, serial, seat); +} + +/** + * @ingroup iface_xdg_activation_token_v1 + * + * The requesting client can specify an app_id to associate the token + * being created with it. + * + * Must be sent before commit. This information is optional. + */ +static inline void xdg_activation_token_v1_set_app_id( + struct xdg_activation_token_v1* xdg_activation_token_v1, + const char* app_id) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1, + XDG_ACTIVATION_TOKEN_V1_SET_APP_ID, app_id); +} + +/** + * @ingroup iface_xdg_activation_token_v1 + * + * This request sets the surface requesting the activation. Note, this is + * different from the surface that will be activated. + * + * Some compositors might refuse to activate toplevels when the token + * doesn't have a requesting surface. + * + * Must be sent before commit. This information is optional. + */ +static inline void xdg_activation_token_v1_set_surface( + struct xdg_activation_token_v1* xdg_activation_token_v1, + struct wl_surface* surface) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1, + XDG_ACTIVATION_TOKEN_V1_SET_SURFACE, surface); +} + +/** + * @ingroup iface_xdg_activation_token_v1 + * + * Requests an activation token based on the different parameters that + * have been offered through set_serial, set_surface and set_app_id. + */ +static inline void xdg_activation_token_v1_commit( + struct xdg_activation_token_v1* xdg_activation_token_v1) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1, + XDG_ACTIVATION_TOKEN_V1_COMMIT); +} + +/** + * @ingroup iface_xdg_activation_token_v1 + * + * Notify the compositor that the xdg_activation_token_v1 object will no + * longer be used. + */ +static inline void xdg_activation_token_v1_destroy( + struct xdg_activation_token_v1* xdg_activation_token_v1) { + wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1, + XDG_ACTIVATION_TOKEN_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy*)xdg_activation_token_v1); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/widget/gtk/wayland/xdg-activation-v1-protocol.c b/widget/gtk/wayland/xdg-activation-v1-protocol.c new file mode 100644 index 0000000000..1fad6dbf97 --- /dev/null +++ b/widget/gtk/wayland/xdg-activation-v1-protocol.c @@ -0,0 +1,82 @@ +/* Generated by wayland-scanner 1.19.0 */ + +/* + * Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org> + * Copyright © 2020 Carlos Garnacho <carlosg@gnome.org> + * + * 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 + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +# define WL_PRIVATE __attribute__((visibility("hidden"))) +#else +# define WL_PRIVATE +#endif + +#pragma GCC visibility push(default) +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +#pragma GCC visibility pop +extern const struct wl_interface xdg_activation_token_v1_interface; + +static const struct wl_interface* xdg_activation_v1_types[] = { + NULL, + &xdg_activation_token_v1_interface, + NULL, + &wl_surface_interface, + NULL, + &wl_seat_interface, + &wl_surface_interface, +}; + +static const struct wl_message xdg_activation_v1_requests[] = { + {"destroy", "", xdg_activation_v1_types + 0}, + {"get_activation_token", "n", xdg_activation_v1_types + 1}, + {"activate", "so", xdg_activation_v1_types + 2}, +}; + +WL_PRIVATE const struct wl_interface xdg_activation_v1_interface = { + "xdg_activation_v1", 1, 3, xdg_activation_v1_requests, 0, NULL, +}; + +static const struct wl_message xdg_activation_token_v1_requests[] = { + {"set_serial", "uo", xdg_activation_v1_types + 4}, + {"set_app_id", "s", xdg_activation_v1_types + 0}, + {"set_surface", "o", xdg_activation_v1_types + 6}, + {"commit", "", xdg_activation_v1_types + 0}, + {"destroy", "", xdg_activation_v1_types + 0}, +}; + +static const struct wl_message xdg_activation_token_v1_events[] = { + {"done", "s", xdg_activation_v1_types + 0}, +}; + +WL_PRIVATE const struct wl_interface xdg_activation_token_v1_interface = { + "xdg_activation_token_v1", 1, 5, + xdg_activation_token_v1_requests, 1, xdg_activation_token_v1_events, +}; 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, +}; |