forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			819 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			819 lines
		
	
	
	
		
			27 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 "AsyncDBus.h"
 | |
| #include "gdk/gdkkeysyms-compat.h"
 | |
| #include "mozilla/BasicEvents.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 "DBusMenu.h"
 | |
| #include "nsLayoutUtils.h"
 | |
| #include "nsGtkKeyUtils.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,
 | |
|                                   nsGkAtoms::menubar);
 | |
| }
 | |
| 
 | |
| // 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 : 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);
 | |
|   }
 | |
| 
 | |
|   dom::Element* Element() { return mElement; }
 | |
| 
 | |
|   void RecomputeModelIfNeeded() {
 | |
|     if (!mDirty) {
 | |
|       return;
 | |
|     }
 | |
|     RecomputeModel();
 | |
|     mDirty = false;
 | |
|   }
 | |
| 
 | |
|   bool IsShowing() { return mShowing; }
 | |
|   void WillShow() {
 | |
|     mShowing = true;
 | |
|     RecomputeModelIfNeeded();
 | |
|   }
 | |
|   void DidHide() { mShowing = false; }
 | |
| 
 | |
|  protected:
 | |
|   virtual void RecomputeModel() = 0;
 | |
|   virtual ~MenuModel() { mElement->RemoveMutationObserver(this); }
 | |
| 
 | |
