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