Bug 1854550 - pt 12. Allow inlining between mozjemalloc and PHC r=glandium
[gecko.git] / widget / cocoa / MOZMenuOpeningCoordinator.mm
blob6b17870940813d88f8c1480fd217cff55aba60a0
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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 /*
7  * Makes sure that the nested event loop for NSMenu tracking is situated as low
8  * on the stack as possible, and that two NSMenu event loops are never nested.
9  */
11 #include "MOZMenuOpeningCoordinator.h"
13 #include "mozilla/ClearOnShutdown.h"
14 #include "mozilla/StaticPrefs_widget.h"
16 #include "nsCocoaFeatures.h"
17 #include "nsCocoaUtils.h"
18 #include "nsMenuX.h"
19 #include "nsObjCExceptions.h"
21 static BOOL sNeedToUnwindForMenuClosing = NO;
23 @interface MOZMenuOpeningInfo : NSObject
24 @property NSInteger handle;
25 @property(retain) NSMenu* menu;
26 @property NSPoint position;
27 @property(retain) NSView* view;
28 @property(retain) NSAppearance* appearance;
29 @property BOOL isContextMenu;
30 @end
32 @implementation MOZMenuOpeningInfo
33 @end
35 @implementation MOZMenuOpeningCoordinator {
36   // non-nil between asynchronouslyOpenMenu:atScreenPosition:forView: and the
37   // time at at which it is unqueued in _runMenu.
38   MOZMenuOpeningInfo* mPendingOpening;  // strong
40   // An incrementing counter
41   NSInteger mLastHandle;
43   // YES while _runMenu is on the stack
44   BOOL mRunMenuIsOnTheStack;
47 + (instancetype)sharedInstance {
48   static MOZMenuOpeningCoordinator* sInstance = nil;
49   if (!sInstance) {
50     sInstance = [[MOZMenuOpeningCoordinator alloc] init];
51     mozilla::RunOnShutdown([&]() {
52       [sInstance release];
53       sInstance = nil;
54     });
55   }
56   return sInstance;
59 - (void)dealloc {
60   MOZ_RELEASE_ASSERT(!mPendingOpening, "should be empty at shutdown");
61   [super dealloc];
64 - (NSInteger)asynchronouslyOpenMenu:(NSMenu*)aMenu
65                    atScreenPosition:(NSPoint)aPosition
66                             forView:(NSView*)aView
67                      withAppearance:(NSAppearance*)aAppearance
68                       asContextMenu:(BOOL)aIsContextMenu {
69   MOZ_RELEASE_ASSERT(!mPendingOpening,
70                      "A menu is already waiting to open. Before opening the "
71                      "next one, either wait "
72                      "for this one to open or cancel the request.");
74   NSInteger handle = ++mLastHandle;
76   MOZMenuOpeningInfo* info = [[MOZMenuOpeningInfo alloc] init];
77   info.handle = handle;
78   info.menu = aMenu;
79   info.position = aPosition;
80   info.view = aView;
81   info.appearance = aAppearance;
82   info.isContextMenu = aIsContextMenu;
83   mPendingOpening = [info retain];
84   [info release];
86   if (!mRunMenuIsOnTheStack) {
87     // Call _runMenu from the event loop, so that it doesn't block this call.
88     [self performSelector:@selector(_runMenu) withObject:nil afterDelay:0.0];
89   }
91   return handle;
94 - (void)_runMenu {
95   MOZ_RELEASE_ASSERT(!mRunMenuIsOnTheStack);
97   mRunMenuIsOnTheStack = YES;
99   while (mPendingOpening) {
100     MOZMenuOpeningInfo* info = [mPendingOpening retain];
101     [mPendingOpening release];
102     mPendingOpening = nil;
104     @try {
105       [self _openMenu:info.menu
106           atScreenPosition:info.position
107                    forView:info.view
108             withAppearance:info.appearance
109              asContextMenu:info.isContextMenu];
110     } @catch (NSException* exception) {
111       nsObjCExceptionLog(exception);
112     }
114     [info release];
116     // We have exited _openMenu's nested event loop.
117     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = NO;
118   }
120   mRunMenuIsOnTheStack = NO;
123 - (void)cancelAsynchronousOpening:(NSInteger)aHandle {
124   if (mPendingOpening && mPendingOpening.handle == aHandle) {
125     [mPendingOpening release];
126     mPendingOpening = nil;
127   }
130 - (void)_openMenu:(NSMenu*)aMenu
131     atScreenPosition:(NSPoint)aPosition
132              forView:(NSView*)aView
133       withAppearance:(NSAppearance*)aAppearance
134        asContextMenu:(BOOL)aIsContextMenu {
135   // There are multiple ways to display an NSMenu as a context menu.
136   //
137   //  1. We can return the NSMenu from -[ChildView menuForEvent:] and the NSView
138   //     will open it for us.
139   //  2. We can call +[NSMenu popUpContextMenu:withEvent:forView:] inside a
140   //     mouseDown handler with a real mouse down event.
141   //  3. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at a later
142   //     time, with a real mouse event that we stored earlier.
143   //  4. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at any time,
144   //     with a synthetic mouse event that we create just for that purpose.
145   //  5. We can call -[NSMenu popUpMenuPositioningItem:atLocation:inView:] and
146   //     it just takes a position, not an event.
147   //
148   // 1-4 look the same, 5 looks different: 5 is made for use with NSPopUpButton,
149   // where the selected item needs to be shown at a specific position. If a tall
150   // menu is opened with a position close to the bottom edge of the screen, 5
151   // results in a cropped menu with scroll arrows, even if the entire menu would
152   // fit on the screen, due to the positioning constraint. 1-2 only work if the
153   // menu contents are known synchronously during the call to menuForEvent or
154   // during the mouseDown event handler.
155   // NativeMenuMac::ShowAsContextMenu can be called at any time. It could be
156   // called during a menuForEvent call (during a "contextmenu" event handler),
157   // or during a mouseDown handler, or at a later time. The code below uses
158   // option 4 as the preferred option for context menus because it's the
159   // simplest: It works in all scenarios and it doesn't have the drawbacks of
160   // option 5. For popups that aren't context menus and that should be
161   // positioned as close as possible to the given screen position, we use
162   // option 5.
164   if (aAppearance) {
165     if (@available(macOS 11.0, *)) {
166       // By default, NSMenu inherits its appearance from the opening NSEvent's
167       // window. If CSS has overridden it, on Big Sur + we can respect it with
168       // -[NSMenu setAppearance].
169       aMenu.appearance = aAppearance;
170     }
171   }
173   if (aView) {
174     NSWindow* window = aView.window;
175     NSPoint locationInWindow =
176         nsCocoaUtils::ConvertPointFromScreen(window, aPosition);
177     if (aIsContextMenu) {
178       // Create a synthetic event at the right location and open the menu
179       // [option 4].
180       NSEvent* event =
181           [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
182                              location:locationInWindow
183                         modifierFlags:0
184                             timestamp:NSProcessInfo.processInfo.systemUptime
185                          windowNumber:window.windowNumber
186                               context:nil
187                           eventNumber:0
188                            clickCount:1
189                              pressure:0.0f];
190       [NSMenu popUpContextMenu:aMenu withEvent:event forView:aView];
191     } else {
192       // For popups which are not context menus, we open the menu using [option
193       // 5]. We pass `nil` to indicate that we're positioning the top left
194       // corner of the menu. This path is used for anchored menupopups, so we
195       // prefer option 5 over option 4 so that the menu doesn't get flipped if
196       // space is tight.
197       NSPoint locationInView = [aView convertPoint:locationInWindow
198                                           fromView:nil];
199       [aMenu popUpMenuPositioningItem:nil
200                            atLocation:locationInView
201                                inView:aView];
202     }
203   } else {
204     // Open the menu using popUpMenuPositioningItem:atLocation:inView: [option
205     // 5]. This is not preferred, because it positions the menu differently from
206     // how a native context menu would be positioned; it enforces aPosition for
207     // the top left corner even if this means that the menu will be displayed in
208     // a clipped fashion with scroll arrows.
209     [aMenu popUpMenuPositioningItem:nil atLocation:aPosition inView:nil];
210   }
213 + (void)setNeedToUnwindForMenuClosing:(BOOL)aValue {
214   sNeedToUnwindForMenuClosing = aValue;
217 + (BOOL)needToUnwindForMenuClosing {
218   return sNeedToUnwindForMenuClosing;
221 @end