| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2012 The Chromium 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 |
| 5 #import "chrome/browser/ui/cocoa/location_bar/action_box_menu_bubble_controller.
h" |
| 6 |
| 7 #include "base/mac/bundle_locations.h" |
| 8 #include "base/mac/foundation_util.h" |
| 9 #include "base/mac/mac_util.h" |
| 10 #include "base/sys_string_conversions.h" |
| 11 #import "chrome/browser/ui/cocoa/browser_window_utils.h" |
| 12 #import "chrome/browser/ui/cocoa/event_utils.h" |
| 13 #import "chrome/browser/ui/cocoa/info_bubble_view.h" |
| 14 #import "chrome/browser/ui/cocoa/info_bubble_window.h" |
| 15 #include "grit/generated_resources.h" |
| 16 #include "grit/theme_resources.h" |
| 17 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| 18 #include "ui/base/models/simple_menu_model.h" |
| 19 #include "ui/base/resource/resource_bundle.h" |
| 20 #include "ui/gfx/image/image.h" |
| 21 |
| 22 @interface ActionBoxMenuBubbleController (Private) |
| 23 - (id)highlightedItem; |
| 24 - (void)keyDown:(NSEvent*)theEvent; |
| 25 - (void)moveDown:(id)sender; |
| 26 - (void)moveUp:(id)sender; |
| 27 - (void)highlightNextItemByDelta:(NSInteger)delta; |
| 28 - (void)highlightItem:(ActionBoxMenuItemController*)newItem; |
| 29 @end |
| 30 |
| 31 namespace { |
| 32 |
| 33 // Some reasonable values for the menu geometry. |
| 34 const CGFloat kBubbleMinWidth = 175; |
| 35 const CGFloat kBubbleMaxWidth = 800; |
| 36 |
| 37 // Distance between the top/bottom of the bubble and the first/last menu item. |
| 38 const CGFloat kVerticalPadding = 7.0; |
| 39 |
| 40 // Minimum distance between the right of a menu item and the right border. |
| 41 const CGFloat kRightMargin = 20.0; |
| 42 |
| 43 // Alpha of the black rectangle overlayed on the item hovered over. |
| 44 const CGFloat kSelectionAlpha = 0.06; |
| 45 |
| 46 } // namespace |
| 47 |
| 48 @implementation ActionBoxMenuBubbleController |
| 49 |
| 50 - (id)initWithModel:(scoped_ptr<ui::MenuModel>)model |
| 51 parentWindow:(NSWindow*)parent |
| 52 anchoredAt:(NSPoint)point { |
| 53 // Use an arbitrary height because it will reflect the size of the content. |
| 54 NSRect contentRect = NSMakeRect(0, 0, kBubbleMinWidth, 150); |
| 55 // Create an empty window into which content is placed. |
| 56 scoped_nsobject<InfoBubbleWindow> window( |
| 57 [[InfoBubbleWindow alloc] initWithContentRect:contentRect |
| 58 styleMask:NSBorderlessWindowMask |
| 59 backing:NSBackingStoreBuffered |
| 60 defer:NO]); |
| 61 if (self = [super initWithWindow:window |
| 62 parentWindow:parent |
| 63 anchoredAt:point]) { |
| 64 model_.reset(model.release()); |
| 65 |
| 66 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge]; |
| 67 [[self bubble] setArrowLocation:info_bubble::kNoArrow]; |
| 68 [[self bubble] setBackgroundColor: |
| 69 [NSColor colorWithDeviceWhite:(251.0f/255.0f) |
| 70 alpha:1.0]]; |
| 71 [self performLayout]; |
| 72 } |
| 73 return self; |
| 74 } |
| 75 |
| 76 - (ui::MenuModel*)model { |
| 77 return model_.get(); |
| 78 } |
| 79 |
| 80 - (IBAction)itemSelected:(id)sender { |
| 81 // If Enter is pressed but nothing is highlighted, don't activate anything. |
| 82 if (!sender) |
| 83 return; |
| 84 |
| 85 // Close the current window and activate the parent browser window, otherwise |
| 86 // the bookmark popup refuses to show. |
| 87 [self close]; |
| 88 [BrowserWindowUtils |
| 89 activateWindowForController:[[self parentWindow] windowController]]; |
| 90 size_t modelIndex = [sender modelIndex]; |
| 91 DCHECK(model_.get()); |
| 92 int eventFlags = event_utils::EventFlagsFromNSEvent([NSApp currentEvent]); |
| 93 model_->ActivatedAt(modelIndex, eventFlags); |
| 94 } |
| 95 |
| 96 // Private ///////////////////////////////////////////////////////////////////// |
| 97 |
| 98 - (void)performLayout { |
| 99 NSView* contentView = [[self window] contentView]; |
| 100 |
| 101 // Reset the array of controllers and remove all the views. |
| 102 items_.reset([[NSMutableArray alloc] init]); |
| 103 [contentView setSubviews:[NSArray array]]; |
| 104 |
| 105 // Leave some space at the bottom of the menu. |
| 106 CGFloat yOffset = kVerticalPadding; |
| 107 |
| 108 // Loop over the items in reverse, constructing the menu items. |
| 109 CGFloat width = kBubbleMinWidth; |
| 110 CGFloat minX = NSMinX([contentView bounds]); |
| 111 for (int i = model_->GetItemCount() - 1; i >= 0; --i) { |
| 112 // Create the item controller. Autorelease it because it will be owned |
| 113 // by the |items_| array. |
| 114 scoped_nsobject<ActionBoxMenuItemController> itemController( |
| 115 [[ActionBoxMenuItemController alloc] initWithModelIndex:i |
| 116 menuController:self]); |
| 117 |
| 118 // Adjust the name field to fit the string. |
| 119 [GTMUILocalizerAndLayoutTweaker sizeToFitView:[itemController nameField]]; |
| 120 |
| 121 // Expand the size of the window if required to fit the menu item. |
| 122 width = std::max(width, |
| 123 NSMaxX([[itemController nameField] frame]) - minX + kRightMargin); |
| 124 |
| 125 // Add the item to the content view. |
| 126 [[itemController view] setFrameOrigin:NSMakePoint(0, yOffset)]; |
| 127 [contentView addSubview:[itemController view]]; |
| 128 yOffset += NSHeight([[itemController view] frame]); |
| 129 |
| 130 // Keep track of the view controller. |
| 131 [items_ addObject:itemController.get()]; |
| 132 } |
| 133 |
| 134 // Leave some space at the top of the menu. |
| 135 yOffset += kVerticalPadding; |
| 136 |
| 137 // Set the window frame, clamping the width at a sensible max. |
| 138 NSRect frame = [[self window] frame]; |
| 139 frame.size.height = yOffset; |
| 140 frame.size.width = std::min(width, kBubbleMaxWidth); |
| 141 [[self window] setFrame:frame display:YES]; |
| 142 } |
| 143 |
| 144 - (id)highlightedItem { |
| 145 for (ActionBoxMenuItemController* item in items_.get()) { |
| 146 if ([item isHighlighted]) { |
| 147 return item; |
| 148 } |
| 149 } |
| 150 return nil; |
| 151 } |
| 152 |
| 153 - (void)keyDown:(NSEvent*)theEvent { |
| 154 // Interpret all possible key events. In particular, this will answer |
| 155 // moveDown, moveUp and insertNewline so that the menu can be navigated |
| 156 // with keystrokes. |
| 157 [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; |
| 158 } |
| 159 |
| 160 - (void)moveDown:(id)sender { |
| 161 [self highlightNextItemByDelta:-1]; |
| 162 } |
| 163 |
| 164 - (void)moveUp:(id)sender { |
| 165 [self highlightNextItemByDelta:1]; |
| 166 } |
| 167 |
| 168 - (void)insertNewline:(id)sender { |
| 169 [self itemSelected:[self highlightedItem]]; |
| 170 } |
| 171 |
| 172 - (void)highlightNextItemByDelta:(NSInteger)delta { |
| 173 NSUInteger count = [items_ count]; |
| 174 if (count == 0) |
| 175 return; |
| 176 |
| 177 // If nothing is selected, select the first (resp. last) item when going up |
| 178 // (resp. going down). Otherwise selects the next (resp. previous) item. |
| 179 // This code does not wrap around if something is already selected. |
| 180 |
| 181 // First assumes nothing is selected. |
| 182 NSUInteger newIndex = delta < 0 ? (count - 1) : 0; |
| 183 NSUInteger oldIndex = [items_ indexOfObject:[self highlightedItem]]; |
| 184 // oldIndex will be NSNotFound when [self highlightedItem] returns nil. |
| 185 if (oldIndex != NSNotFound) { |
| 186 // Something is selected, move only if not already at top/bottom. |
| 187 newIndex = oldIndex; |
| 188 if (newIndex != delta < 0 ? 0 : (count - 1)) |
| 189 newIndex += delta; |
| 190 } |
| 191 |
| 192 [self highlightItem:[items_ objectAtIndex:newIndex]]; |
| 193 } |
| 194 |
| 195 - (void)highlightItem:(ActionBoxMenuItemController*)newItem { |
| 196 ActionBoxMenuItemController* oldItem = [self highlightedItem]; |
| 197 if (oldItem == newItem) |
| 198 return; |
| 199 [oldItem setIsHighlighted:NO]; |
| 200 [newItem setIsHighlighted:YES]; |
| 201 } |
| 202 |
| 203 @end |
| 204 |
| 205 // Menu Item Controller //////////////////////////////////////////////////////// |
| 206 |
| 207 @implementation ActionBoxMenuItemController |
| 208 |
| 209 @synthesize modelIndex = modelIndex_; |
| 210 @synthesize isHighlighted = isHighlighted_; |
| 211 @synthesize nameField = nameField_; |
| 212 |
| 213 - (id)initWithModelIndex:(size_t)modelIndex |
| 214 menuController:(ActionBoxMenuBubbleController*)controller { |
| 215 if ((self = [super initWithNibName:@"ActionBoxMenuItem" |
| 216 bundle:base::mac::FrameworkBundle()])) { |
| 217 modelIndex_ = modelIndex; |
| 218 controller_ = controller; |
| 219 |
| 220 [self loadView]; |
| 221 |
| 222 gfx::Image icon = gfx::Image(); |
| 223 |
| 224 if (controller.model->GetIconAt(modelIndex_, &icon)) |
| 225 iconView_.image = icon.ToNSImage(); |
| 226 else |
| 227 iconView_.image = nil; |
| 228 nameField_.stringValue = base::SysUTF16ToNSString( |
| 229 controller.model->GetLabelAt(modelIndex_)); |
| 230 } |
| 231 return self; |
| 232 } |
| 233 |
| 234 - (void)dealloc { |
| 235 base::mac::ObjCCastStrict<ActionBoxMenuItemView>( |
| 236 self.view).viewController = nil; |
| 237 [super dealloc]; |
| 238 } |
| 239 |
| 240 - (void)highlightForEvent:(NSEvent*)event { |
| 241 switch ([event type]) { |
| 242 case NSMouseEntered: |
| 243 [controller_ highlightItem:self]; |
| 244 break; |
| 245 |
| 246 case NSMouseExited: |
| 247 [controller_ highlightItem:nil]; |
| 248 break; |
| 249 |
| 250 default: |
| 251 NOTREACHED(); |
| 252 }; |
| 253 } |
| 254 |
| 255 - (IBAction)itemSelected:(id)sender { |
| 256 [controller_ itemSelected:self]; |
| 257 } |
| 258 |
| 259 - (void)setIsHighlighted:(BOOL)isHighlighted { |
| 260 if (isHighlighted_ == isHighlighted) |
| 261 return; |
| 262 |
| 263 isHighlighted_ = isHighlighted; |
| 264 [[self view] setNeedsDisplay:YES]; |
| 265 } |
| 266 |
| 267 @end |
| 268 |
| 269 // Items from the action box menu ////////////////////////////////////////////// |
| 270 |
| 271 @implementation ActionBoxMenuItemView |
| 272 |
| 273 @synthesize viewController = viewController_; |
| 274 |
| 275 - (void)updateTrackingAreas { |
| 276 if (trackingArea_.get()) |
| 277 [self removeTrackingArea:trackingArea_.get()]; |
| 278 |
| 279 trackingArea_.reset( |
| 280 [[CrTrackingArea alloc] initWithRect:[self bounds] |
| 281 options:NSTrackingMouseEnteredAndExited | |
| 282 NSTrackingActiveInKeyWindow |
| 283 owner:self |
| 284 userInfo:nil]); |
| 285 [self addTrackingArea:trackingArea_.get()]; |
| 286 |
| 287 [super updateTrackingAreas]; |
| 288 } |
| 289 |
| 290 - (BOOL)acceptsFirstResponder { |
| 291 return YES; |
| 292 } |
| 293 |
| 294 - (void)mouseUp:(NSEvent*)theEvent { |
| 295 [viewController_ itemSelected:self]; |
| 296 } |
| 297 |
| 298 - (void)mouseEntered:(NSEvent*)theEvent { |
| 299 [viewController_ highlightForEvent:theEvent]; |
| 300 } |
| 301 |
| 302 - (void)mouseExited:(NSEvent*)theEvent { |
| 303 [viewController_ highlightForEvent:theEvent]; |
| 304 } |
| 305 |
| 306 - (void)drawRect:(NSRect)dirtyRect { |
| 307 NSColor* backgroundColor = nil; |
| 308 if ([viewController_ isHighlighted]) { |
| 309 backgroundColor = [NSColor colorWithDeviceWhite:0.0 alpha:kSelectionAlpha]; |
| 310 } else { |
| 311 backgroundColor = [NSColor clearColor]; |
| 312 } |
| 313 |
| 314 [backgroundColor set]; |
| 315 [NSBezierPath fillRect:[self bounds]]; |
| 316 } |
| 317 |
| 318 // Make sure the element is focusable for accessibility. |
| 319 - (BOOL)canBecomeKeyView { |
| 320 return YES; |
| 321 } |
| 322 |
| 323 - (BOOL)accessibilityIsIgnored { |
| 324 return NO; |
| 325 } |
| 326 |
| 327 - (NSArray*)accessibilityAttributeNames { |
| 328 NSMutableArray* attributes = |
| 329 [[[super accessibilityAttributeNames] mutableCopy] autorelease]; |
| 330 [attributes addObject:NSAccessibilityTitleAttribute]; |
| 331 [attributes addObject:NSAccessibilityEnabledAttribute]; |
| 332 |
| 333 return attributes; |
| 334 } |
| 335 |
| 336 - (NSArray*)accessibilityActionNames { |
| 337 NSArray* parentActions = [super accessibilityActionNames]; |
| 338 return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; |
| 339 } |
| 340 |
| 341 - (id)accessibilityAttributeValue:(NSString*)attribute { |
| 342 if ([attribute isEqual:NSAccessibilityRoleAttribute]) |
| 343 return NSAccessibilityButtonRole; |
| 344 |
| 345 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute]) |
| 346 return NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil); |
| 347 |
| 348 if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| 349 return [NSNumber numberWithBool:YES]; |
| 350 |
| 351 return [super accessibilityAttributeValue:attribute]; |
| 352 } |
| 353 |
| 354 - (void)accessibilityPerformAction:(NSString*)action { |
| 355 if ([action isEqual:NSAccessibilityPressAction]) { |
| 356 [viewController_ itemSelected:self]; |
| 357 return; |
| 358 } |
| 359 |
| 360 [super accessibilityPerformAction:action]; |
| 361 } |
| 362 |
| 363 @end |
| OLD | NEW |