File: web_menu_runner_mac.mm

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (267 lines) | stat: -rw-r--r-- 10,723 bytes parent folder | download | duplicates (5)
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/app_shim_remote_cocoa/web_menu_runner_mac.h"

#include <AppKit/AppKit.h>
#include <Foundation/Foundation.h>
#include <objc/runtime.h>
#include <stddef.h>

#include <optional>

#include "base/base64.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"

namespace {

// A key to attach a MenuWasRunCallbackHolder to the NSView*.
static const char kMenuWasRunCallbackKey = 0;

}  // namespace

@interface MenuWasRunCallbackHolder : NSObject
@property MenuWasRunCallback callback;
@end

@implementation MenuWasRunCallbackHolder
@synthesize callback = _callback;
@end

@implementation WebMenuRunner {
  // The native menu.
  NSMenu* __strong _menu;

  // The index of the selected menu item.
  std::optional<int> _selectedMenuItemIndex;

  // The font size being used for the menu.
  CGFloat _fontSize;

  // Whether the menu should be displayed right-aligned.
  BOOL _rightAligned;
}

- (id)initWithItems:(const std::vector<blink::mojom::MenuItemPtr>&)items
           fontSize:(CGFloat)fontSize
       rightAligned:(BOOL)rightAligned {
  if ((self = [super init])) {
    _menu = [[NSMenu alloc] initWithTitle:@""];
    _menu.autoenablesItems = NO;
    _fontSize = fontSize;
    _rightAligned = rightAligned;
    for (const auto& item : items) {
      [self addItem:item];
    }
  }
  return self;
}

- (void)addItem:(const blink::mojom::MenuItemPtr&)item {
  if (item->type == blink::mojom::MenuItem::Type::kSeparator) {
    [_menu addItem:[NSMenuItem separatorItem]];
    return;
  }

  std::string label = item->label.value_or("");
  NSString* title = base::SysUTF8ToNSString(label);
  // https://crbug.com/40726719: SysUTF8ToNSString will return nil if the bits
  // that it is passed cannot be turned into a CFString. If this nil value is
  // passed to -[NSMenuItem addItemWithTitle:action:keyEquivalent:], Chromium
  // will crash. Therefore, for debugging, if the result is nil, substitute in
  // the raw bytes, encoded for safety in base64, to allow for investigation.
  if (!title) {
    title = base::SysUTF8ToNSString(base::Base64Encode(label));
  }

  // TODO(https://crbug.com/389084419): Figure out how to handle
  // blink::mojom::MenuItem::Type::kGroup items. This should use the macOS 14+
  // support for section headers, but popup menus have to resize themselves to
  // match the scale of the page, and there's no good way (currently) to get the
  // font used for section header items in order to scale it and set it.
  NSMenuItem* menuItem = [_menu addItemWithTitle:title
                                          action:@selector(menuItemSelected:)
                                   keyEquivalent:@""];

  if (item->tool_tip.has_value()) {
    menuItem.toolTip = base::SysUTF8ToNSString(item->tool_tip.value());
  }
  menuItem.enabled =
      item->enabled && item->type != blink::mojom::MenuItem::Type::kGroup;
  menuItem.target = self;

  // Set various alignment/language attributes.
  NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
  NSMutableParagraphStyle* paragraphStyle =
      [[NSMutableParagraphStyle alloc] init];
  paragraphStyle.alignment =
      _rightAligned ? NSTextAlignmentRight : NSTextAlignmentLeft;
  NSWritingDirection writingDirection =
      item->text_direction == base::i18n::RIGHT_TO_LEFT
          ? NSWritingDirectionRightToLeft
          : NSWritingDirectionLeftToRight;
  paragraphStyle.baseWritingDirection = writingDirection;
  paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
  attrs[NSParagraphStyleAttributeName] = paragraphStyle;

  if (item->has_text_direction_override) {
    attrs[NSWritingDirectionAttributeName] =
        @[ @(long{writingDirection} | NSWritingDirectionOverride) ];
  }

  attrs[NSFontAttributeName] = [NSFont menuFontOfSize:_fontSize];

  NSAttributedString* attrTitle =
      [[NSAttributedString alloc] initWithString:title attributes:attrs];
  menuItem.attributedTitle = attrTitle;

  // Set the title as well as the attributed title here. The attributed title
  // will be displayed in the menu, but type-ahead will use the non-attributed
  // string that doesn't contain any leading or trailing whitespace.
  //
  // This is the approach that WebKit uses; see PopupMenuMac::populate():
  // https://github.com/search?q=repo%3AWebKit/WebKit%20PopupMenuMac%3A%3Apopulate&type=code
  NSCharacterSet* whitespaceSet = NSCharacterSet.whitespaceCharacterSet;
  menuItem.title = [title stringByTrimmingCharactersInSet:whitespaceSet];

  menuItem.tag = _menu.numberOfItems - 1;
}

- (std::optional<int>)selectedMenuItemIndex {
  return _selectedMenuItemIndex;
}

- (void)menuItemSelected:(id)sender {
  _selectedMenuItemIndex = [sender tag];
}

