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"
30 # include "nsWaylandDisplay.h"
33 namespace mozilla::widget
{
35 using GtkMenuPopupAtRect
= void (*)(GtkMenu
* menu
, GdkWindow
* rect_window
,
36 const GdkRectangle
* rect
,
37 GdkGravity rect_anchor
,
38 GdkGravity menu_anchor
,
39 const GdkEvent
* trigger_event
);
41 static bool IsDisabled(const dom::Element
& aElement
) {
42 return aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::disabled
,
43 nsGkAtoms::_true
, eCaseMatters
) ||
44 aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::hidden
,
45 nsGkAtoms::_true
, eCaseMatters
);
47 static bool NodeIsRelevant(const nsINode
& aNode
) {
48 return aNode
.IsAnyOfXULElements(nsGkAtoms::menu
, nsGkAtoms::menuseparator
,
49 nsGkAtoms::menuitem
, nsGkAtoms::menugroup
,
53 // If this is a radio / checkbox menuitem, get the current value.
54 static Maybe
<bool> GetChecked(const dom::Element
& aMenuItem
) {
55 static dom::Element::AttrValuesArray strings
[] = {nsGkAtoms::checkbox
,
56 nsGkAtoms::radio
, nullptr};
57 switch (aMenuItem
.FindAttrValueIn(kNameSpaceID_None
, nsGkAtoms::type
, strings
,
67 return Some(aMenuItem
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::checked
,
68 nsGkAtoms::_true
, eCaseMatters
));
72 RefPtr
<GSimpleActionGroup
> mGroup
;
73 size_t mNextActionIndex
= 0;
75 nsPrintfCString
Register(const dom::Element
&, bool aForSubmenu
);
79 static MOZ_CAN_RUN_SCRIPT
void ActivateItem(dom::Element
& aElement
) {
80 if (Maybe
<bool> checked
= GetChecked(aElement
)) {
81 if (!aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::autocheck
,
82 nsGkAtoms::_false
, eCaseMatters
)) {
83 bool newValue
= !*checked
;
85 aElement
.SetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, u
"true"_ns
,
88 aElement
.UnsetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, true);
93 RefPtr doc
= aElement
.OwnerDoc();
94 RefPtr event
= new dom::XULCommandEvent(doc
, doc
->GetPresContext(), nullptr);
95 IgnoredErrorResult rv
;
96 event
->InitCommandEvent(u
"command"_ns
, true, true,
97 nsGlobalWindowInner::Cast(doc
->GetInnerWindow()), 0,
98 /* ctrlKey = */ false, /* altKey = */ false,
99 /* shiftKey = */ false, /* cmdKey = */ false,
100 /* button = */ MouseButton::ePrimary
, nullptr, 0, rv
);
101 if (MOZ_UNLIKELY(rv
.Failed())) {
104 aElement
.DispatchEvent(*event
);
107 static MOZ_CAN_RUN_SCRIPT
void ActivateSignal(GSimpleAction
* aAction
,
109 gpointer aUserData
) {
110 RefPtr element
= static_cast<dom::Element
*>(aUserData
);
111 ActivateItem(*element
);
114 static MOZ_CAN_RUN_SCRIPT
void FireEvent(dom::Element
* aTarget
,
115 EventMessage aPopupMessage
) {
116 nsEventStatus status
= nsEventStatus_eIgnore
;
117 WidgetMouseEvent
event(true, aPopupMessage
, nullptr, WidgetMouseEvent::eReal
);
118 EventDispatcher::Dispatch(aTarget
, nullptr, &event
, nullptr, &status
);
121 static MOZ_CAN_RUN_SCRIPT
void ChangeStateSignal(GSimpleAction
* aAction
,
123 gpointer aUserData
) {
124 // TODO: Fire events when safe. These run at a bad time for now.
125 static constexpr bool kEnabled
= false;
129 const bool open
= g_variant_get_boolean(aParam
);
130 RefPtr popup
= static_cast<dom::Element
*>(aUserData
);
132 FireEvent(popup
, eXULPopupShowing
);
133 FireEvent(popup
, eXULPopupShown
);
135 FireEvent(popup
, eXULPopupHiding
);
136 FireEvent(popup
, eXULPopupHidden
);
140 nsPrintfCString
Actions::Register(const dom::Element
& aMenuItem
,
142 nsPrintfCString
actionName("item-%zu", mNextActionIndex
++);
143 Maybe
<bool> paramValue
= aForSubmenu
? Some(false) : GetChecked(aMenuItem
);
144 RefPtr
<GSimpleAction
> action
;
146 action
= dont_AddRef(g_simple_action_new_stateful(
147 actionName
.get(), nullptr, g_variant_new_boolean(*paramValue
)));
149 action
= dont_AddRef(g_simple_action_new(actionName
.get(), nullptr));
152 g_signal_connect(action
, "change-state", G_CALLBACK(ChangeStateSignal
),
153 gpointer(&aMenuItem
));
155 g_signal_connect(action
, "activate", G_CALLBACK(ActivateSignal
),
156 gpointer(&aMenuItem
));
158 g_action_map_add_action(G_ACTION_MAP(mGroup
.get()), G_ACTION(action
.get()));
162 void Actions::Clear() {
163 for (size_t i
= 0; i
< mNextActionIndex
; ++i
) {
164 g_action_map_remove_action(G_ACTION_MAP(mGroup
.get()),
165 nsPrintfCString("item-%zu", i
).get());
167 mNextActionIndex
= 0;
170 class MenuModel
: public nsStubMutationObserver
{
173 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
174 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
175 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
176 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
179 explicit MenuModel(dom::Element
* aElement
) : mElement(aElement
) {
180 mElement
->AddMutationObserver(this);
183 dom::Element
* Element() { return mElement
; }
185 void RecomputeModelIfNeeded() {
193 bool IsShowing() { return mShowing
; }
196 RecomputeModelIfNeeded();
198 void DidHide() { mShowing
= false; }
201 virtual void RecomputeModel() = 0;
202 virtual ~MenuModel() { mElement
->RemoveMutationObserver(this); }
207 RecomputeModelIfNeeded();
211 RefPtr
<dom::Element
> mElement
;
213 bool mShowing
= false;
216 class MenuModelGMenu final
: public MenuModel
{
218 explicit MenuModelGMenu(dom::Element
* aElement
) : MenuModel(aElement
) {
219 mGMenu
= dont_AddRef(g_menu_new());
220 mActions
.mGroup
= dont_AddRef(g_simple_action_group_new());
223 GMenuModel
* GetModel() { return G_MENU_MODEL(mGMenu
.get()); }
224 GActionGroup
* GetActionGroup() {
225 return G_ACTION_GROUP(mActions
.mGroup
.get());
229 void RecomputeModel() override
;
230 static void RecomputeModelFor(GMenu
* aMenu
, Actions
& aActions
,
231 const dom::Element
& aElement
);
233 RefPtr
<GMenu
> mGMenu
;
237 NS_IMPL_ISUPPORTS(MenuModel
, nsIMutationObserver
)
239 void MenuModel::ContentWillBeRemoved(nsIContent
* aChild
,
240 const BatchRemovalState
* aState
) {
241 if (NodeIsRelevant(*aChild
)) {
242 nsContentUtils::AddScriptRunner(NewRunnableMethod(
243 "MenuModel::ContentWillBeRemoved", this, &MenuModel::DirtyModel
));
247 void MenuModel::ContentInserted(nsIContent
* aChild
) {
248 if (NodeIsRelevant(*aChild
)) {
253 void MenuModel::ContentAppended(nsIContent
* aChild
) {
254 if (NodeIsRelevant(*aChild
)) {
259 void MenuModel::AttributeChanged(dom::Element
* aElement
, int32_t aNameSpaceID
,
260 nsAtom
* aAttribute
, int32_t aModType
,
261 const nsAttrValue
* aOldValue
) {
262 if (NodeIsRelevant(*aElement
) &&
263 (aAttribute
== nsGkAtoms::label
|| aAttribute
== nsGkAtoms::aria_label
||
264 aAttribute
== nsGkAtoms::disabled
|| aAttribute
== nsGkAtoms::hidden
)) {
269 static const dom::Element
* GetMenuPopupChild(const dom::Element
& aElement
) {
270 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
271 child
= child
->GetNextSibling()) {
272 if (child
->IsXULElement(nsGkAtoms::menupopup
)) {
273 return child
->AsElement();
279 void MenuModelGMenu::RecomputeModelFor(GMenu
* aMenu
, Actions
& aActions
,
280 const dom::Element
& aElement
) {
281 RefPtr
<GMenu
> sectionMenu
;
282 auto FlushSectionMenu
= [&] {
284 g_menu_append_section(aMenu
, nullptr, G_MENU_MODEL(sectionMenu
.get()));
285 sectionMenu
= nullptr;
289 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
290 child
= child
->GetNextSibling()) {
291 if (child
->IsXULElement(nsGkAtoms::menuitem
) &&
292 !IsDisabled(*child
->AsElement())) {
294 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
295 if (label
.IsEmpty()) {
296 child
->AsElement()->GetAttr(nsGkAtoms::aria_label
, label
);
298 nsPrintfCString
actionName(
300 aActions
.Register(*child
->AsElement(), /* aForSubmenu = */ false)
302 g_menu_append(sectionMenu
? sectionMenu
.get() : aMenu
,
303 NS_ConvertUTF16toUTF8(label
).get(), actionName
.get());
306 if (child
->IsXULElement(nsGkAtoms::menuseparator
)) {
308 sectionMenu
= dont_AddRef(g_menu_new());
311 if (child
->IsXULElement(nsGkAtoms::menugroup
)) {
313 sectionMenu
= dont_AddRef(g_menu_new());
314 RecomputeModelFor(sectionMenu
, aActions
, *child
->AsElement());
318 if (child
->IsXULElement(nsGkAtoms::menu
) &&
319 !IsDisabled(*child
->AsElement())) {
320 if (const auto* popup
= GetMenuPopupChild(*child
->AsElement())) {
321 RefPtr
<GMenu
> submenu
= dont_AddRef(g_menu_new());
322 RecomputeModelFor(submenu
, aActions
, *popup
);
324 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
325 RefPtr
<GMenuItem
> submenuItem
= dont_AddRef(g_menu_item_new_submenu(
326 NS_ConvertUTF16toUTF8(label
).get(), G_MENU_MODEL(submenu
.get())));
327 nsPrintfCString
actionName(
329 aActions
.Register(*popup
, /* aForSubmenu = */ true).get());
330 g_menu_item_set_attribute_value(submenuItem
.get(), "submenu-action",
331 g_variant_new_string(actionName
.get()));
332 g_menu_append_item(sectionMenu
? sectionMenu
.get() : aMenu
,
341 void MenuModelGMenu::RecomputeModel() {
343 g_menu_remove_all(mGMenu
.get());
344 RecomputeModelFor(mGMenu
.get(), mActions
, *mElement
);
347 static GtkMenuPopupAtRect
GetPopupAtRectFn() {
348 static GtkMenuPopupAtRect sFunc
=
349 (GtkMenuPopupAtRect
)dlsym(RTLD_DEFAULT
, "gtk_menu_popup_at_rect");
353 bool NativeMenuGtk::CanUse() {
354 return StaticPrefs::widget_gtk_native_context_menus() && GetPopupAtRectFn();
357 void NativeMenuGtk::FireEvent(EventMessage aPopupMessage
) {
358 RefPtr target
= Element();
359 widget::FireEvent(target
, aPopupMessage
);
362 #define METHOD_SIGNAL(name_) \
363 static MOZ_CAN_RUN_SCRIPT_BOUNDARY void On##name_##Signal( \
364 GtkWidget* widget, gpointer user_data) { \
365 RefPtr menu = static_cast<NativeMenuGtk*>(user_data); \
366 return menu->On##name_(); \
369 METHOD_SIGNAL(Unmap
);
373 NativeMenuGtk::NativeMenuGtk(dom::Element
* aElement
)
374 : mMenuModel(MakeRefPtr
<MenuModelGMenu
>(aElement
)) {
375 // Floating, so no need to dont_AddRef.
376 mNativeMenu
= gtk_menu_new_from_model(mMenuModel
->GetModel());
377 gtk_widget_insert_action_group(mNativeMenu
.get(), "menu",
378 mMenuModel
->GetActionGroup());
379 g_signal_connect(mNativeMenu
, "unmap", G_CALLBACK(OnUnmapSignal
), this);
382 NativeMenuGtk::~NativeMenuGtk() {
383 g_signal_handlers_disconnect_by_data(mNativeMenu
, this);
386 RefPtr
<dom::Element
> NativeMenuGtk::Element() { return mMenuModel
->Element(); }
388 void NativeMenuGtk::ShowAsContextMenu(nsIFrame
* aClickedFrame
,
389 const CSSIntPoint
& aPosition
,
390 bool aIsContextMenu
) {
391 if (mMenuModel
->IsShowing()) {
394 RefPtr
<nsIWidget
> widget
= aClickedFrame
->PresContext()->GetRootWidget();
395 if (NS_WARN_IF(!widget
)) {
396 // XXX Do we need to close menus here?
399 auto* win
= static_cast<GdkWindow
*>(widget
->GetNativeData(NS_NATIVE_WINDOW
));
400 if (NS_WARN_IF(!win
)) {
404 auto* geckoWin
= static_cast<nsWindow
*>(widget
.get());
405 // The position needs to be relative to our window.
406 auto pos
= (aPosition
* aClickedFrame
->PresContext()->CSSToDevPixelScale()) -
407 geckoWin
->WidgetToScreenOffset();
408 auto gdkPos
= geckoWin
->DevicePixelsToGdkPointRoundDown(
409 LayoutDeviceIntPoint::Round(pos
));
411 mMenuModel
->WillShow();
412 const GdkRectangle rect
= {gdkPos
.x
, gdkPos
.y
, 1, 1};
413 auto openFn
= GetPopupAtRectFn();
414 openFn(GTK_MENU(mNativeMenu
.get()), win
, &rect
, GDK_GRAVITY_NORTH_WEST
,
415 GDK_GRAVITY_NORTH_WEST
, GetLastMousePressEvent());
418 FireEvent(eXULPopupShown
);
421 bool NativeMenuGtk::Close() {
422 if (!mMenuModel
->IsShowing()) {
425 gtk_menu_popdown(GTK_MENU(mNativeMenu
.get()));
429 void NativeMenuGtk::OnUnmap() {
430 FireEvent(eXULPopupHiding
);
432 mMenuModel
->DidHide();
434 FireEvent(eXULPopupHidden
);
436 for (NativeMenu::Observer
* observer
: mObservers
.Clone()) {
437 observer
->OnNativeMenuClosed();
441 void NativeMenuGtk::ActivateItem(dom::Element
* aItemElement
, Modifiers
,
442 int16_t aButton
, ErrorResult
&) {
443 // TODO: For testing only.
446 void NativeMenuGtk::OpenSubmenu(dom::Element
*) {
447 // TODO: For testing mostly.
450 void NativeMenuGtk::CloseSubmenu(dom::Element
*) {
451 // TODO: For testing mostly.
454 #ifdef MOZ_ENABLE_DBUS
456 class MenubarModelDBus final
: public MenuModel
{
458 explicit MenubarModelDBus(dom::Element
* aElement
) : MenuModel(aElement
) {
459 mRoot
= dont_AddRef(dbusmenu_menuitem_new());
460 dbusmenu_menuitem_set_root(mRoot
.get(), true);
464 DbusmenuMenuitem
* Root() const { return mRoot
.get(); }
467 void RecomputeModel() override
;
468 static void AppendMenuItem(DbusmenuMenuitem
* aParent
,
469 const dom::Element
* aElement
);
470 static void AppendSeparator(DbusmenuMenuitem
* aParent
);
471 static void AppendSubmenu(DbusmenuMenuitem
* aParent
,
472 const dom::Element
* aMenu
,
473 const dom::Element
* aPopup
);
474 static uint
RecomputeModelFor(DbusmenuMenuitem
* aParent
,
475 const dom::Element
& aElement
);
477 RefPtr
<DbusmenuMenuitem
> mRoot
;
480 void MenubarModelDBus::RecomputeModel() {
481 while (GList
* children
= dbusmenu_menuitem_get_children(mRoot
.get())) {
482 auto* first
= static_cast<DbusmenuMenuitem
*>(children
->data
);
486 dbusmenu_menuitem_child_delete(mRoot
.get(), first
);
488 RecomputeModelFor(mRoot
, *Element());
491 static const dom::Element
* RelevantElementForKeys(
492 const dom::Element
* aElement
) {
494 aElement
->GetAttr(nsGkAtoms::key
, key
);
495 if (!key
.IsEmpty()) {
496 dom::Document
* document
= aElement
->OwnerDoc();
497 dom::Element
* element
= document
->GetElementById(key
);
505 static uint32_t ParseKey(const nsAString
& aKey
, const nsAString
& aKeyCode
) {
507 if (!aKey
.IsEmpty()) {
508 key
= gdk_unicode_to_keyval(*aKey
.BeginReading());
511 if (key
== 0 && !aKeyCode
.IsEmpty()) {
512 key
= KeymapWrapper::ConvertGeckoKeyCodeToGDKKeyval(aKeyCode
);
518 static uint32_t KeyFrom(const dom::Element
* aElement
) {
519 const auto* element
= RelevantElementForKeys(aElement
);
522 nsAutoString keycode
;
523 element
->GetAttr(nsGkAtoms::key
, key
);
524 element
->GetAttr(nsGkAtoms::keycode
, keycode
);
526 return ParseKey(key
, keycode
);
529 // TODO(emilio): Unify with nsMenuUtilsX::GeckoModifiersForNodeAttribute (or
530 // at least switch to strtok_r).
531 static uint32_t ParseModifiers(const nsAString
& aModifiers
) {
532 if (aModifiers
.IsEmpty()) {
536 uint32_t modifier
= 0;
537 char* str
= ToNewUTF8String(aModifiers
);
538 char* token
= strtok(str
, ", \t");
540 if (nsCRT::strcmp(token
, "shift") == 0) {
541 modifier
|= GDK_SHIFT_MASK
;
542 } else if (nsCRT::strcmp(token
, "alt") == 0) {
543 modifier
|= GDK_MOD1_MASK
;
544 } else if (nsCRT::strcmp(token
, "meta") == 0) {
545 modifier
|= GDK_META_MASK
;
546 } else if (nsCRT::strcmp(token
, "control") == 0) {
547 modifier
|= GDK_CONTROL_MASK
;
548 } else if (nsCRT::strcmp(token
, "accel") == 0) {
549 auto accel
= WidgetInputEvent::AccelModifier();
550 if (accel
== MODIFIER_META
) {
551 modifier
|= GDK_META_MASK
;
552 } else if (accel
== MODIFIER_ALT
) {
553 modifier
|= GDK_MOD1_MASK
;
554 } else if (accel
== MODIFIER_CONTROL
) {
555 modifier
|= GDK_CONTROL_MASK
;
559 token
= strtok(nullptr, ", \t");
567 static uint32_t ModifiersFrom(const dom::Element
* aContent
) {
568 const auto* element
= RelevantElementForKeys(aContent
);
570 nsAutoString modifiers
;
571 element
->GetAttr(nsGkAtoms::modifiers
, modifiers
);
573 return ParseModifiers(modifiers
);
576 static void UpdateAccel(DbusmenuMenuitem
* aItem
, const nsIContent
* aContent
) {
577 uint32_t key
= KeyFrom(aContent
->AsElement());
579 dbusmenu_menuitem_property_set_shortcut(
581 static_cast<GdkModifierType
>(ModifiersFrom(aContent
->AsElement())));
585 static void UpdateRadioOrCheck(DbusmenuMenuitem
* aItem
,
586 const dom::Element
* aContent
) {
587 static mozilla::dom::Element::AttrValuesArray attrs
[] = {
588 nsGkAtoms::checkbox
, nsGkAtoms::radio
, nullptr};
589 int32_t type
= aContent
->FindAttrValueIn(kNameSpaceID_None
, nsGkAtoms::type
,
590 attrs
, eCaseMatters
);
592 if (type
< 0 || type
>= 2) {
597 dbusmenu_menuitem_property_set(aItem
, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE
,
598 DBUSMENU_MENUITEM_TOGGLE_CHECK
);
600 dbusmenu_menuitem_property_set(aItem
, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE
,
601 DBUSMENU_MENUITEM_TOGGLE_RADIO
);
604 bool isChecked
= aContent
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::checked
,
605 nsGkAtoms::_true
, eCaseMatters
);
606 dbusmenu_menuitem_property_set_int(
607 aItem
, DBUSMENU_MENUITEM_PROP_TOGGLE_STATE
,
608 isChecked
? DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED
609 : DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED
);
612 static void UpdateEnabled(DbusmenuMenuitem
* aItem
, const nsIContent
* aContent
) {
613 bool disabled
= aContent
->AsElement()->AttrValueIs(
614 kNameSpaceID_None
, nsGkAtoms::disabled
, nsGkAtoms::_true
, eCaseMatters
);
616 dbusmenu_menuitem_property_set_bool(aItem
, DBUSMENU_MENUITEM_PROP_ENABLED
,
620 // we rebuild the dbus model when elements are removed from the DOM,
621 // so this isn't going to trigger for asynchronous
622 static MOZ_CAN_RUN_SCRIPT
void DBusActivationCallback(
623 DbusmenuMenuitem
* aMenuitem
, guint aTimestamp
, gpointer aUserData
) {
624 RefPtr element
= static_cast<dom::Element
*>(aUserData
);
625 ActivateItem(*element
);
628 static void ConnectActivated(DbusmenuMenuitem
* aItem
,
629 const dom::Element
* aContent
) {
630 g_signal_connect(G_OBJECT(aItem
), DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED
,
631 G_CALLBACK(DBusActivationCallback
),
632 const_cast<dom::Element
*>(aContent
));
635 static MOZ_CAN_RUN_SCRIPT
void DBusAboutToShowCallback(
636 DbusmenuMenuitem
* aMenuitem
, gpointer aUserData
) {
637 RefPtr element
= static_cast<dom::Element
*>(aUserData
);
638 FireEvent(element
, eXULPopupShowing
);
639 FireEvent(element
, eXULPopupShown
);
642 static void ConnectAboutToShow(DbusmenuMenuitem
* aItem
,
643 const dom::Element
* aContent
) {
644 g_signal_connect(G_OBJECT(aItem
), DBUSMENU_MENUITEM_SIGNAL_ABOUT_TO_SHOW
,
645 G_CALLBACK(DBusAboutToShowCallback
),
646 const_cast<dom::Element
*>(aContent
));
649 void MenubarModelDBus::AppendMenuItem(DbusmenuMenuitem
* aParent
,
650 const dom::Element
* aChild
) {
652 aChild
->GetAttr(nsGkAtoms::label
, label
);
653 if (label
.IsEmpty()) {
654 aChild
->GetAttr(nsGkAtoms::aria_label
, label
);
656 RefPtr
<DbusmenuMenuitem
> child
= dont_AddRef(dbusmenu_menuitem_new());
657 dbusmenu_menuitem_property_set(child
, DBUSMENU_MENUITEM_PROP_LABEL
,
658 NS_ConvertUTF16toUTF8(label
).get());
659 dbusmenu_menuitem_child_append(aParent
, child
);
660 UpdateAccel(child
, aChild
);
661 UpdateRadioOrCheck(child
, aChild
);
662 UpdateEnabled(child
, aChild
);
663 ConnectActivated(child
, aChild
);
667 void MenubarModelDBus::AppendSeparator(DbusmenuMenuitem
* aParent
) {
668 RefPtr
<DbusmenuMenuitem
> child
= dont_AddRef(dbusmenu_menuitem_new());
669 dbusmenu_menuitem_property_set(child
, DBUSMENU_MENUITEM_PROP_TYPE
,
671 dbusmenu_menuitem_child_append(aParent
, child
);
674 void MenubarModelDBus::AppendSubmenu(DbusmenuMenuitem
* aParent
,
675 const dom::Element
* aMenu
,
676 const dom::Element
* aPopup
) {
677 RefPtr
<DbusmenuMenuitem
> submenu
= dont_AddRef(dbusmenu_menuitem_new());
678 if (RecomputeModelFor(submenu
, *aPopup
) == 0) {
679 RefPtr
<DbusmenuMenuitem
> placeholder
= dont_AddRef(dbusmenu_menuitem_new());
680 dbusmenu_menuitem_child_append(submenu
, placeholder
);
683 aMenu
->GetAttr(nsGkAtoms::label
, label
);
684 ConnectAboutToShow(submenu
, aPopup
);
685 dbusmenu_menuitem_property_set(submenu
, DBUSMENU_MENUITEM_PROP_LABEL
,
686 NS_ConvertUTF16toUTF8(label
).get());
687 dbusmenu_menuitem_child_append(aParent
, submenu
);
690 uint
MenubarModelDBus::RecomputeModelFor(DbusmenuMenuitem
* aParent
,
691 const dom::Element
& aElement
) {
693 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
694 child
= child
->GetNextSibling()) {
695 if (child
->IsXULElement(nsGkAtoms::menuitem
) &&
696 !IsDisabled(*child
->AsElement())) {
697 AppendMenuItem(aParent
, child
->AsElement());
701 if (child
->IsXULElement(nsGkAtoms::menuseparator
)) {
702 AppendSeparator(aParent
);
706 if (child
->IsXULElement(nsGkAtoms::menu
) &&
707 !IsDisabled(*child
->AsElement())) {
708 if (const auto* popup
= GetMenuPopupChild(*child
->AsElement())) {
710 AppendSubmenu(aParent
, child
->AsElement(), popup
);
717 void DBusMenuBar::NameOwnerChangedCallback(GObject
*, GParamSpec
*,
718 gpointer user_data
) {
719 static_cast<DBusMenuBar
*>(user_data
)->OnNameOwnerChanged();
722 void DBusMenuBar::OnNameOwnerChanged() {
723 GUniquePtr
<gchar
> nameOwner(g_dbus_proxy_get_name_owner(mProxy
));
728 RefPtr win
= mMenuModel
->Element()->OwnerDoc()->GetInnerWindow();
729 if (NS_WARN_IF(!win
)) {
732 nsIWidget
* widget
= nsGlobalWindowInner::Cast(win
.get())->GetNearestWidget();
733 if (NS_WARN_IF(!widget
)) {
737 static_cast<GdkWindow
*>(widget
->GetNativeData(NS_NATIVE_WINDOW
));
738 if (NS_WARN_IF(!gdkWin
)) {
743 if (auto* display
= widget::WaylandDisplayGet()) {
744 if (!StaticPrefs::widget_gtk_global_menu_wayland_enabled()) {
747 xdg_dbus_annotation_manager_v1
* annotationManager
=
748 display
->GetXdgDbusAnnotationManager();
749 if (NS_WARN_IF(!annotationManager
)) {
753 wl_surface
* surface
= gdk_wayland_window_get_wl_surface(gdkWin
);
754 if (NS_WARN_IF(!surface
)) {
758 GDBusConnection
* connection
= g_dbus_proxy_get_connection(mProxy
);
759 const char* myServiceName
= g_dbus_connection_get_unique_name(connection
);
760 if (NS_WARN_IF(!myServiceName
)) {
764 // FIXME(emilio, bug 1883209): Nothing deletes this as of right now.
765 mAnnotation
= xdg_dbus_annotation_manager_v1_create_surface(
766 annotationManager
, "com.canonical.dbusmenu", surface
);
768 xdg_dbus_annotation_v1_set_address(mAnnotation
, myServiceName
,
775 auto xid
= GDK_WINDOW_XID(gdkWin
);
776 widget::DBusProxyCall(mProxy
, "RegisterWindow",
777 g_variant_new("(uo)", xid
, mObjectPath
.get()),
778 G_DBUS_CALL_FLAGS_NONE
)
780 GetCurrentSerialEventTarget(), __func__
,
781 [self
= RefPtr
{this}](RefPtr
<GVariant
>&& aResult
) {
782 self
->mMenuModel
->Element()->SetBoolAttr(nsGkAtoms::hidden
, true);
784 [self
= RefPtr
{this}](GUniquePtr
<GError
>&& aError
) {
785 g_printerr("Failed to register window menubar: %s\n",
787 self
->mMenuModel
->Element()->SetBoolAttr(nsGkAtoms::hidden
, false);
792 static unsigned sID
= 0;
794 DBusMenuBar::DBusMenuBar(dom::Element
* aElement
)
795 : mObjectPath(nsPrintfCString("/com/canonical/menu/%u", sID
++)),
796 mMenuModel(MakeRefPtr
<MenubarModelDBus
>(aElement
)),
797 mServer(dont_AddRef(dbusmenu_server_new(mObjectPath
.get()))) {
798 mMenuModel
->RecomputeModelIfNeeded();
799 dbusmenu_server_set_root(mServer
.get(), mMenuModel
->Root());
802 RefPtr
<DBusMenuBar
> DBusMenuBar::Create(dom::Element
* aElement
) {
803 RefPtr
<DBusMenuBar
> self
= new DBusMenuBar(aElement
);
804 widget::CreateDBusProxyForBus(
806 GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
|
807 G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS
|
808 G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START
),
809 nullptr, "com.canonical.AppMenu.Registrar",
810 "/com/canonical/AppMenu/Registrar", "com.canonical.AppMenu.Registrar")
812 GetCurrentSerialEventTarget(), __func__
,
813 [self
](RefPtr
<GDBusProxy
>&& aProxy
) {
814 self
->mProxy
= std::move(aProxy
);
815 g_signal_connect(self
->mProxy
, "notify::g-name-owner",
816 G_CALLBACK(NameOwnerChangedCallback
), self
.get());
817 self
->OnNameOwnerChanged();
819 [](GUniquePtr
<GError
>&& aError
) {
820 g_printerr("Failed to create DBUS proxy for menubar: %s\n",
826 DBusMenuBar::~DBusMenuBar() {
828 MozClearPointer(mAnnotation
, xdg_dbus_annotation_v1_destroy
);
833 } // namespace mozilla::widget