Backed out 3 changesets (bug 1870106, bug 1845276) for causing doc generate failures...
[gecko.git] / widget / gtk / NativeMenuGtk.cpp
blob9d413d475e6ec03c38651fc523f2e8e0f500097c
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"
14 #include "nsWindow.h"
15 #include "nsStubMutationObserver.h"
16 #include "mozilla/dom/Element.h"
17 #include "mozilla/StaticPrefs_widget.h"
19 #include <dlfcn.h>
20 #include <gtk/gtk.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,
46 eCaseMatters)) {
47 case 0:
48 break;
49 case 1:
50 break;
51 default:
52 return Nothing();
55 return Some(aMenuItem.AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
56 nsGkAtoms::_true, eCaseMatters));
59 struct Actions {
60 RefPtr<GSimpleActionGroup> mGroup;
61 size_t mNextActionIndex = 0;
63 nsPrintfCString Register(const dom::Element&, bool aForSubmenu);
64 void Clear();
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;
72 if (newValue) {
73 aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns,
74 true);
75 } else {
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())) {
90 return;
92 aElement.DispatchEvent(*event);
95 static MOZ_CAN_RUN_SCRIPT void ActivateSignal(GSimpleAction* aAction,
96 GVariant* aParam,
97 gpointer aUserData) {
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,
110 GVariant* aParam,
111 gpointer aUserData) {
112 // TODO: Fire events when safe. These run at a bad time for now.
113 static constexpr bool kEnabled = false;
114 if (!kEnabled) {
115 return;
117 const bool open = g_variant_get_boolean(aParam);
118 RefPtr popup = static_cast<dom::Element*>(aUserData);
119 if (open) {
120 FireEvent(popup, eXULPopupShowing);
121 FireEvent(popup, eXULPopupShown);
122 } else {
123 FireEvent(popup, eXULPopupHiding);
124 FireEvent(popup, eXULPopupHidden);
128 nsPrintfCString Actions::Register(const dom::Element& aMenuItem,
129 bool aForSubmenu) {
130 nsPrintfCString actionName("item-%zu", mNextActionIndex++);
131 Maybe<bool> paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem);
132 RefPtr<GSimpleAction> action;
133 if (paramValue) {
134 action = dont_AddRef(g_simple_action_new_stateful(
135 actionName.get(), nullptr, g_variant_new_boolean(*paramValue)));
136 } else {
137 action = dont_AddRef(g_simple_action_new(actionName.get(), nullptr));
139 if (aForSubmenu) {
140 g_signal_connect(action, "change-state", G_CALLBACK(ChangeStateSignal),
141 gpointer(&aMenuItem));
142 } else {
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()));
147 return actionName;
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 {
159 NS_DECL_ISUPPORTS
161 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
162 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
163 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
164 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
166 public:
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; }
183 void WillShow() {
184 mPoppedUp = true;
185 RecomputeModelIfNeeded();
187 void DidHide() { mPoppedUp = false; }
189 private:
190 virtual ~MenuModel() { mElement->RemoveMutationObserver(this); }
192 void DirtyModel() {
193 mDirty = true;
194 if (mPoppedUp) {
195 RecomputeModelIfNeeded();
199 RefPtr<dom::Element> mElement;
200 RefPtr<GMenu> mGMenu;
201 Actions mActions;
202 bool mDirty = true;
203 bool mPoppedUp = false;
206 NS_IMPL_ISUPPORTS(MenuModel, nsIMutationObserver)
208 void MenuModel::ContentRemoved(nsIContent* aChild, nsIContent*) {
209 if (NodeIsRelevant(*aChild)) {
210 DirtyModel();
214 void MenuModel::ContentInserted(nsIContent* aChild) {
215 if (NodeIsRelevant(*aChild)) {
216 DirtyModel();
220 void MenuModel::ContentAppended(nsIContent* aChild) {
221 if (NodeIsRelevant(*aChild)) {
222 DirtyModel();
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)) {
232 DirtyModel();
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();
243 return nullptr;
246 static void RecomputeModelFor(GMenu* aMenu, Actions& aActions,
247 const dom::Element& aElement) {
248 RefPtr<GMenu> sectionMenu;
249 auto FlushSectionMenu = [&] {
250 if (sectionMenu) {
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())) {
260 nsAutoString label;
261 child->AsElement()->GetAttr(nsGkAtoms::label, label);
262 if (label.IsEmpty()) {
263 child->AsElement()->GetAttr(nsGkAtoms::aria_label, label);
265 nsPrintfCString actionName(
266 "menu.%s",
267 aActions.Register(*child->AsElement(), /* aForSubmenu = */ false)
268 .get());
269 g_menu_append(sectionMenu ? sectionMenu.get() : aMenu,
270 NS_ConvertUTF16toUTF8(label).get(), actionName.get());
271 continue;
273 if (child->IsXULElement(nsGkAtoms::menuseparator)) {
274 FlushSectionMenu();
275 sectionMenu = dont_AddRef(g_menu_new());
276 continue;
278 if (child->IsXULElement(nsGkAtoms::menugroup)) {
279 FlushSectionMenu();
280 sectionMenu = dont_AddRef(g_menu_new());
281 RecomputeModelFor(sectionMenu, aActions, *child->AsElement());
282 FlushSectionMenu();
283 continue;
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);
290 nsAutoString label;
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(
295 "menu.%s",
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,
300 submenuItem.get());
305 FlushSectionMenu();
308 void MenuModel::RecomputeModelIfNeeded() {
309 if (!mDirty) {
310 return;
312 mActions.Clear();
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");
320 return sFunc;
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);
341 #undef METHOD_SIGNAL
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()) {
362 return;
364 RefPtr<nsIWidget> widget = aClickedFrame->PresContext()->GetRootWidget();
365 if (NS_WARN_IF(!widget)) {
366 // XXX Do we need to close menus here?
367 return;
369 auto* win = static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
370 if (NS_WARN_IF(!win)) {
371 return;
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());
387 RefPtr pin{this};
388 FireEvent(eXULPopupShown);
391 bool NativeMenuGtk::Close() {
392 if (!mMenuModel->IsShowing()) {
393 return false;
395 gtk_menu_popdown(GTK_MENU(mNativeMenu.get()));
396 return true;
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