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 "mozilla/dom/Document.h"
8 #include "mozilla/dom/DocumentInlines.h"
9 #include "mozilla/dom/XULCommandEvent.h"
10 #include "mozilla/WidgetUtilsGtk.h"
11 #include "mozilla/EventDispatcher.h"
12 #include "nsPresContext.h"
13 #include "nsIWidget.h"
15 #include "nsStubMutationObserver.h"
16 #include "mozilla/dom/Element.h"
17 #include "mozilla/StaticPrefs_widget.h"
22 namespace mozilla::widget
{
24 using GtkMenuPopupAtRect
= void (*)(GtkMenu
* menu
, GdkWindow
* rect_window
,
25 const GdkRectangle
* rect
,
26 GdkGravity rect_anchor
,
27 GdkGravity menu_anchor
,
28 const GdkEvent
* trigger_event
);
30 static bool IsDisabled(const dom::Element
& aElement
) {
31 return aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::disabled
,
32 nsGkAtoms::_true
, eCaseMatters
) ||
33 aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::hidden
,
34 nsGkAtoms::_true
, eCaseMatters
);
36 static bool NodeIsRelevant(const nsINode
& aNode
) {
37 return aNode
.IsAnyOfXULElements(nsGkAtoms::menu
, nsGkAtoms::menuseparator
,
38 nsGkAtoms::menuitem
, nsGkAtoms::menugroup
);
41 // If this is a radio / checkbox menuitem, get the current value.
42 static Maybe
<bool> GetChecked(const dom::Element
& aMenuItem
) {
43 static dom::Element::AttrValuesArray strings
[] = {nsGkAtoms::checkbox
,
44 nsGkAtoms::radio
, nullptr};
45 switch (aMenuItem
.FindAttrValueIn(kNameSpaceID_None
, nsGkAtoms::type
, strings
,
55 return Some(aMenuItem
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::checked
,
56 nsGkAtoms::_true
, eCaseMatters
));
60 RefPtr
<GSimpleActionGroup
> mGroup
;
61 size_t mNextActionIndex
= 0;
63 nsPrintfCString
Register(const dom::Element
&, bool aForSubmenu
);
67 static MOZ_CAN_RUN_SCRIPT
void ActivateItem(dom::Element
& aElement
) {
68 if (Maybe
<bool> checked
= GetChecked(aElement
)) {
69 if (!aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::autocheck
,
70 nsGkAtoms::_false
, eCaseMatters
)) {
71 bool newValue
= !*checked
;
73 aElement
.SetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, u
"true"_ns
,
76 aElement
.UnsetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, true);
81 RefPtr doc
= aElement
.OwnerDoc();
82 RefPtr event
= new dom::XULCommandEvent(doc
, doc
->GetPresContext(), nullptr);
83 IgnoredErrorResult rv
;
84 event
->InitCommandEvent(u
"command"_ns
, true, true,
85 nsGlobalWindowInner::Cast(doc
->GetInnerWindow()), 0,
86 /* ctrlKey = */ false, /* altKey = */ false,
87 /* shiftKey = */ false, /* cmdKey = */ false,
88 /* button = */ MouseButton::ePrimary
, nullptr, 0, rv
);
89 if (MOZ_UNLIKELY(rv
.Failed())) {
92 aElement
.DispatchEvent(*event
);
95 static MOZ_CAN_RUN_SCRIPT
void ActivateSignal(GSimpleAction
* aAction
,
98 RefPtr element
= static_cast<dom::Element
*>(aUserData
);
99 ActivateItem(*element
);
102 static MOZ_CAN_RUN_SCRIPT
void FireEvent(dom::Element
* aTarget
,
103 EventMessage aPopupMessage
) {
104 nsEventStatus status
= nsEventStatus_eIgnore
;
105 WidgetMouseEvent
event(true, aPopupMessage
, nullptr, WidgetMouseEvent::eReal
);
106 EventDispatcher::Dispatch(aTarget
, nullptr, &event
, nullptr, &status
);
109 static MOZ_CAN_RUN_SCRIPT
void ChangeStateSignal(GSimpleAction
* aAction
,
111 gpointer aUserData
) {
112 // TODO: Fire events when safe. These run at a bad time for now.
113 static constexpr bool kEnabled
= false;
117 const bool open
= g_variant_get_boolean(aParam
);
118 RefPtr popup
= static_cast<dom::Element
*>(aUserData
);
120 FireEvent(popup
, eXULPopupShowing
);
121 FireEvent(popup
, eXULPopupShown
);
123 FireEvent(popup
, eXULPopupHiding
);
124 FireEvent(popup
, eXULPopupHidden
);
128 nsPrintfCString
Actions::Register(const dom::Element
& aMenuItem
,
130 nsPrintfCString
actionName("item-%zu", mNextActionIndex
++);
131 Maybe
<bool> paramValue
= aForSubmenu
? Some(false) : GetChecked(aMenuItem
);
132 RefPtr
<GSimpleAction
> action
;
134 action
= dont_AddRef(g_simple_action_new_stateful(
135 actionName
.get(), nullptr, g_variant_new_boolean(*paramValue
)));
137 action
= dont_AddRef(g_simple_action_new(actionName
.get(), nullptr));
140 g_signal_connect(action
, "change-state", G_CALLBACK(ChangeStateSignal
),
141 gpointer(&aMenuItem
));
143 g_signal_connect(action
, "activate", G_CALLBACK(ActivateSignal
),
144 gpointer(&aMenuItem
));
146 g_action_map_add_action(G_ACTION_MAP(mGroup
.get()), G_ACTION(action
.get()));
150 void Actions::Clear() {
151 for (size_t i
= 0; i
< mNextActionIndex
; ++i
) {
152 g_action_map_remove_action(G_ACTION_MAP(mGroup
.get()),
153 nsPrintfCString("item-%zu", i
).get());
155 mNextActionIndex
= 0;
158 class MenuModel final
: public nsStubMutationObserver
{
161 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
162 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
163 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
164 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
167 explicit MenuModel(dom::Element
* aElement
) : mElement(aElement
) {
168 mElement
->AddMutationObserver(this);
169 mGMenu
= dont_AddRef(g_menu_new());
170 mActions
.mGroup
= dont_AddRef(g_simple_action_group_new());
173 GMenuModel
* GetModel() { return G_MENU_MODEL(mGMenu
.get()); }
174 GActionGroup
* GetActionGroup() {
175 return G_ACTION_GROUP(mActions
.mGroup
.get());
178 dom::Element
* Element() { return mElement
; }
180 void RecomputeModelIfNeeded();
182 bool IsShowing() { return mPoppedUp
; }
185 RecomputeModelIfNeeded();
187 void DidHide() { mPoppedUp
= false; }
190 virtual ~MenuModel() { mElement
->RemoveMutationObserver(this); }
195 RecomputeModelIfNeeded();
199 RefPtr
<dom::Element
> mElement
;
200 RefPtr
<GMenu
> mGMenu
;
203 bool mPoppedUp
= false;
206 NS_IMPL_ISUPPORTS(MenuModel
, nsIMutationObserver
)
208 void MenuModel::ContentRemoved(nsIContent
* aChild
, nsIContent
*) {
209 if (NodeIsRelevant(*aChild
)) {
214 void MenuModel::ContentInserted(nsIContent
* aChild
) {
215 if (NodeIsRelevant(*aChild
)) {
220 void MenuModel::ContentAppended(nsIContent
* aChild
) {
221 if (NodeIsRelevant(*aChild
)) {
226 void MenuModel::AttributeChanged(dom::Element
* aElement
, int32_t aNameSpaceID
,
227 nsAtom
* aAttribute
, int32_t aModType
,
228 const nsAttrValue
* aOldValue
) {
229 if (NodeIsRelevant(*aElement
) &&
230 (aAttribute
== nsGkAtoms::label
|| aAttribute
== nsGkAtoms::aria_label
||
231 aAttribute
== nsGkAtoms::disabled
|| aAttribute
== nsGkAtoms::hidden
)) {
236 static const dom::Element
* GetMenuPopupChild(const dom::Element
& aElement
) {
237 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
238 child
= child
->GetNextSibling()) {
239 if (child
->IsXULElement(nsGkAtoms::menupopup
)) {
240 return child
->AsElement();
246 static void RecomputeModelFor(GMenu
* aMenu
, Actions
& aActions
,
247 const dom::Element
& aElement
) {
248 RefPtr
<GMenu
> sectionMenu
;
249 auto FlushSectionMenu
= [&] {
251 g_menu_append_section(aMenu
, nullptr, G_MENU_MODEL(sectionMenu
.get()));
252 sectionMenu
= nullptr;
256 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
257 child
= child
->GetNextSibling()) {
258 if (child
->IsXULElement(nsGkAtoms::menuitem
) &&
259 !IsDisabled(*child
->AsElement())) {
261 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
262 if (label
.IsEmpty()) {
263 child
->AsElement()->GetAttr(nsGkAtoms::aria_label
, label
);
265 nsPrintfCString
actionName(
267 aActions
.Register(*child
->AsElement(), /* aForSubmenu = */ false)
269 g_menu_append(sectionMenu
? sectionMenu
.get() : aMenu
,
270 NS_ConvertUTF16toUTF8(label
).get(), actionName
.get());
273 if (child
->IsXULElement(nsGkAtoms::menuseparator
)) {
275 sectionMenu
= dont_AddRef(g_menu_new());
278 if (child
->IsXULElement(nsGkAtoms::menugroup
)) {
280 sectionMenu
= dont_AddRef(g_menu_new());
281 RecomputeModelFor(sectionMenu
, aActions
, *child
->AsElement());
285 if (child
->IsXULElement(nsGkAtoms::menu
) &&
286 !IsDisabled(*child
->AsElement())) {
287 if (const auto* popup
= GetMenuPopupChild(*child
->AsElement())) {
288 RefPtr
<GMenu
> submenu
= dont_AddRef(g_menu_new());
289 RecomputeModelFor(submenu
, aActions
, *popup
);
291 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
292 RefPtr
<GMenuItem
> submenuItem
= dont_AddRef(g_menu_item_new_submenu(
293 NS_ConvertUTF16toUTF8(label
).get(), G_MENU_MODEL(submenu
.get())));
294 nsPrintfCString
actionName(
296 aActions
.Register(*popup
, /* aForSubmenu = */ true).get());
297 g_menu_item_set_attribute_value(submenuItem
.get(), "submenu-action",
298 g_variant_new_string(actionName
.get()));
299 g_menu_append_item(sectionMenu
? sectionMenu
.get() : aMenu
,
308 void MenuModel::RecomputeModelIfNeeded() {
313 g_menu_remove_all(mGMenu
.get());
314 RecomputeModelFor(mGMenu
.get(), mActions
, *mElement
);
317 static GtkMenuPopupAtRect
GetPopupAtRectFn() {
318 static GtkMenuPopupAtRect sFunc
=
319 (GtkMenuPopupAtRect
)dlsym(RTLD_DEFAULT
, "gtk_menu_popup_at_rect");
323 bool NativeMenuGtk::CanUse() {
324 return StaticPrefs::widget_gtk_native_context_menus() && GetPopupAtRectFn();
327 void NativeMenuGtk::FireEvent(EventMessage aPopupMessage
) {
328 RefPtr target
= Element();
329 widget::FireEvent(target
, aPopupMessage
);
332 #define METHOD_SIGNAL(name_) \
333 static MOZ_CAN_RUN_SCRIPT_BOUNDARY void On##name_##Signal( \
334 GtkWidget* widget, gpointer user_data) { \
335 RefPtr menu = static_cast<NativeMenuGtk*>(user_data); \
336 return menu->On##name_(); \
339 METHOD_SIGNAL(Unmap
);
343 NativeMenuGtk::NativeMenuGtk(dom::Element
* aElement
)
344 : mMenuModel(MakeRefPtr
<MenuModel
>(aElement
)) {
345 // Floating, so no need to dont_AddRef.
346 mNativeMenu
= gtk_menu_new_from_model(mMenuModel
->GetModel());
347 gtk_widget_insert_action_group(mNativeMenu
.get(), "menu",
348 mMenuModel
->GetActionGroup());
349 g_signal_connect(mNativeMenu
, "unmap", G_CALLBACK(OnUnmapSignal
), this);
352 NativeMenuGtk::~NativeMenuGtk() {
353 g_signal_handlers_disconnect_by_data(mNativeMenu
, this);
356 RefPtr
<dom::Element
> NativeMenuGtk::Element() { return mMenuModel
->Element(); }
358 void NativeMenuGtk::ShowAsContextMenu(nsIFrame
* aClickedFrame
,
359 const CSSIntPoint
& aPosition
,
360 bool aIsContextMenu
) {
361 if (mMenuModel
->IsShowing()) {
364 RefPtr
<nsIWidget
> widget
= aClickedFrame
->PresContext()->GetRootWidget();
365 if (NS_WARN_IF(!widget
)) {
366 // XXX Do we need to close menus here?
369 auto* win
= static_cast<GdkWindow
*>(widget
->GetNativeData(NS_NATIVE_WINDOW
));
370 if (NS_WARN_IF(!win
)) {
374 auto* geckoWin
= static_cast<nsWindow
*>(widget
.get());
375 // The position needs to be relative to our window.
376 auto pos
= (aPosition
* aClickedFrame
->PresContext()->CSSToDevPixelScale()) -
377 geckoWin
->WidgetToScreenOffset();
378 auto gdkPos
= geckoWin
->DevicePixelsToGdkPointRoundDown(
379 LayoutDeviceIntPoint::Round(pos
));
381 mMenuModel
->WillShow();
382 const GdkRectangle rect
= {gdkPos
.x
, gdkPos
.y
, 1, 1};
383 auto openFn
= GetPopupAtRectFn();
384 openFn(GTK_MENU(mNativeMenu
.get()), win
, &rect
, GDK_GRAVITY_NORTH_WEST
,
385 GDK_GRAVITY_NORTH_WEST
, GetLastMousePressEvent());
388 FireEvent(eXULPopupShown
);
391 bool NativeMenuGtk::Close() {
392 if (!mMenuModel
->IsShowing()) {
395 gtk_menu_popdown(GTK_MENU(mNativeMenu
.get()));
399 void NativeMenuGtk::OnUnmap() {
400 FireEvent(eXULPopupHiding
);
402 mMenuModel
->DidHide();
404 FireEvent(eXULPopupHidden
);
406 for (NativeMenu::Observer
* observer
: mObservers
.Clone()) {
407 observer
->OnNativeMenuClosed();
411 void NativeMenuGtk::ActivateItem(dom::Element
* aItemElement
, Modifiers
,
412 int16_t aButton
, ErrorResult
&) {
413 // TODO: For testing only.
416 void NativeMenuGtk::OpenSubmenu(dom::Element
*) {
417 // TODO: For testing mostly.
420 void NativeMenuGtk::CloseSubmenu(dom::Element
*) {
421 // TODO: For testing mostly.
424 } // namespace mozilla::widget