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"
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"
18 #include "nsStubMutationObserver.h"
19 #include "mozilla/dom/Element.h"
20 #include "mozilla/StaticPrefs_widget.h"
22 #include "nsLayoutUtils.h"
23 #include "nsGtkUtils.h"
24 #include "nsGtkKeyUtils.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
,
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
,
63 return Some(aMenuItem
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::checked
,
64 nsGkAtoms::_true
, eCaseMatters
));
68 RefPtr
<GSimpleActionGroup
> mGroup
;
69 size_t mNextActionIndex
= 0;
71 nsPrintfCString
Register(const dom::Element
&, bool aForSubmenu
);
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
;
81 aElement
.SetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, u
"true"_ns
,
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())) {
100 aElement
.DispatchEvent(*event
);
103 static MOZ_CAN_RUN_SCRIPT
void ActivateSignal(GSimpleAction
* aAction
,
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
,
119 gpointer aUserData
) {
120 // TODO: Fire events when safe. These run at a bad time for now.
121 static constexpr bool kEnabled
= false;
125 const bool open
= g_variant_get_boolean(aParam
);
126 RefPtr popup
= static_cast<dom::Element
*>(aUserData
);
128 FireEvent(popup
, eXULPopupShowing
);
129 FireEvent(popup
, eXULPopupShown
);
131 FireEvent(popup
, eXULPopupHiding
);
132 FireEvent(popup
, eXULPopupHidden
);
136 nsPrintfCString
Actions::Register(const dom::Element
& aMenuItem
,
138 nsPrintfCString
actionName("item-%zu", mNextActionIndex
++);
139 Maybe
<bool> paramValue
= aForSubmenu
? Some(false) : GetChecked(aMenuItem
);
140 RefPtr
<GSimpleAction
> action
;
142 action
= dont_AddRef(g_simple_action_new_stateful(
143 actionName
.get(), nullptr, g_variant_new_boolean(*paramValue
)));
145 action
= dont_AddRef(g_simple_action_new(actionName
.get(), nullptr));
148 g_signal_connect(action
, "change-state", G_CALLBACK(ChangeStateSignal
),
149 gpointer(&aMenuItem
));
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()));
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
{
169 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
170 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
171 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
172 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
175 explicit MenuModel(dom::Element
* aElement
) : mElement(aElement
) {
176 mElement
->AddMutationObserver(this);
179 dom::Element
* Element() { return mElement
; }
181 void RecomputeModelIfNeeded() {
189 bool IsShowing() { return mShowing
; }
192 RecomputeModelIfNeeded();
194 void DidHide() { mShowing
= false; }
197 virtual void RecomputeModel() = 0;
198 virtual ~MenuModel() { mElement
->RemoveMutationObserver(this); }
203 RecomputeModelIfNeeded();
207 RefPtr
<dom::Element
> mElement
;
209 bool mShowing
= false;
212 class MenuModelGMenu final
: public MenuModel
{
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());
225 void RecomputeModel() override
;
226 static void RecomputeModelFor(GMenu
* aMenu
, Actions
& aActions
,
227 const dom::Element
& aElement
);
229 RefPtr
<GMenu
> mGMenu
;
233 NS_IMPL_ISUPPORTS(MenuModel
, nsIMutationObserver
)
235 void MenuModel::ContentRemoved(nsIContent
* aChild
, nsIContent
*) {
236 if (NodeIsRelevant(*aChild
)) {
241 void MenuModel::ContentInserted(nsIContent
* aChild
) {
242 if (NodeIsRelevant(*aChild
)) {
247 void MenuModel::ContentAppended(nsIContent
* aChild
) {
248 if (NodeIsRelevant(*aChild
)) {
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
)) {
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();
273 void MenuModelGMenu::RecomputeModelFor(GMenu
* aMenu
, Actions
& aActions
,
274 const dom::Element
& aElement
) {
275 RefPtr
<GMenu
> sectionMenu
;
276 auto FlushSectionMenu
= [&] {
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())) {
288 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
289 if (label
.IsEmpty()) {
290 child
->AsElement()->GetAttr(nsGkAtoms::aria_label
, label
);
292 nsPrintfCString
actionName(
294 aActions
.Register(*child
->AsElement(), /* aForSubmenu = */ false)
296 g_menu_append(sectionMenu
? sectionMenu
.get() : aMenu
,
297 NS_ConvertUTF16toUTF8(label
).get(), actionName
.get());
300 if (child
->IsXULElement(nsGkAtoms::menuseparator
)) {
302 sectionMenu
= dont_AddRef(g_menu_new());
305 if (child
->IsXULElement(nsGkAtoms::menugroup
)) {
307 sectionMenu
= dont_AddRef(g_menu_new());
308 RecomputeModelFor(sectionMenu
, aActions
, *child
->AsElement());
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
);
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(
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
,
335 void MenuModelGMenu::RecomputeModel() {
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");
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
);
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()) {
388 RefPtr
<nsIWidget
> widget
= aClickedFrame
->PresContext()->GetRootWidget();
389 if (NS_WARN_IF(!widget
)) {
390 // XXX Do we need to close menus here?
393 auto* win
= static_cast<GdkWindow
*>(widget
->GetNativeData(NS_NATIVE_WINDOW
));
394 if (NS_WARN_IF(!win
)) {
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());
412 FireEvent(eXULPopupShown
);
415 bool NativeMenuGtk::Close() {
416 if (!mMenuModel
->IsShowing()) {
419 gtk_menu_popdown(GTK_MENU(mNativeMenu
.get()));
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
{
452 explicit MenubarModelDBus(dom::Element
* aElement
) : MenuModel(aElement
) {
453 mRoot
= dont_AddRef(dbusmenu_menuitem_new());
454 dbusmenu_menuitem_set_root(mRoot
.get(), true);
458 DbusmenuMenuitem
* Root() const { return mRoot
.get(); }
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
);
480 dbusmenu_menuitem_child_delete(mRoot
.get(), first
);
482 RecomputeModelFor(mRoot
, *Element());
485 static const dom::Element
* RelevantElementForKeys(
486 const dom::Element
* aElement
) {
488 aElement
->GetAttr(nsGkAtoms::key
, key
);
489 if (!key
.IsEmpty()) {
490 dom::Document
* document
= aElement
->OwnerDoc();
491 dom::Element
* element
= document
->GetElementById(key
);
499 static uint32_t ParseKey(const nsAString
& aKey
, const nsAString
& aKeyCode
) {
501 if (!aKey
.IsEmpty()) {
502 key
= gdk_unicode_to_keyval(*aKey
.BeginReading());
505 if (key
== 0 && !aKeyCode
.IsEmpty()) {
506 key
= KeymapWrapper::ConvertGeckoKeyCodeToGDKKeyval(aKeyCode
);
512 static uint32_t KeyFrom(const dom::Element
* aElement
) {
513 const auto* element
= RelevantElementForKeys(aElement
);
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()) {
530 uint32_t modifier
= 0;
531 char* str
= ToNewUTF8String(aModifiers
);
532 char* token
= strtok(str
, ", \t");
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");
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());
573 dbusmenu_menuitem_property_set_shortcut(
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) {
591 dbusmenu_menuitem_property_set(aItem
, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE
,
592 DBUSMENU_MENUITEM_TOGGLE_CHECK
);
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
,
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
) {
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
);
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
,
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
);
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
) {
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());
695 if (child
->IsXULElement(nsGkAtoms::menuseparator
)) {
696 AppendSeparator(aParent
);
700 if (child
->IsXULElement(nsGkAtoms::menu
) &&
701 !IsDisabled(*child
->AsElement())) {
702 if (const auto* popup
= GetMenuPopupChild(*child
->AsElement())) {
704 AppendSubmenu(aParent
, child
->AsElement(), popup
);
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
));
722 RefPtr win
= mMenuModel
->Element()->OwnerDoc()->GetInnerWindow();
723 if (NS_WARN_IF(!win
)) {
726 nsIWidget
* widget
= nsGlobalWindowInner::Cast(win
.get())->GetNearestWidget();
727 if (NS_WARN_IF(!widget
)) {
731 static_cast<GdkWindow
*>(widget
->GetNativeData(NS_NATIVE_WINDOW
));
732 if (NS_WARN_IF(!gdkWin
)) {
737 if (auto* display
= widget::WaylandDisplayGet()) {
738 if (!StaticPrefs::widget_gtk_global_menu_wayland_enabled()) {
741 xdg_dbus_annotation_manager_v1
* annotationManager
=
742 display
->GetXdgDbusAnnotationManager();
743 if (NS_WARN_IF(!annotationManager
)) {
747 wl_surface
* surface
= gdk_wayland_window_get_wl_surface(gdkWin
);
748 if (NS_WARN_IF(!surface
)) {
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
)) {
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
,
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
)
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",
781 self
->mMenuModel
->Element()->SetBoolAttr(nsGkAtoms::hidden
, false);
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(
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")
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",
820 DBusMenuBar::~DBusMenuBar() {
822 MozClearPointer(mAnnotation
, xdg_dbus_annotation_v1_destroy
);
827 } // namespace mozilla::widget