Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(335)

Side by Side Diff: ui/base/cocoa/menu_controller.mm

Issue 2852233002: Mac[Views]: Make native menus more responsive by pumping private runloop modes. (Closed)
Patch Set: Merge private categories Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #import "ui/base/cocoa/menu_controller.h" 5 #import "ui/base/cocoa/menu_controller.h"
6 6
7 #include "base/cancelable_callback.h"
7 #include "base/logging.h" 8 #include "base/logging.h"
9 #include "base/mac/bind_objc_block.h"
10 #include "base/memory/ptr_util.h"
8 #include "base/strings/sys_string_conversions.h" 11 #include "base/strings/sys_string_conversions.h"
12 #include "base/threading/thread_task_runner_handle.h"
9 #include "ui/base/accelerators/accelerator.h" 13 #include "ui/base/accelerators/accelerator.h"
10 #include "ui/base/accelerators/platform_accelerator_cocoa.h" 14 #include "ui/base/accelerators/platform_accelerator_cocoa.h"
11 #include "ui/base/l10n/l10n_util_mac.h" 15 #include "ui/base/l10n/l10n_util_mac.h"
12 #include "ui/base/models/simple_menu_model.h" 16 #include "ui/base/models/simple_menu_model.h"
13 #import "ui/events/event_utils.h" 17 #import "ui/events/event_utils.h"
14 #include "ui/gfx/font_list.h" 18 #include "ui/gfx/font_list.h"
15 #include "ui/gfx/image/image.h" 19 #include "ui/gfx/image/image.h"
16 #include "ui/gfx/text_elider.h" 20 #include "ui/gfx/text_elider.h"
17 21
18 NSString* const kMenuControllerMenuWillOpenNotification = 22 NSString* const kMenuControllerMenuWillOpenNotification =
19 @"MenuControllerMenuWillOpen"; 23 @"MenuControllerMenuWillOpen";
20 NSString* const kMenuControllerMenuDidCloseNotification = 24 NSString* const kMenuControllerMenuDidCloseNotification =
21 @"MenuControllerMenuDidClose"; 25 @"MenuControllerMenuDidClose";
22 26
23 @interface MenuController (Private) 27 // Internal methods.
24 - (void)addSeparatorToMenu:(NSMenu*)menu 28 @interface MenuController ()
25 atIndex:(int)index; 29 // Adds a separator item at the given index. As the separator doesn't need
30 // anything from the model, this method doesn't need the model index as the
31 // other method below does.
32 - (void)addSeparatorToMenu:(NSMenu*)menu atIndex:(int)index;
33
34 // Called via a private API hook shortly after the event that selects a menu
35 // item arrives.
36 - (void)itemWillBeSelected:(NSMenuItem*)sender;
37
38 // Called when the user chooses a particular menu item. AppKit sends this only
39 // after the menu has fully faded out. |sender| is the menu item chosen.
40 - (void)itemSelected:(id)sender;
41
42 // Called by the posted task to selected an item during menu fade out.
43 // |uiEventFlags| are the ui::EventFlags captured from the triggering NSEvent.
44 - (void)itemSelected:(id)sender uiEventFlags:(int)uiEventFlags;
26 @end 45 @end
27 46
28 @implementation MenuController 47 @interface ResponsiveNSMenuItem : NSMenuItem
48 @end
49
50 @implementation MenuController {
51 BOOL useWithPopUpButtonCell_; // If YES, 0th item is blank
52 BOOL isMenuOpen_;
53 BOOL postItemSelectedAsTask_;
54 std::unique_ptr<base::CancelableClosure> postedItemSelectedTask_;
55 }
29 56
30 @synthesize model = model_; 57 @synthesize model = model_;
31 @synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_; 58 @synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_;
59 @synthesize postItemSelectedAsTask = postItemSelectedAsTask_;
32 60
33 + (base::string16)elideMenuTitle:(const base::string16&)title 61 + (base::string16)elideMenuTitle:(const base::string16&)title
34 toWidth:(int)width { 62 toWidth:(int)width {
35 NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default" 63 NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default"
36 return gfx::ElideText(title, gfx::FontList(gfx::Font(nsfont)), width, 64 return gfx::ElideText(title, gfx::FontList(gfx::Font(nsfont)), width,
37 gfx::ELIDE_TAIL); 65 gfx::ELIDE_TAIL);
38 } 66 }
39 67
40 - (id)init { 68 - (id)init {
41 self = [super init]; 69 self = [super init];
(...skipping 22 matching lines...) Expand all
64 } 92 }
65 93
66 - (void)cancel { 94 - (void)cancel {
67 if (isMenuOpen_) { 95 if (isMenuOpen_) {
68 [menu_ cancelTracking]; 96 [menu_ cancelTracking];
69 model_->MenuWillClose(); 97 model_->MenuWillClose();
70 isMenuOpen_ = NO; 98 isMenuOpen_ = NO;
71 } 99 }
72 } 100 }
73 101
74 // Creates a NSMenu from the given model. If the model has submenus, this can
75 // be invoked recursively.
76 - (NSMenu*)menuFromModel:(ui::MenuModel*)model { 102 - (NSMenu*)menuFromModel:(ui::MenuModel*)model {
77 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; 103 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
78 104
79 const int count = model->GetItemCount(); 105 const int count = model->GetItemCount();
80 for (int index = 0; index < count; index++) { 106 for (int index = 0; index < count; index++) {
81 if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SEPARATOR) 107 if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SEPARATOR)
82 [self addSeparatorToMenu:menu atIndex:index]; 108 [self addSeparatorToMenu:menu atIndex:index];
83 else 109 else
84 [self addItemToMenu:menu atIndex:index fromModel:model]; 110 [self addItemToMenu:menu atIndex:index fromModel:model];
85 } 111 }
86 112
87 return menu; 113 return menu;
88 } 114 }
89 115
90 - (int)maxWidthForMenuModel:(ui::MenuModel*)model 116 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
91 modelIndex:(int)modelIndex { 117 modelIndex:(int)modelIndex {
92 return -1; 118 return -1;
93 } 119 }
94 120
95 // Adds a separator item at the given index. As the separator doesn't need
96 // anything from the model, this method doesn't need the model index as the
97 // other method below does.
98 - (void)addSeparatorToMenu:(NSMenu*)menu 121 - (void)addSeparatorToMenu:(NSMenu*)menu
99 atIndex:(int)index { 122 atIndex:(int)index {
100 NSMenuItem* separator = [NSMenuItem separatorItem]; 123 NSMenuItem* separator = [NSMenuItem separatorItem];
101 [menu insertItem:separator atIndex:index]; 124 [menu insertItem:separator atIndex:index];
102 } 125 }
103 126
104 // Adds an item or a hierarchical menu to the item at the |index|,
105 // associated with the entry in the model identified by |modelIndex|.
106 - (void)addItemToMenu:(NSMenu*)menu 127 - (void)addItemToMenu:(NSMenu*)menu
107 atIndex:(NSInteger)index 128 atIndex:(NSInteger)index
108 fromModel:(ui::MenuModel*)model { 129 fromModel:(ui::MenuModel*)model {
109 base::string16 label16 = model->GetLabelAt(index); 130 base::string16 label16 = model->GetLabelAt(index);
110 int maxWidth = [self maxWidthForMenuModel:model modelIndex:index]; 131 int maxWidth = [self maxWidthForMenuModel:model modelIndex:index];
111 if (maxWidth != -1) 132 if (maxWidth != -1)
112 label16 = [MenuController elideMenuTitle:label16 toWidth:maxWidth]; 133 label16 = [MenuController elideMenuTitle:label16 toWidth:maxWidth];
113 134
114 NSString* label = l10n_util::FixUpWindowsStyleLabel(label16); 135 NSString* label = l10n_util::FixUpWindowsStyleLabel(label16);
115 base::scoped_nsobject<NSMenuItem> item( 136 base::scoped_nsobject<NSMenuItem> item([[ResponsiveNSMenuItem alloc]
116 [[NSMenuItem alloc] initWithTitle:label 137 initWithTitle:label
117 action:@selector(itemSelected:) 138 action:@selector(itemSelected:)
118 keyEquivalent:@""]); 139 keyEquivalent:@""]);
119 140
120 // If the menu item has an icon, set it. 141 // If the menu item has an icon, set it.
121 gfx::Image icon; 142 gfx::Image icon;
122 if (model->GetIconAt(index, &icon) && !icon.IsEmpty()) 143 if (model->GetIconAt(index, &icon) && !icon.IsEmpty())
123 [item setImage:icon.ToNSImage()]; 144 [item setImage:icon.ToNSImage()];
124 145
125 ui::MenuModel::ItemType type = model->GetTypeAt(index); 146 ui::MenuModel::ItemType type = model->GetTypeAt(index);
126 if (type == ui::MenuModel::TYPE_SUBMENU) { 147 if (type == ui::MenuModel::TYPE_SUBMENU) {
127 // Recursively build a submenu from the sub-model at this index. 148 // Recursively build a submenu from the sub-model at this index.
128 [item setTarget:nil]; 149 [item setTarget:nil];
(...skipping 20 matching lines...) Expand all
149 if (platformAccelerator) { 170 if (platformAccelerator) {
150 [item setKeyEquivalent:platformAccelerator->characters()]; 171 [item setKeyEquivalent:platformAccelerator->characters()];
151 [item setKeyEquivalentModifierMask: 172 [item setKeyEquivalentModifierMask:
152 platformAccelerator->modifier_mask()]; 173 platformAccelerator->modifier_mask()];
153 } 174 }
154 } 175 }
155 } 176 }
156 [menu insertItem:item atIndex:index]; 177 [menu insertItem:item atIndex:index];
157 } 178 }
158 179
159 // Called before the menu is to be displayed to update the state (enabled,
160 // radio, etc) of each item in the menu. Also will update the title if
161 // the item is marked as "dynamic".
162 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { 180 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
163 SEL action = [item action]; 181 SEL action = [item action];
164 if (action != @selector(itemSelected:)) 182 if (action != @selector(itemSelected:))
165 return NO; 183 return NO;
166 184
167 NSInteger modelIndex = [item tag]; 185 NSInteger modelIndex = [item tag];
168 ui::MenuModel* model = 186 ui::MenuModel* model =
169 static_cast<ui::MenuModel*>( 187 static_cast<ui::MenuModel*>(
170 [[(id)item representedObject] pointerValue]); 188 [[(id)item representedObject] pointerValue]);
171 DCHECK(model); 189 DCHECK(model);
(...skipping 21 matching lines...) Expand all
193 base::scoped_nsobject<NSAttributedString> title( 211 base::scoped_nsobject<NSAttributedString> title(
194 [[NSAttributedString alloc] initWithString:[(id)item title] 212 [[NSAttributedString alloc] initWithString:[(id)item title]
195 attributes:attributes]); 213 attributes:attributes]);
196 [(id)item setAttributedTitle:title.get()]; 214 [(id)item setAttributedTitle:title.get()];
197 } 215 }
198 return model->IsEnabledAt(modelIndex); 216 return model->IsEnabledAt(modelIndex);
199 } 217 }
200 return NO; 218 return NO;
201 } 219 }
202 220
203 // Called when the user chooses a particular menu item. |sender| is the menu 221 - (void)itemWillBeSelected:(NSMenuItem*)sender {
204 // item chosen. 222 if (postItemSelectedAsTask_ && [sender action] == @selector(itemSelected:) &&
223 [[sender target]
224 respondsToSelector:@selector(itemSelected:uiEventFlags:)]) {
225 const int uiEventFlags = ui::EventFlagsFromNative([NSApp currentEvent]);
226 postedItemSelectedTask_ =
227 base::MakeUnique<base::CancelableClosure>(base::BindBlock(^() {
Robert Sesek 2017/05/10 13:37:27 nit: can omit the () and just do |^{|.
tapted 2017/05/11 00:24:06 Done.
228 id target = [sender target];
229 if ([target respondsToSelector:@selector(itemSelected:uiEventFlags:)])
230 [target itemSelected:sender uiEventFlags:uiEventFlags];
231 else
232 NOTREACHED();
233 }));
234 base::ThreadTaskRunnerHandle::Get()->PostTask(
235 FROM_HERE, postedItemSelectedTask_->callback());
236 }
237 }
238
205 - (void)itemSelected:(id)sender { 239 - (void)itemSelected:(id)sender {
240 if (postedItemSelectedTask_) {
241 if (!postedItemSelectedTask_->IsCancelled())
242 postedItemSelectedTask_->callback().Run();
243 postedItemSelectedTask_.reset();
244 } else {
245 [self itemSelected:sender
246 uiEventFlags:ui::EventFlagsFromNative([NSApp currentEvent])];
247 }
248 }
249
250 - (void)itemSelected:(id)sender uiEventFlags:(int)uiEventFlags {
206 NSInteger modelIndex = [sender tag]; 251 NSInteger modelIndex = [sender tag];
207 ui::MenuModel* model = 252 ui::MenuModel* model =
208 static_cast<ui::MenuModel*>( 253 static_cast<ui::MenuModel*>(
209 [[sender representedObject] pointerValue]); 254 [[sender representedObject] pointerValue]);
210 DCHECK(model); 255 DCHECK(model);
211 if (model) { 256 if (model)
212 int event_flags = ui::EventFlagsFromNative([NSApp currentEvent]); 257 model->ActivatedAt(modelIndex, uiEventFlags);
213 model->ActivatedAt(modelIndex, event_flags); 258
214 } 259 // Cancel any posted task, but don't reset it, so that the correct path is
260 // taken in -itemSelected:.
261 if (postedItemSelectedTask_)
262 postedItemSelectedTask_->Cancel();
215 } 263 }
216 264
217 - (NSMenu*)menu { 265 - (NSMenu*)menu {
218 if (!menu_ && model_) { 266 if (!menu_ && model_) {
219 menu_.reset([[self menuFromModel:model_] retain]); 267 menu_.reset([[self menuFromModel:model_] retain]);
220 [menu_ setDelegate:self]; 268 [menu_ setDelegate:self];
221 // If this is to be used with a NSPopUpButtonCell, add an item at the 0th 269 // If this is to be used with a NSPopUpButtonCell, add an item at the 0th
222 // position that's empty. Doing it after the menu has been constructed won't 270 // position that's empty. Doing it after the menu has been constructed won't
223 // complicate creation logic, and since the tags are model indexes, they 271 // complicate creation logic, and since the tags are model indexes, they
224 // are unaffected by the extra item. 272 // are unaffected by the extra item.
(...skipping 22 matching lines...) Expand all
247 if (isMenuOpen_) { 295 if (isMenuOpen_) {
248 model_->MenuWillClose(); 296 model_->MenuWillClose();
249 isMenuOpen_ = NO; 297 isMenuOpen_ = NO;
250 } 298 }
251 [[NSNotificationCenter defaultCenter] 299 [[NSNotificationCenter defaultCenter]
252 postNotificationName:kMenuControllerMenuDidCloseNotification 300 postNotificationName:kMenuControllerMenuDidCloseNotification
253 object:self]; 301 object:self];
254 } 302 }
255 303
256 @end 304 @end
305
306 @interface NSMenuItem (Private)
307 // Private method which is invoked very soon after the event that activates a
308 // menu item is received. AppKit then spends 300ms or so flashing the menu item,
309 // and fading out the menu, in private run loop modes.
310 - (void)_sendItemSelectedNote;
311 @end
312
313 @implementation ResponsiveNSMenuItem
314 - (void)_sendItemSelectedNote {
315 if ([[self target] respondsToSelector:@selector(itemWillBeSelected:)])
316 [[self target] itemWillBeSelected:self];
317 [super _sendItemSelectedNote];
318 }
319 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698