|   void DirtyModel() {
 | |
|     mDirty = true;
 | |
|     if (mShowing) {
 | |
|       RecomputeModelIfNeeded();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   RefPtr<dom::Element> mElement;
 | |
|   bool mDirty = true;
 | |
|   bool mShowing = false;
 | |
| };
 | |
| 
 | |
| class MenuModelGMenu final : public MenuModel {
 | |
|  public:
 | |
|   explicit MenuModelGMenu(dom::Element* aElement) : MenuModel(aElement) {
 | |
|     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());
 | |
|   }
 | |
| 
 | |
|  protected:
 | |
|   void RecomputeModel() override;
 | |
|   static void RecomputeModelFor(GMenu* aMenu, Actions& aActions,
 | |
|                                 const dom::Element& aElement);
 | |
| 
 | |
|   RefPtr<GMenu> mGMenu;
 | |
|   Actions mActions;
 | |
| };
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| void MenuModelGMenu::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 MenuModelGMenu::RecomputeModel() {
 | |
|   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<MenuModelGMenu>(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.
 | |
| }
 | |
| 
 | |
| #ifdef MOZ_ENABLE_DBUS
 | |
| 
 | |
| class MenubarModelDBus final : public MenuModel {
 | |
|  public:
 | |
|   explicit MenubarModelDBus(dom::Element* aElement) : MenuModel(aElement) {
 | |
|     mRoot = dont_AddRef(dbusmenu_menuitem_new());
 | |
|     dbusmenu_menuitem_set_root(mRoot.get(), true);
 | |
|     mShowing = true;
 | |
|   }
 | |
| 
 | |
|   DbusmenuMenuitem* Root() const { return mRoot.get(); }
 | |
| 
 | |
|  protected:
 | |
|   void RecomputeModel() override;
 | |
|   static void AppendMenuItem(DbusmenuMenuitem* aParent,
 | |
|                              const dom::Element* aElement);
 | |
|   static void AppendSeparator(DbusmenuMenuitem* aParent);
 | |
|   static void AppendSubmenu(DbusmenuMenuitem* aParent,
 | |
|                             const dom::Element* aMenu,
 | |
|                             const dom::Element* aPopup);
 | |
|   static uint RecomputeModelFor(DbusmenuMenuitem* aParent,
 | |
|                                 const dom::Element& aElement);
 | |
| 
 | |
|   RefPtr<DbusmenuMenuitem> mRoot;
 | |
| };
 | |
| 
 | |
| void MenubarModelDBus::RecomputeModel() {
 | |
|   while (GList* children = dbusmenu_menuitem_get_children(mRoot.get())) {
 | |
|     auto* first = static_cast<DbusmenuMenuitem*>(children->data);
 | |
|     if (!first) {
 | |
|       break;
 | |
|     }
 | |
|     dbusmenu_menuitem_child_delete(mRoot.get(), first);
 | |
|   }
 | |
|   RecomputeModelFor(mRoot, *Element());
 | |
| }
 | |
| 
 | |
| static const dom::Element* RelevantElementForKeys(
 | |
|     const dom::Element* aElement) {
 | |
|   nsAutoString key;
 | |
|   aElement->GetAttr(nsGkAtoms::key, key);
 | |
|   if (!key.IsEmpty()) {
 | |
|     dom::Document* document = aElement->OwnerDoc();
 | |
|     dom::Element* element = document->GetElementById(key);
 | |
|     if (element) {
 | |
|       return element;
 | |
|     }
 | |
|   }
 | |
|   return aElement;
 | |
| }
 | |
| 
 | |
| static uint32_t ParseKey(const nsAString& aKey, const nsAString& aKeyCode) {
 | |
|   guint key = 0;
 | |
|   if (!aKey.IsEmpty()) {
 | |
|     key = gdk_unicode_to_keyval(*aKey.BeginReading());
 | |
|   }
 | |
| 
 | |
|   if (key == 0 && !aKeyCode.IsEmpty()) {
 | |
|     key = KeymapWrapper::ConvertGeckoKeyCodeToGDKKeyval(aKeyCode);
 | |
|   }
 | |
| 
 | |
|   return key;
 | |
| }
 | |
| 
 | |
| static uint32_t KeyFrom(const dom::Element* aElement) {
 | |
|   const auto* element = RelevantElementForKeys(aElement);
 | |
| 
 | |
|   nsAutoString key;
 | |
|   nsAutoString keycode;
 | |
|   element->GetAttr(nsGkAtoms::key, key);
 | |
|   element->GetAttr(nsGkAtoms::keycode, keycode);
 | |
| 
 | |
|   return ParseKey(key, keycode);
 | |
| }
 | |
| 
 | |
| // TODO(emilio): Unify with nsMenuUtilsX::GeckoModifiersForNodeAttribute (or
 | |
| // at least switch to strtok_r).
 | |
| static uint32_t ParseModifiers(const nsAString& aModifiers) {
 | |
|   if (aModifiers.IsEmpty()) {
 | |
|     return 0;
 | |
|   }
 | |
| 
 | |
|   uint32_t modifier = 0;
 | |
|   char* str = ToNewUTF8String(aModifiers);
 | |
|   char* token = strtok(str, ", \t");
 | |
|   while (token) {
 | |
|     if (nsCRT::strcmp(token, "shift") == 0) {
 | |
|       modifier |= GDK_SHIFT_MASK;
 | |
|     } else if (nsCRT::strcmp(token, "alt") == 0) {
 | |
|       modifier |= GDK_MOD1_MASK;
 | |
|     } else if (nsCRT::strcmp(token, "meta") == 0) {
 | |
|       modifier |= GDK_META_MASK;
 | |
|     } else if (nsCRT::strcmp(token, "control") == 0) {
 | |
|       modifier |= GDK_CONTROL_MASK;
 | |
|     } else if (nsCRT::strcmp(token, "accel") == 0) {
 | |
|       auto accel = WidgetInputEvent::AccelModifier();
 | |
|       if (accel == MODIFIER_META) {
 | |
|         modifier |= GDK_META_MASK;
 | |
|       } else if (accel == MODIFIER_ALT) {
 | |
|         modifier |= GDK_MOD1_MASK;
 | |
|       } else if (accel == MODIFIER_CONTROL) {
 | |
|         modifier |= GDK_CONTROL_MASK;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     token = strtok(nullptr, ", \t");
 | |
|   }
 | |
| 
 | |
|   free(str);
 | |
| 
 | |
|   return modifier;
 | |
| }
 | |
| 
 | |
| static uint32_t ModifiersFrom(const dom::Element* aContent) {
 | |
|   const auto* element = RelevantElementForKeys(aContent);
 | |
| 
 | |
|   nsAutoString modifiers;
 | |
|   element->GetAttr(nsGkAtoms::modifiers, modifiers);
 | |
| 
 | |
|   return ParseModifiers(modifiers);
 | |
| }
 | |
| 
 | |
| static void UpdateAccel(DbusmenuMenuitem* aItem, const nsIContent* aContent) {
 | |
|   uint32_t key = KeyFrom(aContent->AsElement());
 | |
|   if (key != 0) {
 | |
|     dbusmenu_menuitem_property_set_shortcut(
 | |
|         aItem, key,
 | |
|         static_cast<GdkModifierType>(ModifiersFrom(aContent->AsElement())));
 | |
|   }
 | |
| }
 | |
| 
 | |
| static void UpdateRadioOrCheck(DbusmenuMenuitem* aItem,
 | |
|                                const dom::Element* aContent) {
 | |
|   static mozilla::dom::Element::AttrValuesArray attrs[] = {
 | |
|       nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr};
 | |
|   int32_t type = aContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
 | |
|                                            attrs, eCaseMatters);
 | |
| 
 | |
|   if (type < 0 || type >= 2) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (type == 0) {
 | |
|     dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
 | |
|                                    DBUSMENU_MENUITEM_TOGGLE_CHECK);
 | |
|   } else {
 | |
|     dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
 | |
|                                    DBUSMENU_MENUITEM_TOGGLE_RADIO);
 | |
|   }
 | |
| 
 | |
|   bool isChecked = aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
 | |
|                                          nsGkAtoms::_true, eCaseMatters);
 | |
