summaryrefslogtreecommitdiffstats
path: root/widget/gtk/NativeMenuGtk.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/gtk/NativeMenuGtk.cpp')
-rw-r--r--widget/gtk/NativeMenuGtk.cpp423
1 files changed, 423 insertions, 0 deletions
diff --git a/widget/gtk/NativeMenuGtk.cpp b/widget/gtk/NativeMenuGtk.cpp
new file mode 100644
index 0000000000..7f2c5916fe
--- /dev/null
+++ b/widget/gtk/NativeMenuGtk.cpp
@@ -0,0 +1,423 @@
+/* -*- 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) {
+ 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