Flutter Engine
The Flutter Engine
FlutterMenuPlugin.mm
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
6
7#include <map>
8
9#import "flutter/shell/platform/common/platform_provided_menu.h"
10#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
11#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
12
13// Channel constants
14static NSString* const kChannelName = @"flutter/menu";
15static NSString* const kIsPluginAvailableMethod = @"Menu.isPluginAvailable";
16static NSString* const kMenuSetMenusMethod = @"Menu.setMenus";
17static NSString* const kMenuSelectedCallbackMethod = @"Menu.selectedCallback";
18static NSString* const kMenuOpenedMethod = @"Menu.opened";
19static NSString* const kMenuClosedMethod = @"Menu.closed";
20
21// Serialization keys for menu objects
22static NSString* const kIdKey = @"id";
23static NSString* const kLabelKey = @"label";
24static NSString* const kEnabledKey = @"enabled";
25static NSString* const kChildrenKey = @"children";
26static NSString* const kDividerKey = @"isDivider";
27static NSString* const kShortcutCharacterKey = @"shortcutCharacter";
28static NSString* const kShortcutTriggerKey = @"shortcutTrigger";
29static NSString* const kShortcutModifiersKey = @"shortcutModifiers";
30static NSString* const kPlatformProvidedMenuKey = @"platformProvidedMenu";
31
32// Key shortcut constants
33constexpr int kFlutterShortcutModifierMeta = 1 << 0;
34constexpr int kFlutterShortcutModifierShift = 1 << 1;
35constexpr int kFlutterShortcutModifierAlt = 1 << 2;
36constexpr int kFlutterShortcutModifierControl = 1 << 3;
37
38constexpr uint64_t kFlutterKeyIdPlaneMask = 0xff00000000l;
39constexpr uint64_t kFlutterKeyIdUnicodePlane = 0x0000000000l;
40constexpr uint64_t kFlutterKeyIdValueMask = 0x00ffffffffl;
41
42static const NSDictionary* logicalKeyToKeyCode = {};
43
44// What to look for in menu titles to replace with the application name.
45static NSString* const kAppName = @"APP_NAME";
46
47// Odd facts about AppKit key equivalents:
48//
49// 1) ⌃⇧1 and ⇧1 cannot exist in the same app, or the former triggers the latter’s
50// action.
51// 2) ⌃⌥⇧1 and ⇧1 cannot exist in the same app, or the former triggers the latter’s
52// action.
53// 3) ⌃⌥⇧1 and ⌃⇧1 cannot exist in the same app, or the former triggers the latter’s
54// action.
55// 4) ⌃⇧a is equivalent to ⌃A: If a keyEquivalent is a capitalized alphabetical
56// letter and keyEquivalentModifierMask does not include
57// NSEventModifierFlagShift, AppKit will add ⇧ automatically in the UI.
58
59/**
60 * Maps the string used by NSMenuItem for the given special key equivalent.
61 * Keys are the logical key ids of matching trigger keys.
62 */
63static NSDictionary<NSNumber*, NSNumber*>* GetMacOsSpecialKeys() {
64 return @{
65 @0x00100000008 : [NSNumber numberWithInt:NSBackspaceCharacter],
66 @0x00100000009 : [NSNumber numberWithInt:NSTabCharacter],
67 @0x0010000000a : [NSNumber numberWithInt:NSNewlineCharacter],
68 @0x0010000000c : [NSNumber numberWithInt:NSFormFeedCharacter],
69 @0x0010000000d : [NSNumber numberWithInt:NSCarriageReturnCharacter],
70 @0x0010000007f : [NSNumber numberWithInt:NSDeleteCharacter],
71 @0x00100000801 : [NSNumber numberWithInt:NSF1FunctionKey],
72 @0x00100000802 : [NSNumber numberWithInt:NSF2FunctionKey],
73 @0x00100000803 : [NSNumber numberWithInt:NSF3FunctionKey],
74 @0x00100000804 : [NSNumber numberWithInt:NSF4FunctionKey],
75 @0x00100000805 : [NSNumber numberWithInt:NSF5FunctionKey],
76 @0x00100000806 : [NSNumber numberWithInt:NSF6FunctionKey],
77 @0x00100000807 : [NSNumber numberWithInt:NSF7FunctionKey],
78 @0x00100000808 : [NSNumber numberWithInt:NSF8FunctionKey],
79 @0x00100000809 : [NSNumber numberWithInt:NSF9FunctionKey],
80 @0x0010000080a : [NSNumber numberWithInt:NSF10FunctionKey],
81 @0x0010000080b : [NSNumber numberWithInt:NSF11FunctionKey],
82 @0x0010000080c : [NSNumber numberWithInt:NSF12FunctionKey],
83 @0x0010000080d : [NSNumber numberWithInt:NSF13FunctionKey],
84 @0x0010000080e : [NSNumber numberWithInt:NSF14FunctionKey],
85 @0x0010000080f : [NSNumber numberWithInt:NSF15FunctionKey],
86 @0x00100000810 : [NSNumber numberWithInt:NSF16FunctionKey],
87 @0x00100000811 : [NSNumber numberWithInt:NSF17FunctionKey],
88 @0x00100000812 : [NSNumber numberWithInt:NSF18FunctionKey],
89 @0x00100000813 : [NSNumber numberWithInt:NSF19FunctionKey],
90 @0x00100000814 : [NSNumber numberWithInt:NSF20FunctionKey],
91
92 // For some reason, there don't appear to be constants for these in ObjC. In
93 // Swift, there is a class with static members for these: KeyEquivalent. The
94 // values below are taken from that (where they don't already appear above).
95 @0x00100000302 : @0xf702, // ArrowLeft
96 @0x00100000303 : @0xf703, // ArrowRight
97 @0x00100000304 : @0xf700, // ArrowUp
98 @0x00100000301 : @0xf701, // ArrowDown
99 @0x00100000306 : @0xf729, // Home
100 @0x00100000305 : @0xf72B, // End
101 @0x00100000308 : @0xf72c, // PageUp
102 @0x00100000307 : @0xf72d, // PageDown
103 @0x0010000001b : @0x001B, // Escape
104 };
105}
106
107/**
108 * The mapping from the PlatformProvidedMenu enum to the macOS selectors for the provided
109 * menus.
110 */
111static const std::map<flutter::PlatformProvidedMenu, SEL> GetMacOSProvidedMenus() {
112 return {
113 {flutter::PlatformProvidedMenu::kAbout, @selector(orderFrontStandardAboutPanel:)},
114 {flutter::PlatformProvidedMenu::kQuit, @selector(terminate:)},
115 // servicesSubmenu is handled specially below: it is assumed to be the first
116 // submenu in the preserved platform provided menus, since it doesn't have a
117 // definitive selector like the rest.
118 {flutter::PlatformProvidedMenu::kServicesSubmenu, @selector(submenuAction:)},
120 {flutter::PlatformProvidedMenu::kHideOtherApplications, @selector(hideOtherApplications:)},
121 {flutter::PlatformProvidedMenu::kShowAllApplications, @selector(unhideAllApplications:)},
122 {flutter::PlatformProvidedMenu::kStartSpeaking, @selector(startSpeaking:)},
123 {flutter::PlatformProvidedMenu::kStopSpeaking, @selector(stopSpeaking:)},
124 {flutter::PlatformProvidedMenu::kToggleFullScreen, @selector(toggleFullScreen:)},
125 {flutter::PlatformProvidedMenu::kMinimizeWindow, @selector(performMiniaturize:)},
126 {flutter::PlatformProvidedMenu::kZoomWindow, @selector(performZoom:)},
127 {flutter::PlatformProvidedMenu::kArrangeWindowsInFront, @selector(arrangeInFront:)},
128 };
129}
130
131/**
132 * Returns the NSEventModifierFlags of |modifiers|, a value from
133 * kShortcutKeyModifiers.
134 */
135static NSEventModifierFlags KeyEquivalentModifierMaskForModifiers(NSNumber* modifiers) {
136 int flutterModifierFlags = modifiers.intValue;
137 NSEventModifierFlags flags = 0;
138 if (flutterModifierFlags & kFlutterShortcutModifierMeta) {
139 flags |= NSEventModifierFlagCommand;
140 }
141 if (flutterModifierFlags & kFlutterShortcutModifierShift) {
142 flags |= NSEventModifierFlagShift;
143 }
144 if (flutterModifierFlags & kFlutterShortcutModifierAlt) {
145 flags |= NSEventModifierFlagOption;
146 }
147 if (flutterModifierFlags & kFlutterShortcutModifierControl) {
148 flags |= NSEventModifierFlagControl;
149 }
150 // There are also modifier flags for things like the function (Fn) key, but
151 // the framework doesn't support those.
152 return flags;
153}
154
155/**
156 * An NSMenuDelegate used to listen for changes in the menu when it opens and
157 * closes.
158 */
159@interface FlutterMenuDelegate : NSObject <NSMenuDelegate>
160/**
161 * When this delegate receives notification that the menu opened or closed, it
162 * will send a message on the given channel to that effect for the menu item
163 * with the given id (the ID comes from the data supplied by the framework to
164 * |FlutterMenuPlugin.setMenus|).
165 */
166- (instancetype)initWithIdentifier:(int64_t)identifier channel:(FlutterMethodChannel*)channel;
167@end
168
169@implementation FlutterMenuDelegate {
171 int64_t _identifier;
172}
173
174- (instancetype)initWithIdentifier:(int64_t)identifier channel:(FlutterMethodChannel*)channel {
175 self = [super init];
176 if (self) {
178 _channel = channel;
179 }
180 return self;
181}
182
183- (void)menuWillOpen:(NSMenu*)menu {
184 [_channel invokeMethod:kMenuOpenedMethod arguments:@(_identifier)];
185}
186
187- (void)menuDidClose:(NSMenu*)menu {
188 [_channel invokeMethod:kMenuClosedMethod arguments:@(_identifier)];
189}
190@end
191
192@interface FlutterMenuPlugin ()
193// Initialize the plugin with the given method channel.
194- (instancetype)initWithChannel:(FlutterMethodChannel*)channel;
195
196// Iterates through the given menu hierarchy, and replaces "APP_NAME"
197// with the localized running application name.
198- (void)replaceAppName:(NSArray<NSMenuItem*>*)items;
199
200// Look up the menu item with the given selector in the list of provided menus
201// and return it.
202- (NSMenuItem*)findProvidedMenuItem:(NSMenu*)menu ofType:(SEL)selector;
203
204// Create a platform-provided menu from the given enum type.
205- (NSMenuItem*)createPlatformProvidedMenu:(flutter::PlatformProvidedMenu)type;
206
207// Create an NSMenuItem from information in the dictionary sent by the framework.
208- (NSMenuItem*)menuItemFromFlutterRepresentation:(NSDictionary*)representation;
209
210// Invokes kMenuSelectedCallbackMethod with the senders ID.
211//
212// Used as the callback for all Flutter-created menu items that have IDs.
213- (void)flutterMenuItemSelected:(id)sender;
214
215// Replaces the NSApp.mainMenu with menus created from an array of top level
216// menus sent by the framework.
217- (void)setMenus:(nonnull NSDictionary*)representation;
218@end
219
220@implementation FlutterMenuPlugin {
221 // The channel used to communicate with Flutter.
223
224 // This contains a copy of the default platform provided items.
225 NSArray<NSMenuItem*>* _platformProvidedItems;
226 // These are the menu delegates that will listen to open/close events for menu
227 // items. This array is holding them so that we can deallocate them when
228 // rebuilding the menus.
229 NSMutableArray<FlutterMenuDelegate*>* _menuDelegates;
230}
231
232#pragma mark - Private Methods
233
234- (instancetype)initWithChannel:(FlutterMethodChannel*)channel {
235 self = [super init];
236 if (self) {
237 _channel = channel;
239 _menuDelegates = [[NSMutableArray alloc] init];
240
241 // Make a copy of all the platform provided menus for later use.
242 _platformProvidedItems = [[NSApp.mainMenu itemArray] mutableCopy];
243
244 // As copied, these platform provided menu items don't yet have the APP_NAME
245 // string replaced in them, so this rectifies that.
246 [self replaceAppName:_platformProvidedItems];
247 }
248 return self;
249}
250
251/**
252 * Iterates through the given menu hierarchy, and replaces "APP_NAME"
253 * with the localized running application name.
254 */
255- (void)replaceAppName:(NSArray<NSMenuItem*>*)items {
256 NSString* appName = [NSRunningApplication currentApplication].localizedName;
257 for (NSMenuItem* item in items) {
258 if ([[item title] containsString:kAppName]) {
259 [item setTitle:[[item title] stringByReplacingOccurrencesOfString:kAppName
260 withString:appName]];
261 }
262 if ([item hasSubmenu]) {
263 [self replaceAppName:[[item submenu] itemArray]];
264 }
265 }
266}
267
268- (NSMenuItem*)findProvidedMenuItem:(NSMenu*)menu ofType:(SEL)selector {
269 const NSArray<NSMenuItem*>* items = menu ? menu.itemArray : _platformProvidedItems;
270 for (NSMenuItem* item in items) {
271 if ([item action] == selector) {
272 return item;
273 }
274 if ([[item submenu] numberOfItems] > 0) {
275 NSMenuItem* foundChild = [self findProvidedMenuItem:[item submenu] ofType:selector];
276 if (foundChild) {
277 return foundChild;
278 }
279 }
280 }
281 return nil;
282}
283
284- (NSMenuItem*)createPlatformProvidedMenu:(flutter::PlatformProvidedMenu)type {
285 const std::map<flutter::PlatformProvidedMenu, SEL> providedMenus = GetMacOSProvidedMenus();
286 auto found_type = providedMenus.find(type);
287 if (found_type == providedMenus.end()) {
288 return nil;
289 }
290 SEL selectorTarget = found_type->second;
291 // Since it doesn't have a definitive selector, the Services submenu is
292 // assumed to be the first item with a submenu action in the first menu item
293 // of the default menu set. We can't just get the title to check, since that
294 // is localized, and the contents of the menu aren't fixed (or even available).
296 ? [_platformProvidedItems[0] submenu]
297 : nil;
298 NSMenuItem* found = [self findProvidedMenuItem:startingMenu ofType:selectorTarget];
299 // Return a copy because the original menu item might not have been removed
300 // from the main menu yet, and AppKit doesn't like menu items that exist in
301 // more than one menu at a time.
302 return [found copy];
303}
304
305- (NSMenuItem*)menuItemFromFlutterRepresentation:(NSDictionary*)representation {
306 if ([(NSNumber*)([representation valueForKey:kDividerKey]) intValue] == YES) {
307 return [NSMenuItem separatorItem];
308 }
309 NSNumber* platformProvidedMenuId = representation[kPlatformProvidedMenuKey];
310 NSString* keyEquivalent = @"";
311
312 if (platformProvidedMenuId) {
313 return [self
314 createPlatformProvidedMenu:(flutter::PlatformProvidedMenu)platformProvidedMenuId.intValue];
315 } else {
316 if (representation[kShortcutCharacterKey]) {
317 keyEquivalent = representation[kShortcutCharacterKey];
318 } else {
319 NSNumber* triggerKeyId = representation[kShortcutTriggerKey];
320 const NSDictionary<NSNumber*, NSNumber*>* specialKeys = GetMacOsSpecialKeys();
321 NSNumber* trigger = specialKeys[triggerKeyId];
322 if (trigger) {
323 keyEquivalent = [NSString stringWithFormat:@"%C", [trigger unsignedShortValue]];
324 } else {
325 if (([triggerKeyId unsignedLongLongValue] & kFlutterKeyIdPlaneMask) ==
327 keyEquivalent = [[NSString
328 stringWithFormat:@"%C", (unichar)([triggerKeyId unsignedLongLongValue] &
329 kFlutterKeyIdValueMask)] lowercaseString];
330 }
331 }
332 }
333 }
334
335 NSNumber* identifier = representation[kIdKey];
336 SEL action = (identifier ? @selector(flutterMenuItemSelected:) : NULL);
337 NSString* appName = [NSRunningApplication currentApplication].localizedName;
338 NSString* title = [representation[kLabelKey] stringByReplacingOccurrencesOfString:kAppName
339 withString:appName];
340 NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
341 action:action
342 keyEquivalent:keyEquivalent];
343 if ([keyEquivalent length] > 0) {
344 item.keyEquivalentModifierMask =
346 }
347 if (identifier) {
348 item.tag = identifier.longLongValue;
349 item.target = self;
350 }
351 NSNumber* enabled = representation[kEnabledKey];
352 if (enabled) {
353 item.enabled = enabled.boolValue;
354 }
355
356 NSArray* children = representation[kChildrenKey];
357 if (children && children.count > 0) {
358 NSMenu* submenu = [[NSMenu alloc] initWithTitle:title];
359 FlutterMenuDelegate* delegate = [[FlutterMenuDelegate alloc] initWithIdentifier:item.tag
360 channel:_channel];
361 [_menuDelegates addObject:delegate];
362 submenu.delegate = delegate;
363 submenu.autoenablesItems = NO;
364 for (NSDictionary* child in children) {
365 NSMenuItem* newItem = [self menuItemFromFlutterRepresentation:child];
366 if (newItem) {
367 [submenu addItem:newItem];
368 }
369 }
370 item.submenu = submenu;
371 }
372 return item;
373}
374
375- (void)flutterMenuItemSelected:(id)sender {
376 NSMenuItem* item = sender;
377 [_channel invokeMethod:kMenuSelectedCallbackMethod arguments:@(item.tag)];
378}
379
380- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
381 if ([call.method isEqualToString:kIsPluginAvailableMethod]) {
382 result(@YES);
383 } else if ([call.method isEqualToString:kMenuSetMenusMethod]) {
384 NSDictionary* menus = call.arguments;
385 [self setMenus:menus];
386 result(nil);
387 } else {
389 }
390}
391
392- (void)setMenus:(NSDictionary*)representation {
393 [_menuDelegates removeAllObjects];
394 NSMenu* newMenu = [[NSMenu alloc] init];
395 // There's currently only one window, named "0", but there could be other
396 // eventually, with different menu configurations.
397 for (NSDictionary* item in representation[@"0"]) {
398 NSMenuItem* menuItem = [self menuItemFromFlutterRepresentation:item];
399 menuItem.representedObject = self;
400 NSNumber* identifier = item[kIdKey];
401 FlutterMenuDelegate* delegate =
402 [[FlutterMenuDelegate alloc] initWithIdentifier:identifier.longLongValue channel:_channel];
403 [_menuDelegates addObject:delegate];
404 [menuItem submenu].delegate = delegate;
405 [newMenu addItem:menuItem];
406 }
407 NSApp.mainMenu = newMenu;
408}
409
410#pragma mark - Public Class Methods
411
412+ (void)registerWithRegistrar:(nonnull id<FlutterPluginRegistrar>)registrar {
414 binaryMessenger:registrar.messenger];
415 FlutterMenuPlugin* instance = [[FlutterMenuPlugin alloc] initWithChannel:channel];
416 [registrar addMethodCallDelegate:instance channel:channel];
417}
418
419@end
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
constexpr int kFlutterShortcutModifierControl
constexpr uint64_t kFlutterKeyIdValueMask
constexpr int kFlutterShortcutModifierAlt
static NSString *const kAppName
static NSString *const kIsPluginAvailableMethod
static NSString *const kMenuClosedMethod
static const std::map< flutter::PlatformProvidedMenu, SEL > GetMacOSProvidedMenus()
NSArray< NSMenuItem * > * _platformProvidedItems
constexpr int kFlutterShortcutModifierMeta
static NSString *const kPlatformProvidedMenuKey
constexpr uint64_t kFlutterKeyIdPlaneMask
NSMutableArray< FlutterMenuDelegate * > * _menuDelegates
static NSString *const kShortcutTriggerKey
static NSEventModifierFlags KeyEquivalentModifierMaskForModifiers(NSNumber *modifiers)
static NSString *const kChannelName
static NSString *const kShortcutCharacterKey
static NSString *const kMenuOpenedMethod
static NSString *const kDividerKey
static NSString *const kEnabledKey
constexpr int kFlutterShortcutModifierShift
static NSString *const kMenuSetMenusMethod
constexpr uint64_t kFlutterKeyIdUnicodePlane
static const NSDictionary * logicalKeyToKeyCode
static NSString *const kShortcutModifiersKey
static NSString *const kMenuSelectedCallbackMethod
static NSString *const kChildrenKey
static NSString *const kIdKey
int64_t _identifier
static NSString *const kLabelKey
static NSDictionary< NSNumber *, NSNumber * > * GetMacOsSpecialKeys()
FlutterMethodChannel * _channel
GLenum type
static SkString identifier(const FontFamilyDesc &family, const FontDesc &font)
VkInstance instance
Definition: main.cc:48
FlutterSemanticsFlag flags
GAsyncResult * result
static FlMethodResponse * hide(FlTextInputPlugin *self)
instancetype methodChannelWithName:binaryMessenger:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger)
size_t length
def call(args)
Definition: dom.py:159
void addMethodCallDelegate:channel:(NSObject< FlutterPlugin > *delegate,[channel] FlutterMethodChannel *channel)