forked from mirrors/gecko-dev
		
	 929e1e4d77
			
		
	
	
		929e1e4d77
		
	
	
	
	
		
			
			This does two things: * Modify `nsXULPopupManager::ShowPopup()` so it calls `ShowPopupAsNativeMenu()` as long as an anchor wasn't passed in, and only on Mac. * Modify `-[MOZMenuOpeningCoordinator _openMenu:atScreenPosition:forView:withAppearance:]` so it also takes a `aIsContextMenu` param. If the param is true, we synthesize a right-click event and pop up a context menu as usual. If it's false, we use `-[NSMenu popUpMenuPositioningItem:atLocation:inView:]` instead. The reason this works is because `-[NSMenu popUpMenuPositioningItem:atLocation:inView:]` opens the menu in a sensible place when the x-y coords are near the right edge of the screen. In contrast, `+[NSMenu popUpContextMenu:withEvent:forView:]` will anchor the menu's top-right corner to the mouse cursor when near the right edge. Differential Revision: https://phabricator.services.mozilla.com/D177355
		
			
				
	
	
		
			424 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			424 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* -*- 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
 |