Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / widget / gtk / NativeMenuGtk.cpp
blobb557f068e0784bff6b4c7fa18441a9387d6e31e3
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 #ifdef MOZ_WAYLAND
30 # include "nsWaylandDisplay.h"
31 #endif // MOZ_WAYLAND
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,
50 nsGkAtoms::menubar);
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,
58 eCaseMatters)) {
59 case 0:
60 break;
61 case 1:
62 break;
63 default:
64 return Nothing();
67 return Some(aMenuItem.AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
68 nsGkAtoms::_true, eCaseMatters));
71 struct Actions {
72 RefPtr<GSimpleActionGroup> mGroup;
73 size_t mNextActionIndex = 0;
75 nsPrintfCString Register(const dom::Element&, bool aForSubmenu);
76 void Clear();
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;
84 if (newValue) {
85 aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns,
86 true);
87 } else {
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())) {
102 return;
104 aElement.DispatchEvent(*event);
107 static MOZ_CAN_RUN_SCRIPT void ActivateSignal(GSimpleAction* aAction,
108 GVariant* aParam,
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,
122 GVariant* aParam,
123 gpointer aUserData) {
124 // TODO: Fire events when safe. These run at a bad time for now.
125 static constexpr bool kEnabled = false;
126 if (!kEnabled) {
127 return;
129 const bool open = g_variant_get_boolean(aParam);
130 RefPtr popup = static_cast<dom::Element*>(aUserData);
131 if (open) {
132 FireEvent(popup, eXULPopupShowing);
133 FireEvent(popup, eXULPopupShown);
134 } else {
135 FireEvent(popup, eXULPopupHiding);
136 FireEvent(popup, eXULPopupHidden);
140 nsPrintfCString Actions::Register(const dom::Element& aMenuItem,
141 bool aForSubmenu) {
142 nsPrintfCString actionName("item-%zu", mNextActionIndex++);
143 Maybe<bool> paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem);
144 RefPtr<GSimpleAction> action;
145 if (paramValue) {
146 action = dont_AddRef(g_simple_action_new_stateful(
147 actionName.get(), nullptr, g_variant_new_boolean(*paramValue)));
148 } else {
149 action = dont_AddRef(g_simple_action_new(actionName.get(), nullptr));
151 if (aForSubmenu) {
152 g_signal_connect(action, "change-state", G_CALLBACK(ChangeStateSignal),
153 gpointer(&aMenuItem));
154 } else {
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()));
159 return actionName;
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 {
171 NS_DECL_ISUPPORTS
173 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
174 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
175 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
176 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
178 public:
179 explicit MenuModel(dom::Element* aElement) : mElement(aElement) {
180 mElement->AddMutationObserver(this);
183 dom::Element* Element() { return mElement; }
185 void RecomputeModelIfNeeded() {
186 if (!mDirty) {
187 return;
189 RecomputeModel();
190 mDirty = false;
193 bool IsShowing() { return mShowing; }
194 void WillShow() {
195 mShowing = true;
196 RecomputeModelIfNeeded();
198 void DidHide() { mShowing = false; }
200 protected:
201 virtual void RecomputeModel() = 0;
202 virtual ~MenuModel() { mElement->RemoveMutationObserver(this); }
204 void DirtyModel() {
205 mDirty = true;
206 if (mShowing) {
207 RecomputeModelIfNeeded();
211 RefPtr<dom::Element> mElement;
212 bool mDirty = true;
213 bool mShowing = false;
216 class MenuModelGMenu final : public MenuModel {
217 public:
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());
228 protected:
229 void RecomputeModel() override;
230 static void RecomputeModelFor(GMenu* aMenu, Actions& aActions,
231 const dom::Element& aElement);
233 RefPtr<GMenu> mGMenu;
234 Actions mActions;
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)) {
249 DirtyModel();
253 void MenuModel::ContentAppended(nsIContent* aChild) {
254 if (NodeIsRelevant(*aChild)) {
255 DirtyModel();
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)) {
265 DirtyModel();
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();
276 return nullptr;
279 void MenuModelGMenu::RecomputeModelFor(GMenu* aMenu, Actions& aActions,
280 const dom::Element& aElement) {
281 RefPtr<GMenu> sectionMenu;
282 auto FlushSectionMenu = [&] {
283 if (sectionMenu) {
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())) {
293 nsAutoString label;
294 child->AsElement()->GetAttr(nsGkAtoms::label, label);
295 if (label.IsEmpty()) {
296 child->AsElement()->GetAttr(nsGkAtoms::aria_label, label);
298 nsPrintfCString actionName(
299 "menu.%s",
300 aActions.Register(*child->AsElement(), /* aForSubmenu = */ false)
301 .get());
302 g_menu_append(sectionMenu ? sectionMenu.get() : aMenu,
303 NS_ConvertUTF16toUTF8(label).get(), actionName.get());
304 continue;
306 if (child->IsXULElement(nsGkAtoms::menuseparator)) {
307 FlushSectionMenu();
308 sectionMenu = dont_AddRef(g_menu_new());
309 continue;
311 if (child->IsXULElement(nsGkAtoms::menugroup)) {
312 FlushSectionMenu();
313 sectionMenu = dont_AddRef(g_menu_new());
314 RecomputeModelFor(sectionMenu, aActions, *child->AsElement());
315 FlushSectionMenu();
316 continue;
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);
323 nsAutoString label;
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(
328 "menu.%s",
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,
333 submenuItem.get());
338 FlushSectionMenu();
341 void MenuModelGMenu::RecomputeModel() {
342 mActions.Clear();
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");
350 return sFunc;
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);
371 #undef METHOD_SIGNAL
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()) {
392 return;
394 RefPtr<nsIWidget> widget = aClickedFrame->PresContext()->GetRootWidget();
395 if (NS_WARN_IF(!widget)) {
396 // XXX Do we need to close menus here?
397 return;
399 auto* win = static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
400 if (NS_WARN_IF(!win)) {
401 return;
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());
417 RefPtr pin{this};
418 FireEvent(eXULPopupShown);
421 bool NativeMenuGtk::Close() {
422 if (!mMenuModel->IsShowing()) {
423 return false;
425 gtk_menu_popdown(GTK_MENU(mNativeMenu.get()));
426 return true;
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 {
457 public:
458 explicit MenubarModelDBus(dom::Element* aElement) : MenuModel(aElement) {
459 mRoot = dont_AddRef(dbusmenu_menuitem_new());
460 dbusmenu_menuitem_set_root(mRoot.get(), true);
461 mShowing = true;
464 DbusmenuMenuitem* Root() const { return mRoot.get(); }
466 protected:
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);
483 if (!first) {
484 break;
486 dbusmenu_menuitem_child_delete(mRoot.get(), first);
488 RecomputeModelFor(mRoot, *Element());
491 static const dom::Element* RelevantElementForKeys(
492 const dom::Element* aElement) {
493 nsAutoString key;
494 aElement->GetAttr(nsGkAtoms::key, key);
495 if (!key.IsEmpty()) {
496 dom::Document* document = aElement->OwnerDoc();
497 dom::Element* element = document->GetElementById(key);
498 if (element) {
499 return element;
502 return aElement;
505 static uint32_t ParseKey(const nsAString& aKey, const nsAString& aKeyCode) {
506 guint key = 0;
507 if (!aKey.IsEmpty()) {
508 key = gdk_unicode_to_keyval(*aKey.BeginReading());
511 if (key == 0 && !aKeyCode.IsEmpty()) {
512 key = KeymapWrapper::ConvertGeckoKeyCodeToGDKKeyval(aKeyCode);
515 return key;
518 static uint32_t KeyFrom(const dom::Element* aElement) {
519 const auto* element = RelevantElementForKeys(aElement);
521 nsAutoString key;
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()) {
533 return 0;
536 uint32_t modifier = 0;
537 char* str = ToNewUTF8String(aModifiers);
538 char* token = strtok(str, ", \t");
539 while (token) {
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");
562 free(str);
564 return modifier;
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());
578 if (key != 0) {
579 dbusmenu_menuitem_property_set_shortcut(
580 aItem, key,
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) {
593 return;
596 if (type == 0) {
597 dbusmenu_menuitem_property_set(aItem, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
598 DBUSMENU_MENUITEM_TOGGLE_CHECK);
599 } else {
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,
617 !disabled);
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) {
651 nsAutoString label;
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);
664 // TODO: icons
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,
670 "separator");
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);
682 nsAutoString label;
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) {
692 uint childCount = 0;
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());
698 childCount++;
699 continue;
701 if (child->IsXULElement(nsGkAtoms::menuseparator)) {
702 AppendSeparator(aParent);
703 childCount++;
704 continue;
706 if (child->IsXULElement(nsGkAtoms::menu) &&
707 !IsDisabled(*child->AsElement())) {
708 if (const auto* popup = GetMenuPopupChild(*child->AsElement())) {
709 childCount++;
710 AppendSubmenu(aParent, child->AsElement(), popup);
714 return childCount;
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));
724 if (!nameOwner) {
725 return;
728 RefPtr win = mMenuModel->Element()->OwnerDoc()->GetInnerWindow();
729 if (NS_WARN_IF(!win)) {
730 return;
732 nsIWidget* widget = nsGlobalWindowInner::Cast(win.get())->GetNearestWidget();
733 if (NS_WARN_IF(!widget)) {
734 return;
736 auto* gdkWin =
737 static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
738 if (NS_WARN_IF(!gdkWin)) {
739 return;
742 # ifdef MOZ_WAYLAND
743 if (auto* display = widget::WaylandDisplayGet()) {
744 if (!StaticPrefs::widget_gtk_global_menu_wayland_enabled()) {
745 return;
747 xdg_dbus_annotation_manager_v1* annotationManager =
748 display->GetXdgDbusAnnotationManager();
749 if (NS_WARN_IF(!annotationManager)) {
750 return;
753 wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWin);
754 if (NS_WARN_IF(!surface)) {
755 return;
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)) {
761 return;
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,
769 mObjectPath.get());
770 return;
772 # endif
773 # ifdef MOZ_X11
774 // legacy path
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)
779 ->Then(
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",
786 aError->message);
787 self->mMenuModel->Element()->SetBoolAttr(nsGkAtoms::hidden, false);
789 # endif
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(
805 G_BUS_TYPE_SESSION,
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")
811 ->Then(
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",
821 aError->message);
823 return self;
826 DBusMenuBar::~DBusMenuBar() {
827 # ifdef MOZ_WAYLAND
828 MozClearPointer(mAnnotation, xdg_dbus_annotation_v1_destroy);
829 # endif
831 #endif
833 } // namespace mozilla::widget