Bug 1751217 Part 3: Make HDR-capable macOS screens report 30 pixelDepth. r=mstange
[gecko.git] / widget / gtk / NativeMenuGtk.cpp
blobf29dd7b0d3be1bc7fffa59a87e0856a9e0b6a79e
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"
13 #include "nsWindow.h"
14 #include "nsStubMutationObserver.h"
15 #include "mozilla/dom/Element.h"
16 #include "mozilla/StaticPrefs_widget.h"
18 #include <dlfcn.h>
19 #include <gtk/gtk.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,
45 eCaseMatters)) {
46 case 0:
47 break;
48 case 1:
49 break;
50 default:
51 return Nothing();
54 return Some(aMenuItem.AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
55 nsGkAtoms::_true, eCaseMatters));
58 struct Actions {
59 RefPtr<GSimpleActionGroup> mGroup;
60 size_t mNextActionIndex = 0;
62 nsPrintfCString Register(const dom::Element&, bool aForSubmenu);
63 void Clear();
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;
71 if (newValue) {
72 aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns,
73 true);
74 } else {
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())) {
89 return;
91 aElement.DispatchEvent(*event);
94 static MOZ_CAN_RUN_SCRIPT void ActivateSignal(GSimpleAction* aAction,
95 GVariant* aParam,
96 gpointer aUserData) {
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,
109 GVariant* aParam,
110 gpointer aUserData) {
111 // TODO: Fire events when safe. These run at a bad time for now.
112 static constexpr bool kEnabled = false;
113 if (!kEnabled) {
114 return;
116 const bool open = g_variant_get_boolean(aParam);
117 RefPtr popup = static_cast<dom::Element*>(aUserData);
118 if (open) {
119 FireEvent(popup, eXULPopupShowing);
120 FireEvent(popup, eXULPopupShown);
121 } else {
122 FireEvent(popup, eXULPopupHiding);
123 FireEvent(popup, eXULPopupHidden);
127 nsPrintfCString Actions::Register(const dom::Element& aMenuItem,
128 bool aForSubmenu) {
129 nsPrintfCString actionName("item-%zu", mNextActionIndex++);
130 Maybe<bool> paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem);
131 RefPtr<GSimpleAction> action;
132 if (paramValue) {
133 action = dont_AddRef(g_simple_action_new_stateful(
134 actionName.get(), nullptr, g_variant_new_boolean(*paramValue)));
135 } else {
136 action = dont_AddRef(g_simple_action_new(actionName.get(), nullptr));
138 if (aForSubmenu) {
139 g_signal_connect(action, "change-state", G_CALLBACK(ChangeStateSignal),
140 gpointer(&aMenuItem));
141 } else {
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()));
146 return actionName;
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 {
158 NS_DECL_ISUPPORTS
160 NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
161 NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
162 NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
163 NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
165 public:
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; }
182 void WillShow() {
183 mPoppedUp = true;
184 RecomputeModelIfNeeded();
186 void DidHide() { mPoppedUp = false; }
188 private:
189 virtual ~MenuModel() { mElement->RemoveMutationObserver(this); }
191 void DirtyModel() {
192 mDirty = true;
193 if (mPoppedUp) {
194 RecomputeModelIfNeeded();
198 RefPtr<dom::Element> mElement;
199 RefPtr<GMenu> mGMenu;
200 Actions mActions;
201 bool mDirty = true;
202 bool mPoppedUp = false;
205 NS_IMPL_ISUPPORTS(MenuModel, nsIMutationObserver)
207 void MenuModel::ContentRemoved(nsIContent* aChild, nsIContent*) {
208 if (NodeIsRelevant(*aChild)) {
209 DirtyModel();
213 void MenuModel::ContentInserted(nsIContent* aChild) {
214 if (NodeIsRelevant(*aChild)) {
215 DirtyModel();
219 void MenuModel::ContentAppended(nsIContent* aChild) {
220 if (NodeIsRelevant(*aChild)) {
221 DirtyModel();
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)) {
231 DirtyModel();
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();
242 return nullptr;
245 static void RecomputeModelFor(GMenu* aMenu, Actions& aActions,
246 const dom::Element& aElement) {
247 RefPtr<GMenu> sectionMenu;
248 auto FlushSectionMenu = [&] {
249 if (sectionMenu) {
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())) {
259 nsAutoString label;
260 child->AsElement()->GetAttr(nsGkAtoms::label, label);
261 if (label.IsEmpty()) {
262 child->AsElement()->GetAttr(nsGkAtoms::aria_label, label);
264 nsPrintfCString actionName(
265 "menu.%s",
266 aActions.Register(*child->AsElement(), /* aForSubmenu = */ false)
267 .get());
268 g_menu_append(sectionMenu ? sectionMenu.get() : aMenu,
269 NS_ConvertUTF16toUTF8(label).get(), actionName.get());
270 continue;
272 if (child->IsXULElement(nsGkAtoms::menuseparator)) {
273 FlushSectionMenu();
274 sectionMenu = dont_AddRef(g_menu_new());
275 continue;
277 if (child->IsXULElement(nsGkAtoms::menugroup)) {
278 FlushSectionMenu();
279 sectionMenu = dont_AddRef(g_menu_new());
280 RecomputeModelFor(sectionMenu, aActions, *child->AsElement());
281 FlushSectionMenu();
282 continue;
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);
289 nsAutoString label;
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(
294 "menu.%s",
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,
299 submenuItem.get());
304 FlushSectionMenu();
307 void MenuModel::RecomputeModelIfNeeded() {
308 if (!mDirty) {
309 return;
311 mActions.Clear();
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");
319 return sFunc;
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);
340 #undef METHOD_SIGNAL
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()) {
360 return;
362 RefPtr<nsIWidget> widget = aPc->GetRootWidget();
363 if (NS_WARN_IF(!widget)) {
364 // XXX Do we need to close menus here?
365 return;
367 auto* win = static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
368 if (NS_WARN_IF(!win)) {
369 return;
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);
385 RefPtr pin{this};
386 FireEvent(eXULPopupShown);
389 bool NativeMenuGtk::Close() {
390 if (!mMenuModel->IsShowing()) {
391 return false;
393 gtk_menu_popdown(GTK_MENU(mNativeMenu.get()));
394 return true;
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