/* -*- 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 "nsGtkUtils.h" #include "nsGtkKeyUtils.h" #include #include 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 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 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 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(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(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 paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem); RefPtr 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 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 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 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 submenu = dont_AddRef(g_menu_new()); RecomputeModelFor(submenu, aActions, *popup); nsAutoString label; child->AsElement()->GetAttr(nsGkAtoms::label, label); RefPtr 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(user_data); \ return menu->On##name_(); \ } METHOD_SIGNAL(Unmap); #undef METHOD_SIGNAL NativeMenuGtk::NativeMenuGtk(dom::Element* aElement) : mMenuModel(MakeRefPtr(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 NativeMenuGtk::Element() { return mMenuModel->Element(); } void NativeMenuGtk::ShowAsContextMenu(nsIFrame* aClickedFrame, const CSSIntPoint& aPosition, bool aIsContextMenu) { if (mMenuModel->IsShowing()) { return; } RefPtr widget = aClickedFrame->PresContext()->GetRootWidget(); if (NS_WARN_IF(!widget)) { // XXX Do we need to close menus here? return; } auto* win = static_cast(widget->GetNativeData(NS_NATIVE_WINDOW)); if (NS_WARN_IF(!win)) { return; } auto* geckoWin = static_cast(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 mRoot; }; void MenubarModelDBus::RecomputeModel() { while (GList* children = dbusmenu_menuitem_get_children(mRoot.get())) { auto* first = static_cast(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(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(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(aContent)); } static MOZ_CAN_RUN_SCRIPT void DBusAboutToShowCallback( DbusmenuMenuitem* aMenuitem, gpointer aUserData) { RefPtr element = static_cast(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(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 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 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 submenu = dont_AddRef(dbusmenu_menuitem_new()); if (RecomputeModelFor(submenu, *aPopup) == 0) { RefPtr 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(user_data)->OnNameOwnerChanged(); } void DBusMenuBar::OnNameOwnerChanged() { GUniquePtr 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(widget->GetNativeData(NS_NATIVE_WINDOW)); if (NS_WARN_IF(!gdkWin)) { return; } # ifdef MOZ_WAYLAND if (auto* display = widget::WaylandDisplayGet()) { if (!StaticPrefs::widget_gtk_global_menu_wayland_enabled()) { return; } 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&& aResult) { self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, true); }, [self = RefPtr{this}](GUniquePtr&& 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(aElement)), mServer(dont_AddRef(dbusmenu_server_new(mObjectPath.get()))) { mMenuModel->RecomputeModelIfNeeded(); dbusmenu_server_set_root(mServer.get(), mMenuModel->Root()); } RefPtr DBusMenuBar::Create(dom::Element* aElement) { RefPtr 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&& aProxy) { self->mProxy = std::move(aProxy); g_signal_connect(self->mProxy, "notify::g-name-owner", G_CALLBACK(NameOwnerChangedCallback), self.get()); self->OnNameOwnerChanged(); }, [](GUniquePtr&& aError) { g_printerr("Failed to create DBUS proxy for menubar: %s\n", aError->message); }); return self; } DBusMenuBar::~DBusMenuBar() { # ifdef MOZ_WAYLAND MozClearPointer(mAnnotation, xdg_dbus_annotation_v1_destroy); # endif } #endif } // namespace mozilla::widget