|   dbusmenu_menuitem_property_set_int(
 | |
|       aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_STATE,
 | |
|       isChecked ? DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED
 | |
|                 : DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED);
 | |
| }
 | |
| 
 | |
| static void UpdateEnabled(DbusmenuMenuitem* aItem, const nsIContent* aContent) {
 | |
|   bool disabled = aContent->AsElement()->AttrValueIs(
 | |
|       kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
 | |
| 
 | |
|   dbusmenu_menuitem_property_set_bool(aItem, DBUSMENU_MENUITEM_PROP_ENABLED,
 | |
|                                       !disabled);
 | |
| }
 | |
| 
 | |
| // we rebuild the dbus model when elements are removed from the DOM,
 | |
| // so this isn't going to trigger for asynchronous
 | |
| static MOZ_CAN_RUN_SCRIPT void DBusActivationCallback(
 | |
|     DbusmenuMenuitem* aMenuitem, guint aTimestamp, gpointer aUserData) {
 | |
|   RefPtr element = static_cast<dom::Element*>(aUserData);
 | |
|   ActivateItem(*element);
 | |
| }
 | |
| 
 | |
| static void ConnectActivated(DbusmenuMenuitem* aItem,
 | |
|                              const dom::Element* aContent) {
 | |
|   g_signal_connect(G_OBJECT(aItem), DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED,
 | |
|                    G_CALLBACK(DBusActivationCallback),
 | |
|                    const_cast<dom::Element*>(aContent));
 | |
| }
 | |
| 
 | |
| static MOZ_CAN_RUN_SCRIPT void DBusAboutToShowCallback(
 | |
|     DbusmenuMenuitem* aMenuitem, gpointer aUserData) {
 | |
|   RefPtr element = static_cast<dom::Element*>(aUserData);
 | |
|   FireEvent(element, eXULPopupShowing);
 | |
|   FireEvent(element, eXULPopupShown);
 | |
| }
 | |
| 
 | |
| static void ConnectAboutToShow(DbusmenuMenuitem* aItem,
 | |
|                                const dom::Element* aContent) {
 | |
|   g_signal_connect(G_OBJECT(aItem), DBUSMENU_MENUITEM_SIGNAL_ABOUT_TO_SHOW,
 | |
|                    G_CALLBACK(DBusAboutToShowCallback),
 | |
|                    const_cast<dom::Element*>(aContent));
 | |
| }
 | |
| 
 | |
| void MenubarModelDBus::AppendMenuItem(DbusmenuMenuitem* aParent,
 | |
|                                       const dom::Element* aChild) {
 | |
|   nsAutoString label;
 | |
|   aChild->GetAttr(nsGkAtoms::label, label);
 | |
|   if (label.IsEmpty()) {
 | |
|     aChild->GetAttr(nsGkAtoms::aria_label, label);
 | |
|   }
 | |
|   RefPtr<DbusmenuMenuitem> child = dont_AddRef(dbusmenu_menuitem_new());
 | |
|   dbusmenu_menuitem_property_set(child, DBUSMENU_MENUITEM_PROP_LABEL,
 | |
|                                  NS_ConvertUTF16toUTF8(label).get());
 | |
|   dbusmenu_menuitem_child_append(aParent, child);
 | |
|   UpdateAccel(child, aChild);
 | |
|   UpdateRadioOrCheck(child, aChild);
 | |
|   UpdateEnabled(child, aChild);
 | |
|   ConnectActivated(child, aChild);
 | |
|   // TODO: icons
 | |
| }
 | |
| 
 | |
| void MenubarModelDBus::AppendSeparator(DbusmenuMenuitem* aParent) {
 | |
|   RefPtr<DbusmenuMenuitem> child = dont_AddRef(dbusmenu_menuitem_new());
 | |
|   dbusmenu_menuitem_property_set(child, DBUSMENU_MENUITEM_PROP_TYPE,
 | |
|                                  "separator");
 | |
|   dbusmenu_menuitem_child_append(aParent, child);
 | |
| }
 | |
| 
 | |
| void MenubarModelDBus::AppendSubmenu(DbusmenuMenuitem* aParent,
 | |
|                                      const dom::Element* aMenu,
 | |
|                                      const dom::Element* aPopup) {
 | |
|   RefPtr<DbusmenuMenuitem> submenu = dont_AddRef(dbusmenu_menuitem_new());
 | |
|   if (RecomputeModelFor(submenu, *aPopup) == 0) {
 | |
|     RefPtr<DbusmenuMenuitem> placeholder = dont_AddRef(dbusmenu_menuitem_new());
 | |
|     dbusmenu_menuitem_child_append(submenu, placeholder);
 | |
|   }
 | |
|   nsAutoString label;
 | |
|   aMenu->GetAttr(nsGkAtoms::label, label);
 | |
|   ConnectAboutToShow(submenu, aPopup);
 | |
|   dbusmenu_menuitem_property_set(submenu, DBUSMENU_MENUITEM_PROP_LABEL,
 | |
|                                  NS_ConvertUTF16toUTF8(label).get());
 | |
|   dbusmenu_menuitem_child_append(aParent, submenu);
 | |
| }
 | |
| 
 | |
| uint MenubarModelDBus::RecomputeModelFor(DbusmenuMenuitem* aParent,
 | |
|                                          const dom::Element& aElement) {
 | |
|   uint childCount = 0;
 | |
|   for (const nsIContent* child = aElement.GetFirstChild(); child;
 | |
|        child = child->GetNextSibling()) {
 | |
|     if (child->IsXULElement(nsGkAtoms::menuitem) &&
 | |
|         !IsDisabled(*child->AsElement())) {
 | |
|       AppendMenuItem(aParent, child->AsElement());
 | |
|       childCount++;
 | |
|       continue;
 | |
|     }
 | |
|     if (child->IsXULElement(nsGkAtoms::menuseparator)) {
 | |
|       AppendSeparator(aParent);
 | |
|       childCount++;
 | |
|       continue;
 | |
|     }
 | |
|     if (child->IsXULElement(nsGkAtoms::menu) &&
 | |
|         !IsDisabled(*child->AsElement())) {
 | |
|       if (const auto* popup = GetMenuPopupChild(*child->AsElement())) {
 | |
|         childCount++;
 | |
|         AppendSubmenu(aParent, child->AsElement(), popup);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   return childCount;
 | |
| }
 | |
| 
 | |
| void DBusMenuBar::NameOwnerChangedCallback(GObject*, GParamSpec*,
 | |
|                                            gpointer user_data) {
 | |
|   static_cast<DBusMenuBar*>(user_data)->OnNameOwnerChanged();
 | |
| }
 | |
| 
 | |
| void DBusMenuBar::OnNameOwnerChanged() {
 | |
|   GUniquePtr<gchar> nameOwner(g_dbus_proxy_get_name_owner(mProxy));
 | |
|   if (!nameOwner) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   RefPtr win = mMenuModel->Element()->OwnerDoc()->GetInnerWindow();
 | |
|   if (NS_WARN_IF(!win)) {
 | |
|     return;
 | |
|   }
 | |
|   nsIWidget* widget = nsGlobalWindowInner::Cast(win.get())->GetNearestWidget();
 | |
|   if (NS_WARN_IF(!widget)) {
 | |
|     return;
 | |
|   }
 | |
|   auto* gdkWin =
 | |
|       static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
 | |
|   if (NS_WARN_IF(!gdkWin)) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
| #  ifdef MOZ_WAYLAND
 | |
|   if (auto* display = widget::WaylandDisplayGet()) {
 | |
|     xdg_dbus_annotation_manager_v1* annotationManager =
 | |
|         display->GetXdgDbusAnnotationManager();
 | |
|     if (NS_WARN_IF(!annotationManager)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWin);
 | |
|     if (NS_WARN_IF(!surface)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     GDBusConnection* connection = g_dbus_proxy_get_connection(mProxy);
 | |
|     const char* myServiceName = g_dbus_connection_get_unique_name(connection);
 | |
|     if (NS_WARN_IF(!myServiceName)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // FIXME(emilio, bug 1883209): Nothing deletes this as of right now.
 | |
|     mAnnotation = xdg_dbus_annotation_manager_v1_create_surface(
 | |
|         annotationManager, "com.canonical.dbusmenu", surface);
 | |
| 
 | |
|     xdg_dbus_annotation_v1_set_address(mAnnotation, myServiceName,
 | |
|                                        mObjectPath.get());
 | |
|     return;
 | |
|   }
 | |
| #  endif
 | |
| #  ifdef MOZ_X11
 | |
|   // legacy path
 | |
|   auto xid = GDK_WINDOW_XID(gdkWin);
 | |
|   widget::DBusProxyCall(mProxy, "RegisterWindow",
 | |
|                         g_variant_new("(uo)", xid, mObjectPath.get()),
 | |
|                         G_DBUS_CALL_FLAGS_NONE)
 | |
|       ->Then(
 | |
|           GetCurrentSerialEventTarget(), __func__,
 | |
|           [self = RefPtr{this}](RefPtr<GVariant>&& aResult) {
 | |
|             self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, true);
 | |
|           },
 | |
|           [self = RefPtr{this}](GUniquePtr<GError>&& aError) {
 | |
|             g_printerr("Failed to register window menubar: %s\n",
 | |
|                        aError->message);
 | |
|             self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, false);
 | |
|           });
 | |
| #  endif
 | |
| }
 | |
| 
 | |
| static unsigned sID = 0;
 | |
| 
 | |
| DBusMenuBar::DBusMenuBar(dom::Element* aElement)
 | |
|     : mObjectPath(nsPrintfCString("/com/canonical/menu/%u", sID++)),
 | |
|       mMenuModel(MakeRefPtr<MenubarModelDBus>(aElement)),
 | |
|       mServer(dont_AddRef(dbusmenu_server_new(mObjectPath.get()))) {
 | |
|   mMenuModel->RecomputeModelIfNeeded();
 | |
|   dbusmenu_server_set_root(mServer.get(), mMenuModel->Root());
 | |
| }
 | |
| 
 | |
| RefPtr<DBusMenuBar> DBusMenuBar::Create(dom::Element* aElement) {
 | |
|   RefPtr<DBusMenuBar> self = new DBusMenuBar(aElement);
 | |
|   widget::CreateDBusProxyForBus(
 | |
|       G_BUS_TYPE_SESSION,
 | |
|       GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
 | |
|                       G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
 | |
|                       G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START),
 | |
|       nullptr, "com.canonical.AppMenu.Registrar",
 | |
|       "/com/canonical/AppMenu/Registrar", "com.canonical.AppMenu.Registrar")
 | |
|       ->Then(
 | |
|           GetCurrentSerialEventTarget(), __func__,
 | |
|           [self](RefPtr<GDBusProxy>&& aProxy) {
 | |
|             self->mProxy = std::move(aProxy);
 | |
|             g_signal_connect(self->mProxy, "notify::g-name-owner",
 | |
|                              G_CALLBACK(NameOwnerChangedCallback), self.get());
 | |
|             self->OnNameOwnerChanged();
 | |
|           },
 | |
|           [](GUniquePtr<GError>&& aError) {
 | |
|             g_printerr("Failed to create DBUS proxy for menubar: %s\n",
 | |
|                        aError->message);
 | |
|           });
 | |
|   return self;
 | |
| }
 | |
| 
 | |
| DBusMenuBar::~DBusMenuBar() = default;
 | |
| #endif
 | |
| 
 | |
| }  // namespace mozilla::widget
 | 
