no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / widget / gtk / NativeMenuGtk.cpp
blob3f8aeb194093f9076b0de1f252544b328ec0bd67
1 /* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "NativeMenuGtk.h"
7 #include "AsyncDBus.h"
8 #include "gdk/gdkkeysyms-compat.h"
9 #include "mozilla/BasicEvents.h"
10 #include "mozilla/dom/Document.h"
11 #include "mozilla/dom/DocumentInlines.h"
12 #include "mozilla/dom/XULCommandEvent.h"
13 #include "mozilla/WidgetUtilsGtk.h"
14 #include "mozilla/EventDispatcher.h"
15 #include "nsPresContext.h"
16 #include "nsIWidget.h"
17 #include "nsWindow.h"
18 #include "nsStubMutationObserver.h"
19 #include "mozilla/dom/Element.h"
20 #include "mozilla/StaticPrefs_widget.h"
21 #include "DBusMenu.h"
22 #include "nsLayoutUtils.h"
23 #include "nsGtkUtils.h"
24 #include "nsGtkKeyUtils.h"
26 #include <dlfcn.h>
27 #include <gtk/gtk.h>
29 namespace mozilla::widget {
31 using GtkMenuPopupAtRect = void (*)(GtkMenu* menu, GdkWindow* rect_window,
32 const GdkRectangle* rect,
33 GdkGravity rect_anchor,
34 GdkGravity menu_anchor,
35 const GdkEvent* trigger_event);
37 static bool IsDisabled(const dom::Element& aElement) {
38 return aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
39 nsGkAtoms::_true, eCaseMatters) ||
40 aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
41 nsGkAtoms::_true, eCaseMatters);
43 static bool NodeIsRelevant(const nsINode& aNode) {
44 return aNode.IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuseparator,
45 nsGkAtoms::menuitem, nsGkAtoms::menugroup,
46 nsGkAtoms::menubar);
49 // If this is a radio / checkbox menuitem, get the current value.
50 static Maybe<bool> GetChecked(const dom::Element& aMenuItem) {
51 static dom::Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox,
52 nsGkAtoms::radio, nullptr};
53 switch (aMenuItem.FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, strings,
54 eCaseMatters)) {
55 case 0:
56 break;
57 case 1:
58 break;
59 default:
60 return Nothing();
63 return Some(aMenuItem.AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
64 nsGkAtoms::_true, eCaseMatters));
67 struct Actions {
68 RefPtr<GSimpleActionGroup> mGroup;
69 size_t mNextActionIndex = 0;
71 nsPrintfCString Register(const dom::Element&, bool aForSubmenu);
72 void Clear();
75 static MOZ_CAN_RUN_SCRIPT void ActivateItem(dom::Element& aElement) {
76 if (Maybe<bool> checked = GetChecked(aElement)) {
77 if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
78 nsGkAtoms::_false, eCaseMatters)) {
79 bool newValue = !*checked;
80 if (newValue) {
81 aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns,
82 true);
83 } else {
84 aElement.UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
89 RefPtr doc = aElement.OwnerDoc();
90 RefPtr event = new dom::XULCommandEvent(doc, doc->GetPresContext(), nullptr);
91 IgnoredErrorResult rv;
92 event->InitCommandEvent(u"command"_ns, true, true,
93 nsGlobalWindowInner::Cast(doc->GetInnerWindow()), 0,
94 /* ctrlKey = */ false, /* altKey = */ false,
95 /* shiftKey = */ false, /* cmdKey = */ false,
96 /* button = */ MouseButton::ePrimary, nullptr, 0, rv);
97 if (MOZ_UNLIKELY(rv.Failed())) {
98 return;
100 aElement.DispatchEvent(*event);
103 static MOZ_CAN_RUN_SCRIPT void ActivateSignal(GSimpleAction* aAction,
104 GVariant* aParam,
105 gpointer aUserData) {
106 RefPtr element = static_cast<dom::Element*>(aUserData);
107 ActivateItem(*element);
110 static MOZ_CAN_RUN_SCRIPT void FireEvent(dom::Element* aTarget,
111 EventMessage aPopupMessage) {
112 nsEventStatus status = nsEventStatus_eIgnore;
113 WidgetMouseEvent event(true, aPopupMessage, nullptr, WidgetMouseEvent::eReal);
114 EventDispatcher::Dispatch(aTarget, nullptr, &event, nullptr, &status);
117 static MOZ_CAN_RUN_SCRIPT void ChangeStateSignal(GSimpleAction* aAction,
118 GVariant* aParam,
119 gpointer aUserData) {
120 // TODO: Fire events when safe. These run at a bad time for now.
121 static constexpr bool kEnabled = false;
122 if (!kEnabled) {
123 return;
125 const bool open = g_variant_get_boolean(aParam);
126 RefPtr popup = static_cast<dom::Element*>(aUserData);
127 if (open) {
128 FireEvent(popup, eXULPopupShowing);
129 FireEvent(popup, eXULPopupShown);
130 } else {
131 FireEvent(popup, eXULPopupHiding);
132 FireEvent(popup, eXULPopupHidden);
136 nsPrintfCString Actions::Register(const dom::Element& aMenuItem,
137 bool aForSubmenu) {
138 nsPrintfCString actionName("item-%zu", mNextActionIndex++);
139 Maybe<bool> paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem);
140 RefPtr<GSimpleAction> action;
141 if (paramValue) {
142 action = dont_AddRef(g_simple_action_new_stateful(
143 actionName.get(), nullptr, g_variant_new_boolean(*paramValue)));
144 } else {
145 action = dont_AddRef(g_simple_action_new(actionName.get(), nullptr));
147 if (aForSubmenu) {
148 g_signal_connect(action, "change-state", G_CALLBACK(ChangeStateSignal),
149 gpointer(&aMenuItem));
150 } else {
151 g_signal_connect(action, "activate", G_CALLBACK(ActivateSignal),
152 gpointer(&aMenuItem));
154 g_action_map_add_action(G_ACTION_MAP(mGroup.get()), G_ACTION(action.get()));
155 return actionName;
158 void Actions::Clear() {
159 for (size_t i = 0; i < mNextActionIndex; ++i) {
160 g_action_map_remove_action(G_ACTION_MAP(mGroup.get()),
161 nsPrintfCString("item-%zu", i).get());
163 mNextActionIndex = 0;
166 class MenuModel : public nsStubMutationObserver {
167 NS_DECL_ISUPPORTS
169 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
170 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
171 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
172 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
174 public:
175 explicit MenuModel(dom::Element* aElement) : mElement(aElement) {
176 mElement->AddMutationObserver(this);
179 dom::Element* Element() { return mElement; }
181 void RecomputeModelIfNeeded() {
182 if (!mDirty) {
183 return;
185 RecomputeModel();
186 mDirty = false;
189 bool IsShowing() { return mShowing; }
190 void WillShow() {
191 mShowing = true;
192 RecomputeModelIfNeeded();
194 void DidHide() { mShowing = false; }
196 protected:
197 virtual void RecomputeModel() = 0;
198 virtual ~MenuModel() { mElement->RemoveMutationObserver(this); }
200 void DirtyModel() {
201 mDirty = true;
202 if (mShowing) {
203 RecomputeModelIfNeeded();
207 RefPtr<dom::Element> mElement;
208 bool mDirty = true;
209 bool mShowing = false;
212 class MenuModelGMenu final : public MenuModel {
213 public:
214 explicit MenuModelGMenu(dom::Element* aElement) : MenuModel(aElement) {
215 mGMenu = dont_AddRef(g_menu_new());
216 mActions.mGroup = dont_AddRef(g_simple_action_group_new());
219 GMenuModel* GetModel() { return G_MENU_MODEL(mGMenu.get()); }
220 GActionGroup* GetActionGroup() {
221 return G_ACTION_GROUP(mActions.mGroup.get());
224 protected:
225 void RecomputeModel() override;
226 static void RecomputeModelFor(GMenu* aMenu, Actions& aActions,
227 const dom::Element& aElement);
229 RefPtr<GMenu> mGMenu;
230 Actions mActions;
233 NS_IMPL_ISUPPORTS(MenuModel, nsIMutationObserver)
235 void MenuModel::ContentRemoved(nsIContent* aChild, nsIContent*) {
236 if (NodeIsRelevant(*aChild)) {
237 DirtyModel();
241 void MenuModel::ContentInserted(nsIContent* aChild) {
242 if (NodeIsRelevant(*aChild)) {
243 DirtyModel();
247 void MenuModel::ContentAppended(nsIContent* aChild) {
248 if (NodeIsRelevant(*aChild)) {
249 DirtyModel();
253 void MenuModel::AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
254 nsAtom* aAttribute, int32_t aModType,
255 const nsAttrValue* aOldValue) {
256 if (NodeIsRelevant(*aElement) &&
257 (aAttribute == nsGkAtoms::label || aAttribute == nsGkAtoms::aria_label ||
258 aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::hidden)) {
259 DirtyModel();
263 static const dom::Element* GetMenuPopupChild(const dom::Element& aElement) {
264 for (const nsIContent* child = aElement.GetFirstChild(); child;
265 child = child->GetNextSibling()) {
266 if (child->IsXULElement(nsGkAtoms::menupopup)) {
267 return child->AsElement();
270 return nullptr;
273 void MenuModelGMenu::RecomputeModelFor(GMenu* aMenu, Actions& aActions,
274 const dom::Element& aElement) {
275 RefPtr<GMenu> sectionMenu;
276 auto FlushSectionMenu = [&] {
277 if (sectionMenu) {
278 g_menu_append_section(aMenu, nullptr, G_MENU_MODEL(sectionMenu.get()));
279 sectionMenu = nullptr;
283 for (const nsIContent* child = aElement.GetFirstChild(); child;
284 child = child->GetNextSibling()) {
285 if (child->IsXULElement(nsGkAtoms::menuitem) &&
286 !IsDisabled(*child->AsElement())) {
287 nsAutoString label;
288 child->AsElement()->GetAttr(nsGkAtoms::label, label);
289 if (label.IsEmpty()) {
290 child->AsElement()->GetAttr(nsGkAtoms::aria_label, label);
292 nsPrintfCString actionName(
293 "menu.%s",
294 aActions.Register(*child->AsElement(), /* aForSubmenu = */ false)
295 .get());
296 g_menu_append(sectionMenu ? sectionMenu.get() : aMenu,
297 NS_ConvertUTF16toUTF8(label).get(), actionName.get());
298 continue;
300 if (child->IsXULElement(nsGkAtoms::menuseparator)) {
301 FlushSectionMenu();
302 sectionMenu = dont_AddRef(g_menu_new());
303 continue;
305 if (child->IsXULElement(nsGkAtoms::menugroup)) {
306 FlushSectionMenu();
307 sectionMenu = dont_AddRef(g_menu_new());
308 RecomputeModelFor(sectionMenu, aActions, *child->AsElement());
309 FlushSectionMenu();
310 continue;
312 if (child->IsXULElement(nsGkAtoms::menu) &&
313 !IsDisabled(*child->AsElement())) {
314 if (const auto* popup = GetMenuPopupChild(*child->AsElement())) {
315 RefPtr<GMenu> submenu = dont_AddRef(g_menu_new());
316 RecomputeModelFor(submenu, aActions, *popup);
317 nsAutoString label;
318 child->AsElement()->GetAttr(nsGkAtoms::label, label);
319 RefPtr<GMenuItem> submenuItem = dont_AddRef(g_menu_item_new_submenu(
320 NS_ConvertUTF16toUTF8(label).get(), G_MENU_MODEL(submenu.get())));
321 nsPrintfCString actionName(
322 "menu.%s",
323 aActions.Register(*popup, /* aForSubmenu = */ true).get());
324 g_menu_item_set_attribute_value(submenuItem.get(), "submenu-action",
325 g_variant_new_string(actionName.get()));
326 g_menu_append_item(sectionMenu ? sectionMenu.get() : aMenu,
327 submenuItem.get());
332 FlushSectionMenu();
335 void MenuModelGMenu::RecomputeModel() {
336 mActions.Clear();
337 g_menu_remove_all(mGMenu.get());
338 RecomputeModelFor(mGMenu.get(), mActions, *mElement);
341 static GtkMenuPopupAtRect GetPopupAtRectFn() {
342 static GtkMenuPopupAtRect sFunc =
343 (GtkMenuPopupAtRect)dlsym(RTLD_DEFAULT, "gtk_menu_popup_at_rect");
344 return sFunc;
347 bool NativeMenuGtk::CanUse() {
348 return StaticPrefs::widget_gtk_native_context_menus() && GetPopupAtRectFn();
351 void NativeMenuGtk::FireEvent(EventMessage aPopupMessage) {
352 RefPtr target = Element();
353 widget::FireEvent(target, aPopupMessage);
356 #define METHOD_SIGNAL(name_) \
357 static MOZ_CAN_RUN_SCRIPT_BOUNDARY void On##name_##Signal( \
358 GtkWidget* widget, gpointer user_data) { \
359 RefPtr menu = static_cast<NativeMenuGtk*>(user_data); \
360 return menu->On##name_(); \
363 METHOD_SIGNAL(Unmap);
365 #undef METHOD_SIGNAL
367 NativeMenuGtk::NativeMenuGtk(dom::Element* aElement)
368 : mMenuModel(MakeRefPtr<MenuModelGMenu>(aElement)) {
369 // Floating, so no need to dont_AddRef.
370 mNativeMenu = gtk_menu_new_from_model(mMenuModel->GetModel());
371 gtk_widget_insert_action_group(mNativeMenu.get(), "menu",
372 mMenuModel->GetActionGroup());
373 g_signal_connect(mNativeMenu, "unmap", G_CALLBACK(OnUnmapSignal), this);
376 NativeMenuGtk::~NativeMenuGtk() {
377 g_signal_handlers_disconnect_by_data(mNativeMenu, this);
380 RefPtr<dom::Element> NativeMenuGtk::Element() { return mMenuModel->Element(); }
382 void NativeMenuGtk::ShowAsContextMenu(nsIFrame* aClickedFrame,
383 const CSSIntPoint& aPosition,
384 bool aIsContextMenu) {
385 if (mMenuModel->IsShowing()) {
386 return;
388 RefPtr<nsIWidget> widget = aClickedFrame->PresContext()->GetRootWidget();
389 if (NS_WARN_IF(!widget)) {
390 // XXX Do we need to close menus here?
391 return;
393 auto* win = static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
394 if (NS_WARN_IF(!win)) {
395 return;
398 auto* geckoWin = static_cast<nsWindow*>(widget.get());
399 // The position needs to be relative to our window.
400 auto pos = (aPosition * aClickedFrame->PresContext()->CSSToDevPixelScale()) -
401 geckoWin->WidgetToScreenOffset();
402 auto gdkPos = geckoWin->DevicePixelsToGdkPointRoundDown(
403 LayoutDeviceIntPoint::Round(pos));
405 mMenuModel->WillShow();
406 const GdkRectangle rect = {gdkPos.x, gdkPos.y, 1, 1};
407 auto openFn = GetPopupAtRectFn();
408 openFn(GTK_MENU(mNativeMenu.get()), win, &rect, GDK_GRAVITY_NORTH_WEST,
409 GDK_GRAVITY_NORTH_WEST, GetLastMousePressEvent());
411 RefPtr pin{this};
412 FireEvent(eXULPopupShown);
415 bool NativeMenuGtk::Close() {
416 if (!mMenuModel->IsShowing()) {
417 return false;
419 gtk_menu_popdown(GTK_MENU(mNativeMenu.get()));
420 return true;
423 void NativeMenuGtk::OnUnmap() {
424 FireEvent(eXULPopupHiding);
426 mMenuModel->DidHide();
428 FireEvent(eXULPopupHidden);
430 for (NativeMenu::Observer* observer : mObservers.Clone()) {
431 observer->OnNativeMenuClosed();
435 void NativeMenuGtk::ActivateItem(dom::Element* aItemElement, Modifiers,
436 int16_t aButton, ErrorResult&) {
437 // TODO: For testing only.
440 void NativeMenuGtk::OpenSubmenu(dom::Element*) {
441 // TODO: For testing mostly.
444 void NativeMenuGtk::CloseSubmenu(dom::Element*) {
445 // TODO: For testing mostly.
448 #ifdef MOZ_ENABLE_DBUS
450 class MenubarModelDBus final : public MenuModel {
451 public:
452 explicit MenubarModelDBus(dom::Element* aElement) : MenuModel(aElement) {
453 mRoot = dont_AddRef(dbusmenu_menuitem_new());
454 dbusmenu_menuitem_set_root(mRoot.get(), true);
455 mShowing = true;
458 DbusmenuMenuitem* Root() const { return mRoot.get(); }
460 protected:
461 void RecomputeModel() override;
462 static void AppendMenuItem(DbusmenuMenuitem* aParent,
463 const dom::Element* aElement);
464 static void AppendSeparator(DbusmenuMenuitem* aParent);
465 static void AppendSubmenu(DbusmenuMenuitem* aParent,
466 const dom::Element* aMenu,
467 const dom::Element* aPopup);
468 static uint RecomputeModelFor(DbusmenuMenuitem* aParent,
469 const dom::Element& aElement);
471 RefPtr<DbusmenuMenuitem> mRoot;
474 void MenubarModelDBus::RecomputeModel() {
475 while (GList* children = dbusmenu_menuitem_get_children(mRoot.get())) {
476 auto* first = static_cast<DbusmenuMenuitem*>(children->data);
477 if (!first) {
478 break;
480 dbusmenu_menuitem_child_delete(mRoot.get(), first);
482 RecomputeModelFor(mRoot, *Element());
485 static const dom::Element* RelevantElementForKeys(
486 const dom::Element* aElement) {
487 nsAutoString key;
488 aElement->GetAttr(nsGkAtoms::key, key);
489 if (!key.IsEmpty()) {
490 dom::Document* document = aElement->OwnerDoc();
491 dom::Element* element = document->GetElementById(key);
492 if (element) {
493 return element;
496 return aElement;
499 static uint32_t ParseKey(const nsAString& aKey, const nsAString& aKeyCode) {
500 guint key = 0;
501 if (!aKey.IsEmpty()) {
502 key = gdk_unicode_to_keyval(*aKey.BeginReading());
505 if (key == 0 && !aKeyCode.IsEmpty()) {
506 key = KeymapWrapper::ConvertGeckoKeyCodeToGDKKeyval(aKeyCode);
509 return key;
512 static uint32_t KeyFrom(const dom::Element* aElement) {
513 const auto* element = RelevantElementForKeys(aElement);
515 nsAutoString key;
516 nsAutoString keycode;
517 element->GetAttr(nsGkAtoms::key, key);
518 element->GetAttr(nsGkAtoms::keycode, keycode);
520 return ParseKey(key, keycode);
523 // TODO(emilio): Unify with nsMenuUtilsX::GeckoModifiersForNodeAttribute (or
524 // at least switch to strtok_r).
525 static uint32_t ParseModifiers(const nsAString& aModifiers) {
526 if (aModifiers.IsEmpty()) {
527 return 0;
530 uint32_t modifier = 0;
531 char* str = ToNewUTF8String(aModifiers);
532 char* token = strtok(str, ", \t");
533 while (token) {
534 if (nsCRT::strcmp(token, "shift") == 0) {
535 modifier |= GDK_SHIFT_MASK;
536 } else if (nsCRT::strcmp(token, "alt") == 0) {
537 modifier |= GDK_MOD1_MASK;
538 } else if (nsCRT::strcmp(token, "meta") == 0) {
539 modifier |= GDK_META_MASK;
540 } else if (nsCRT::strcmp(token, "control") == 0) {
541 modifier |= GDK_CONTROL_MASK;
542 } else if (nsCRT::strcmp(token, "accel") == 0) {
543 auto accel = WidgetInputEvent::AccelModifier();
544 if (accel == MODIFIER_META) {
545 modifier |= GDK_META_MASK;
546 } else if (accel == MODIFIER_ALT) {
547 modifier |= GDK_MOD1_MASK;
548 } else if (accel == MODIFIER_CONTROL) {
549 modifier |= GDK_CONTROL_MASK;
553 token = strtok(nullptr, ", \t");
556 free(str);
558 return modifier;
561 static uint32_t ModifiersFrom(const dom::Element* aContent) {
562 const auto* element = RelevantElementForKeys(aContent);
564 nsAutoString modifiers;
565 element->GetAttr(nsGkAtoms::modifiers, modifiers);
567 return ParseModifiers(modifiers);
570 static void UpdateAccel(DbusmenuMenuitem* aItem, const nsIContent* aContent) {
571 uint32_t key = KeyFrom(aContent->AsElement());
572 if (key != 0) {
573 dbusmenu_menuitem_property_set_shortcut(
574 aItem, key,
575 static_cast<GdkModifierType>(ModifiersFrom(aContent->AsElement())));
579 static void UpdateRadioOrCheck(DbusmenuMenuitem* aItem,
580 const dom::Element* aContent) {
581 static mozilla::dom::Element::AttrValuesArray attrs[] = {
582 nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr};
583 int32_t type = aContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
584 attrs, eCaseMatters);
586 if (type < 0 || type >= 2) {
587 return;
590 if (type == 0) {
591 dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
592 DBUSMENU_MENUITEM_TOGGLE_CHECK);
593 } else {
594 dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
595 DBUSMENU_MENUITEM_TOGGLE_RADIO);
598 bool isChecked = aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
599 nsGkAtoms::_true, eCaseMatters);
600 dbusmenu_menuitem_property_set_int(
601 aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_STATE,
602 isChecked ? DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED
603 : DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED);
606 static void UpdateEnabled(DbusmenuMenuitem* aItem, const nsIContent* aContent) {
607 bool disabled = aContent->AsElement()->AttrValueIs(
608 kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
610 dbusmenu_menuitem_property_set_bool(aItem, DBUSMENU_MENUITEM_PROP_ENABLED,
611 !disabled);
614 // we rebuild the dbus model when elements are removed from the DOM,
615 // so this isn't going to trigger for asynchronous
616 static MOZ_CAN_RUN_SCRIPT void DBusActivationCallback(
617 DbusmenuMenuitem* aMenuitem, guint aTimestamp, gpointer aUserData) {
618 RefPtr element = static_cast<dom::Element*>(aUserData);
619 ActivateItem(*element);
622 static void ConnectActivated(DbusmenuMenuitem* aItem,
623 const dom::Element* aContent) {
624 g_signal_connect(G_OBJECT(aItem), DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED,
625 G_CALLBACK(DBusActivationCallback),
626 const_cast<dom::Element*>(aContent));
629 static MOZ_CAN_RUN_SCRIPT void DBusAboutToShowCallback(
630 DbusmenuMenuitem* aMenuitem, gpointer aUserData) {
631 RefPtr element = static_cast<dom::Element*>(aUserData);
632 FireEvent(element, eXULPopupShowing);
633 FireEvent(element, eXULPopupShown);
636 static void ConnectAboutToShow(DbusmenuMenuitem* aItem,
637 const dom::Element* aContent) {
638 g_signal_connect(G_OBJECT(aItem), DBUSMENU_MENUITEM_SIGNAL_ABOUT_TO_SHOW,
639 G_CALLBACK(DBusAboutToShowCallback),
640 const_cast<dom::Element*>(aContent));
643 void MenubarModelDBus::AppendMenuItem(DbusmenuMenuitem* aParent,
644 const dom::Element* aChild) {
645 nsAutoString label;
646 aChild->GetAttr(nsGkAtoms::label, label);
647 if (label.IsEmpty()) {
648 aChild->GetAttr(nsGkAtoms::aria_label, label);
650 RefPtr<DbusmenuMenuitem> child = dont_AddRef(dbusmenu_menuitem_new());
651 dbusmenu_menuitem_property_set(child, DBUSMENU_MENUITEM_PROP_LABEL,
652 NS_ConvertUTF16toUTF8(label).get());
653 dbusmenu_menuitem_child_append(aParent, child);
654 UpdateAccel(child, aChild);
655 UpdateRadioOrCheck(child, aChild);
656 UpdateEnabled(child, aChild);
657 ConnectActivated(child, aChild);
658 // TODO: icons
661 void MenubarModelDBus::AppendSeparator(DbusmenuMenuitem* aParent) {
662 RefPtr<DbusmenuMenuitem> child = dont_AddRef(dbusmenu_menuitem_new());
663 dbusmenu_menuitem_property_set(child, DBUSMENU_MENUITEM_PROP_TYPE,
664 "separator");
665 dbusmenu_menuitem_child_append(aParent, child);
668 void MenubarModelDBus::AppendSubmenu(DbusmenuMenuitem* aParent,
669 const dom::Element* aMenu,
670 const dom::Element* aPopup) {
671 RefPtr<DbusmenuMenuitem> submenu = dont_AddRef(dbusmenu_menuitem_new());
672 if (RecomputeModelFor(submenu, *aPopup) == 0) {
673 RefPtr<DbusmenuMenuitem> placeholder = dont_AddRef(dbusmenu_menuitem_new());
674 dbusmenu_menuitem_child_append(submenu, placeholder);
676 nsAutoString label;
677 aMenu->GetAttr(nsGkAtoms::label, label);
678 ConnectAboutToShow(submenu, aPopup);
679 dbusmenu_menuitem_property_set(submenu, DBUSMENU_MENUITEM_PROP_LABEL,
680 NS_ConvertUTF16toUTF8(label).get());
681 dbusmenu_menuitem_child_append(aParent, submenu);
684 uint MenubarModelDBus::RecomputeModelFor(DbusmenuMenuitem* aParent,
685 const dom::Element& aElement) {
686 uint childCount = 0;
687 for (const nsIContent* child = aElement.GetFirstChild(); child;
688 child = child->GetNextSibling()) {
689 if (child->IsXULElement(nsGkAtoms::menuitem) &&
690 !IsDisabled(*child->AsElement())) {
691 AppendMenuItem(aParent, child->AsElement());
692 childCount++;
693 continue;
695 if (child->IsXULElement(nsGkAtoms::menuseparator)) {
696 AppendSeparator(aParent);
697 childCount++;
698 continue;
700 if (child->IsXULElement(nsGkAtoms::menu) &&
701 !IsDisabled(*child->AsElement())) {
702 if (const auto* popup = GetMenuPopupChild(*child->AsElement())) {
703 childCount++;
704 AppendSubmenu(aParent, child->AsElement(), popup);
708 return childCount;
711 void DBusMenuBar::NameOwnerChangedCallback(GObject*, GParamSpec*,
712 gpointer user_data) {
713 static_cast<DBusMenuBar*>(user_data)->OnNameOwnerChanged();
716 void DBusMenuBar::OnNameOwnerChanged() {
717 GUniquePtr<gchar> nameOwner(g_dbus_proxy_get_name_owner(mProxy));
718 if (!nameOwner) {
719 return;
722 RefPtr win = mMenuModel->Element()->OwnerDoc()->GetInnerWindow();
723 if (NS_WARN_IF(!win)) {
724 return;
726 nsIWidget* widget = nsGlobalWindowInner::Cast(win.get())->GetNearestWidget();
727 if (NS_WARN_IF(!widget)) {
728 return;
730 auto* gdkWin =
731 static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
732 if (NS_WARN_IF(!gdkWin)) {
733 return;
736 # ifdef MOZ_WAYLAND
737 if (auto* display = widget::WaylandDisplayGet()) {
738 if (!StaticPrefs::widget_gtk_global_menu_wayland_enabled()) {
739 return;
741 xdg_dbus_annotation_manager_v1* annotationManager =
742 display->GetXdgDbusAnnotationManager();
743 if (NS_WARN_IF(!annotationManager)) {
744 return;
747 wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWin);
748 if (NS_WARN_IF(!surface)) {
749 return;
752 GDBusConnection* connection = g_dbus_proxy_get_connection(mProxy);
753 const char* myServiceName = g_dbus_connection_get_unique_name(connection);
754 if (NS_WARN_IF(!myServiceName)) {
755 return;
758 // FIXME(emilio, bug 1883209): Nothing deletes this as of right now.
759 mAnnotation = xdg_dbus_annotation_manager_v1_create_surface(
760 annotationManager, "com.canonical.dbusmenu", surface);
762 xdg_dbus_annotation_v1_set_address(mAnnotation, myServiceName,
763 mObjectPath.get());
764 return;
766 # endif
767 # ifdef MOZ_X11
768 // legacy path
769 auto xid = GDK_WINDOW_XID(gdkWin);
770 widget::DBusProxyCall(mProxy, "RegisterWindow",
771 g_variant_new("(uo)", xid, mObjectPath.get()),
772 G_DBUS_CALL_FLAGS_NONE)
773 ->Then(
774 GetCurrentSerialEventTarget(), __func__,
775 [self = RefPtr{this}](RefPtr<GVariant>&& aResult) {
776 self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, true);
778 [self = RefPtr{this}](GUniquePtr<GError>&& aError) {
779 g_printerr("Failed to register window menubar: %s\n",
780 aError->message);
781 self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, false);
783 # endif
786 static unsigned sID = 0;
788 DBusMenuBar::DBusMenuBar(dom::Element* aElement)
789 : mObjectPath(nsPrintfCString("/com/canonical/menu/%u", sID++)),
790 mMenuModel(MakeRefPtr<MenubarModelDBus>(aElement)),
791 mServer(dont_AddRef(dbusmenu_server_new(mObjectPath.get()))) {
792 mMenuModel->RecomputeModelIfNeeded();
793 dbusmenu_server_set_root(mServer.get(), mMenuModel->Root());
796 RefPtr<DBusMenuBar> DBusMenuBar::Create(dom::Element* aElement) {
797 RefPtr<DBusMenuBar> self = new DBusMenuBar(aElement);
798 widget::CreateDBusProxyForBus(
799 G_BUS_TYPE_SESSION,
800 GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
801 G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
802 G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START),
803 nullptr, "com.canonical.AppMenu.Registrar",
804 "/com/canonical/AppMenu/Registrar", "com.canonical.AppMenu.Registrar")
805 ->Then(
806 GetCurrentSerialEventTarget(), __func__,
807 [self](RefPtr<GDBusProxy>&& aProxy) {
808 self->mProxy = std::move(aProxy);
809 g_signal_connect(self->mProxy, "notify::g-name-owner",
810 G_CALLBACK(NameOwnerChangedCallback), self.get());
811 self->OnNameOwnerChanged();
813 [](GUniquePtr<GError>&& aError) {
814 g_printerr("Failed to create DBUS proxy for menubar: %s\n",
815 aError->message);
817 return self;
820 DBusMenuBar::~DBusMenuBar() {
821 # ifdef MOZ_WAYLAND
822 MozClearPointer(mAnnotation, xdg_dbus_annotation_v1_destroy);
823 # endif
825 #endif
827 } // namespace mozilla::widget