- (void)runMenuInView:(NSView*)view
           withBounds:(NSRect)bounds
         initialIndex:(int)index {
  // In a testing situation, make the callback and early-exit.
  MenuWasRunCallbackHolder* holder =
      objc_getAssociatedObject(view, &kMenuWasRunCallbackKey);
  if (holder) {
    holder.callback.Run(view, bounds, index);
    return;
  }

  // Using NSPopUpButtonCell in this way is not SPI, but there is new(er) API to
  // show a pop-up menu in a way that avoids the hassle of instantiating a cell
  // just to use its innards.
  //
  // However, that API, -[NSMenu popUpMenuPositioningItem:atLocation:inView:],
  // is broken and displays menus that are the incorrect width and which
  // improperly truncate their contents (see https://crbug.com/401443090).
  //
  // This has been filed as FB16843355. TODO(https://crbug.com/389067059): When
  // this FB is resolved, switch to the new API by relanding an adapted version
  // of https://crrev.com/c/6173642.
  //
  // In addition, note that there are web pages that use popups with a font size
  // of 0. When relanding, font size will likely play a part in the calculation
  // of the menu position of the reland, so be sure to not regress menu
  // positioning in that case (https://crbug.com/404294118).

  // Set up the button cell, converting to NSView coordinates. The menu is
  // positioned such that the currently selected menu item appears over the
  // popup button, which is the expected Mac popup menu behavior.
  NSPopUpButtonCell* cell = [[NSPopUpButtonCell alloc] initTextCell:@""
                                                          pullsDown:NO];
  cell.menu = _menu;
  // Use -selectItemWithTag: so if the index is out-of-bounds nothing bad
  // happens.
  [cell selectItemWithTag:index];

  if (_rightAligned) {
    cell.userInterfaceLayoutDirection =
        NSUserInterfaceLayoutDirectionRightToLeft;
    _menu.userInterfaceLayoutDirection =
        NSUserInterfaceLayoutDirectionRightToLeft;
  }

  // When popping up a menu near the Dock, Cocoa restricts the menu size to not
  // overlap the Dock, with a scroll arrow. At a certain point, though, this
  // doesn't work, so the menu is repositioned, so that the current item can be
  // selected without mouse-tracking selecting a different item immediately.
  //
  // Unfortunately, in that situation, the cell will try to reposition the menu
  // relative to the view passed in, as it believes that the view is the
  // NSPopUpButton control. However, `view` is the view containing the entire
  // web page, so if it were to be passed in, the menu would be repositioned
  // relative to that, and would end up being wildly misplaced.
  //
  // Therefore, set up a fake "control" view corresponding to the visual bounds
  // of the HTML element, so that if the menu needs to be repositioned, it is
  // repositioned relative to that.
  NSView* fakeControlView = [[NSView alloc] initWithFrame:bounds];
  [view addSubview:fakeControlView];

  // Display the menu.
  [cell attachPopUpWithFrame:fakeControlView.bounds inView:fakeControlView];
  [cell performClickWithFrame:fakeControlView.bounds inView:fakeControlView];

  [fakeControlView removeFromSuperview];
}

- (void)cancelSynchronously {
  [_menu cancelTrackingWithoutAnimation];

  // Starting with macOS 14, menus were reimplemented with Cocoa (rather than
  // with the old Carbon). However, in macOS 14, with that reimplementation came
  // a bug whereupon using -cancelTrackingWithoutAnimation did not consistently
  // immediately cancel the tracking, and left associated state remaining
  // uncleared for an indeterminate amount of time. If a new tracking session
  // began before that state was cleared, an NSInternalInconsistencyException
  // was thrown. See the discussion on https://crbug.com/40939221 and
  // FB13320260.
  //
  // On macOS 14, therefore, when cancelling synchronously, clear out that state
  // so that a new tracking session can begin immediately.
  //
  // With macOS 15, these global state methods moved from being class methods on
  // NSPopupMenuWindow to being instance methods on NSMenuTrackingSession, so
  // this workaround is inapplicable.
  if (base::mac::MacOSMajorVersion() == 14) {
    // When running a menu tracking session, the instances of
    // NSMenuTrackingSession make calls to class methods of NSPopupMenuWindow:
    //
    // -[NSMenuTrackingSession sendBeginTrackingNotifications]
    //   -> +[NSPopupMenuWindow enableWindowReuse]
    // and
    // -[NSMenuTrackingSession sendEndTrackingNotifications]
    //   -> +[NSPopupMenuWindow disableWindowReusePurgingCache]
    //
    // +enableWindowReuse populates the _NSContextMenuWindowReuseSet global, and
    // +disableWindowReusePurgingCache walks the set, clears out some state
    // inside of each item, and then nils out the global, preparing for the next
    // call to +enableWindowReuse.
    //
    // +disableWindowReusePurgingCache can be called directly here, as it's
    // idempotent enough.

    Class popupMenuWindowClass = NSClassFromString(@"NSPopupMenuWindow");
    if ([popupMenuWindowClass
            respondsToSelector:@selector(disableWindowReusePurgingCache)]) {
      [popupMenuWindowClass
          performSelector:@selector(disableWindowReusePurgingCache)];
    }
  }
}

+ (void)registerForTestingMenuRunCallback:(MenuWasRunCallback)callback
                                  forView:(NSView*)view {
  MenuWasRunCallbackHolder* holder = [[MenuWasRunCallbackHolder alloc] init];
  holder.callback = callback;
  objc_setAssociatedObject(view, &kMenuWasRunCallbackKey, holder,
                           OBJC_ASSOCIATION_RETAIN);
}

+ (void)unregisterForTestingMenuRunCallbackForView:(NSView*)view {
  objc_setAssociatedObject(view, &kMenuWasRunCallbackKey, nil,
                           OBJC_ASSOCIATION_RETAIN);
}

@end  // WebMenuRunner