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/EventDispatcher.h"
11 #include "nsPresContext.h"
12 #include "nsIWidget.h"
14 #include "nsStubMutationObserver.h"
15 #include "mozilla/dom/Element.h"
16 #include "mozilla/StaticPrefs_widget.h"
21 namespace mozilla::widget
{
23 using GtkMenuPopupAtRect
= void (*)(GtkMenu
* menu
, GdkWindow
* rect_window
,
24 const GdkRectangle
* rect
,
25 GdkGravity rect_anchor
,
26 GdkGravity menu_anchor
,
27 const GdkEvent
* trigger_event
);
29 static bool IsDisabled(const dom::Element
& aElement
) {
30 return aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::disabled
,
31 nsGkAtoms::_true
, eCaseMatters
) ||
32 aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::hidden
,
33 nsGkAtoms::_true
, eCaseMatters
);
35 static bool NodeIsRelevant(const nsINode
& aNode
) {
36 return aNode
.IsAnyOfXULElements(nsGkAtoms::menu
, nsGkAtoms::menuseparator
,
37 nsGkAtoms::menuitem
, nsGkAtoms::menugroup
);
40 // If this is a radio / checkbox menuitem, get the current value.
41 static Maybe
<bool> GetChecked(const dom::Element
& aMenuItem
) {
42 static dom::Element::AttrValuesArray strings
[] = {nsGkAtoms::checkbox
,
43 nsGkAtoms::radio
, nullptr};
44 switch (aMenuItem
.FindAttrValueIn(kNameSpaceID_None
, nsGkAtoms::type
, strings
,
54 return Some(aMenuItem
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::checked
,
55 nsGkAtoms::_true
, eCaseMatters
));
59 RefPtr
<GSimpleActionGroup
> mGroup
;
60 size_t mNextActionIndex
= 0;
62 nsPrintfCString
Register(const dom::Element
&, bool aForSubmenu
);
66 static MOZ_CAN_RUN_SCRIPT
void ActivateItem(dom::Element
& aElement
) {
67 if (Maybe
<bool> checked
= GetChecked(aElement
)) {
68 if (!aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::autocheck
,
69 nsGkAtoms::_false
, eCaseMatters
)) {
70 bool newValue
= !*checked
;
72 aElement
.SetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, u
"true"_ns
,
75 aElement
.UnsetAttr(kNameSpaceID_None
, nsGkAtoms::checked
, true);
80 RefPtr doc
= aElement
.OwnerDoc();
81 RefPtr event
= new dom::XULCommandEvent(doc
, doc
->GetPresContext(), nullptr);
82 IgnoredErrorResult rv
;
83 event
->InitCommandEvent(u
"command"_ns
, true, true,
84 nsGlobalWindowInner::Cast(doc
->GetInnerWindow()), 0,
85 /* ctrlKey = */ false, /* altKey = */ false,
86 /* shiftKey = */ false, /* cmdKey = */ false,
87 /* button = */ MouseButton::ePrimary
, nullptr, 0, rv
);
88 if (MOZ_UNLIKELY(rv
.Failed())) {
91 aElement
.DispatchEvent(*event
);
94 static MOZ_CAN_RUN_SCRIPT
void ActivateSignal(GSimpleAction
* aAction
,
97 RefPtr element
= static_cast<dom::Element
*>(aUserData
);
98 ActivateItem(*element
);
101 static MOZ_CAN_RUN_SCRIPT
void FireEvent(dom::Element
* aTarget
,
102 EventMessage aPopupMessage
) {
103 nsEventStatus status
= nsEventStatus_eIgnore
;
104 WidgetMouseEvent
event(true, aPopupMessage
, nullptr, WidgetMouseEvent::eReal
);
105 EventDispatcher::Dispatch(aTarget
, nullptr, &event
, nullptr, &status
);
108 static MOZ_CAN_RUN_SCRIPT
void ChangeStateSignal(GSimpleAction
* aAction
,
110 gpointer aUserData
) {
111 // TODO: Fire events when safe. These run at a bad time for now.
112 static constexpr bool kEnabled
= false;
116 const bool open
= g_variant_get_boolean(aParam
);
117 RefPtr popup
= static_cast<dom::Element
*>(aUserData
);
119 FireEvent(popup
, eXULPopupShowing
);
120 FireEvent(popup
, eXULPopupShown
);
122 FireEvent(popup
, eXULPopupHiding
);
123 FireEvent(popup
, eXULPopupHidden
);
127 nsPrintfCString
Actions::Register(const dom::Element
& aMenuItem
,
129 nsPrintfCString
actionName("item-%zu", mNextActionIndex
++);
130 Maybe
<bool> paramValue
= aForSubmenu
? Some(false) : GetChecked(aMenuItem
);
131 RefPtr
<GSimpleAction
> action
;
133 action
= dont_AddRef(g_simple_action_new_stateful(
134 actionName
.get(), nullptr, g_variant_new_boolean(*paramValue
)));
136 action
= dont_AddRef(g_simple_action_new(actionName
.get(), nullptr));
139 g_signal_connect(action
, "change-state", G_CALLBACK(ChangeStateSignal
),
140 gpointer(&aMenuItem
));
142 g_signal_connect(action
, "activate", G_CALLBACK(ActivateSignal
),
143 gpointer(&aMenuItem
));
145 g_action_map_add_action(G_ACTION_MAP(mGroup
.get()), G_ACTION(action
.get()));
149 void Actions::Clear() {
150 for (size_t i
= 0; i
< mNextActionIndex
; ++i
) {
151 g_action_map_remove_action(G_ACTION_MAP(mGroup
.get()),
152 nsPrintfCString("item-%zu", i
).get());
154 mNextActionIndex
= 0;
157 class MenuModel final
: public nsStubMutationObserver
{
160 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
161 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
162 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
163 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
166 explicit MenuModel(dom::Element
* aElement
) : mElement(aElement
) {
167 mElement
->AddMutationObserver(this);
168 mGMenu
= dont_AddRef(g_menu_new());
169 mActions
.mGroup
= dont_AddRef(g_simple_action_group_new());
172 GMenuModel
* GetModel() { return G_MENU_MODEL(mGMenu
.get()); }
173 GActionGroup
* GetActionGroup() {
174 return G_ACTION_GROUP(mActions
.mGroup
.get());
177 dom::Element
* Element() { return mElement
; }
179 void RecomputeModelIfNeeded();
181 bool IsShowing() { return mPoppedUp
; }
184 RecomputeModelIfNeeded();
186 void DidHide() { mPoppedUp
= false; }
189 virtual ~MenuModel() { mElement
->RemoveMutationObserver(this); }
194 RecomputeModelIfNeeded();
198 RefPtr
<dom::Element
> mElement
;
199 RefPtr
<GMenu
> mGMenu
;
202 bool mPoppedUp
= false;
205 NS_IMPL_ISUPPORTS(MenuModel
, nsIMutationObserver
)
207 void MenuModel::ContentRemoved(nsIContent
* aChild
, nsIContent
*) {
208 if (NodeIsRelevant(*aChild
)) {
213 void MenuModel::ContentInserted(nsIContent
* aChild
) {
214 if (NodeIsRelevant(*aChild
)) {
219 void MenuModel::ContentAppended(nsIContent
* aChild
) {
220 if (NodeIsRelevant(*aChild
)) {
225 void MenuModel::AttributeChanged(dom::Element
* aElement
, int32_t aNameSpaceID
,
226 nsAtom
* aAttribute
, int32_t aModType
,
227 const nsAttrValue
* aOldValue
) {
228 if (NodeIsRelevant(*aElement
) &&
229 (aAttribute
== nsGkAtoms::label
|| aAttribute
== nsGkAtoms::aria_label
||
230 aAttribute
== nsGkAtoms::disabled
|| aAttribute
== nsGkAtoms::hidden
)) {
235 static const dom::Element
* GetMenuPopupChild(const dom::Element
& aElement
) {
236 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
237 child
= child
->GetNextSibling()) {
238 if (child
->IsXULElement(nsGkAtoms::menupopup
)) {
239 return child
->AsElement();
245 static void RecomputeModelFor(GMenu
* aMenu
, Actions
& aActions
,
246 const dom::Element
& aElement
) {
247 RefPtr
<GMenu
> sectionMenu
;
248 auto FlushSectionMenu
= [&] {
250 g_menu_append_section(aMenu
, nullptr, G_MENU_MODEL(sectionMenu
.get()));
251 sectionMenu
= nullptr;
255 for (const nsIContent
* child
= aElement
.GetFirstChild(); child
;
256 child
= child
->GetNextSibling()) {
257 if (child
->IsXULElement(nsGkAtoms::menuitem
) &&
258 !IsDisabled(*child
->AsElement())) {
260 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
261 if (label
.IsEmpty()) {
262 child
->AsElement()->GetAttr(nsGkAtoms::aria_label
, label
);
264 nsPrintfCString
actionName(
266 aActions
.Register(*child
->AsElement(), /* aForSubmenu = */ false)
268 g_menu_append(sectionMenu
? sectionMenu
.get() : aMenu
,
269 NS_ConvertUTF16toUTF8(label
).get(), actionName
.get());
272 if (child
->IsXULElement(nsGkAtoms::menuseparator
)) {
274 sectionMenu
= dont_AddRef(g_menu_new());
277 if (child
->IsXULElement(nsGkAtoms::menugroup
)) {
279 sectionMenu
= dont_AddRef(g_menu_new());
280 RecomputeModelFor(sectionMenu
, aActions
, *child
->AsElement());
284 if (child
->IsXULElement(nsGkAtoms::menu
) &&
285 !IsDisabled(*child
->AsElement())) {
286 if (const auto* popup
= GetMenuPopupChild(*child
->AsElement())) {
287 RefPtr
<GMenu
> submenu
= dont_AddRef(g_menu_new());
288 RecomputeModelFor(submenu
, aActions
, *popup
);
290 child
->AsElement()->GetAttr(nsGkAtoms::label
, label
);
291 RefPtr
<GMenuItem
> submenuItem
= dont_AddRef(g_menu_item_new_submenu(
292 NS_ConvertUTF16toUTF8(label
).get(), G_MENU_MODEL(submenu
.get())));
293 nsPrintfCString
actionName(
295 aActions
.Register(*popup
, /* aForSubmenu = */ true).get());
296 g_menu_item_set_attribute_value(submenuItem
.get(), "submenu-action",
297 g_variant_new_string(actionName
.get()));
298 g_menu_append_item(sectionMenu
? sectionMenu
.get() : aMenu
,
307 void MenuModel::RecomputeModelIfNeeded() {
312 g_menu_remove_all(mGMenu
.get());
313 RecomputeModelFor(mGMenu
.get(), mActions
, *mElement
);
316 static GtkMenuPopupAtRect
GetPopupAtRectFn() {
317 static GtkMenuPopupAtRect sFunc
=
318 (GtkMenuPopupAtRect
)dlsym(RTLD_DEFAULT
, "gtk_menu_popup_at_rect");
322 bool NativeMenuGtk::CanUse() {
323 return StaticPrefs::widget_gtk_native_context_menus() && GetPopupAtRectFn();
326 void NativeMenuGtk::FireEvent(EventMessage aPopupMessage
) {
327 RefPtr target
= Element();
328 widget::FireEvent(target
, aPopupMessage
);
331 #define METHOD_SIGNAL(name_) \
332 static MOZ_CAN_RUN_SCRIPT_BOUNDARY void On##name_##Signal( \
333 GtkWidget* widget, gpointer user_data) { \
334 RefPtr menu = static_cast<NativeMenuGtk*>(user_data); \
335 return menu->On##name_(); \
338 METHOD_SIGNAL(Unmap
);
342 NativeMenuGtk::NativeMenuGtk(dom::Element
* aElement
)
343 : mMenuModel(MakeRefPtr
<MenuModel
>(aElement
)) {
344 // Floating, so no need to dont_AddRef.
345 mNativeMenu
= gtk_menu_new_from_model(mMenuModel
->GetModel());
346 gtk_widget_insert_action_group(mNativeMenu
.get(), "menu",
347 mMenuModel
->GetActionGroup());
348 g_signal_connect(mNativeMenu
, "unmap", G_CALLBACK(OnUnmapSignal
), this);
351 NativeMenuGtk::~NativeMenuGtk() {
352 g_signal_handlers_disconnect_by_data(mNativeMenu
, this);
355 RefPtr
<dom::Element
> NativeMenuGtk::Element() { return mMenuModel
->Element(); }
357 void NativeMenuGtk::ShowAsContextMenu(nsPresContext
* aPc
,
358 const CSSIntPoint
& aPosition
) {
359 if (mMenuModel
->IsShowing()) {
362 RefPtr
<nsIWidget
> widget
= aPc
->GetRootWidget();
363 if (NS_WARN_IF(!widget
)) {
364 // XXX Do we need to close menus here?
367 auto* win
= static_cast<GdkWindow
*>(widget
->GetNativeData(NS_NATIVE_WINDOW
));
368 if (NS_WARN_IF(!win
)) {
372 auto* geckoWin
= static_cast<nsWindow
*>(widget
.get());
373 // The position needs to be relative to our window.
374 auto pos
= (aPosition
* aPc
->CSSToDevPixelScale()) -
375 geckoWin
->WidgetToScreenOffset();
376 auto gdkPos
= geckoWin
->DevicePixelsToGdkPointRoundDown(
377 LayoutDeviceIntPoint::Round(pos
));
379 mMenuModel
->WillShow();
380 const GdkRectangle rect
= {gdkPos
.x
, gdkPos
.y
, 1, 1};
381 auto openFn
= GetPopupAtRectFn();
382 openFn(GTK_MENU(mNativeMenu
.get()), win
, &rect
, GDK_GRAVITY_NORTH_WEST
,
383 GDK_GRAVITY_NORTH_WEST
, nullptr);
386 FireEvent(eXULPopupShown
);
389 bool NativeMenuGtk::Close() {
390 if (!mMenuModel
->IsShowing()) {
393 gtk_menu_popdown(GTK_MENU(mNativeMenu
.get()));
397 void NativeMenuGtk::OnUnmap() {
398 FireEvent(eXULPopupHiding
);
400 mMenuModel
->DidHide();
402 FireEvent(eXULPopupHidden
);
404 for (NativeMenu::Observer
* observer
: mObservers
.Clone()) {
405 observer
->OnNativeMenuClosed();
409 void NativeMenuGtk::ActivateItem(dom::Element
* aItemElement
, Modifiers
,
410 int16_t aButton
, ErrorResult
&) {
411 // TODO: For testing only.
414 void NativeMenuGtk::OpenSubmenu(dom::Element
*) {
415 // TODO: For testing mostly.
418 void NativeMenuGtk::CloseSubmenu(dom::Element
*) {
419 // TODO: For testing mostly.
422 } // namespace mozilla::widget