apps_search_box_controller.mm revision 3551c9c881056c480085172ff9840cab31610854
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 3// found in the LICENSE file. 4 5#import "ui/app_list/cocoa/apps_search_box_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "base/mac/mac_util.h" 9#include "base/strings/sys_string_conversions.h" 10#include "grit/ui_resources.h" 11#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" 12#include "ui/app_list/app_list_menu.h" 13#import "ui/app_list/cocoa/current_user_menu_item_view.h" 14#include "ui/app_list/search_box_model.h" 15#include "ui/app_list/search_box_model_observer.h" 16#import "ui/base/cocoa/controls/hover_image_menu_button.h" 17#import "ui/base/cocoa/controls/hover_image_menu_button_cell.h" 18#import "ui/base/cocoa/menu_controller.h" 19#include "ui/base/resource/resource_bundle.h" 20#include "ui/gfx/image/image_skia_util_mac.h" 21 22namespace { 23 24// Padding either side of the search icon and menu button. 25const CGFloat kPadding = 14; 26 27// Size of the search icon. 28const CGFloat kSearchIconDimension = 32; 29 30// Size of the menu button on the right. 31const CGFloat kMenuButtonDimension = 29; 32 33// Menu offset relative to the bottom-right corner of the menu button. 34const CGFloat kMenuYOffsetFromButton = -4; 35const CGFloat kMenuXOffsetFromButton = -7; 36 37} 38 39@interface AppsSearchBoxController () 40 41- (NSImageView*)searchImageView; 42- (void)addSubviews; 43 44@end 45 46namespace app_list { 47 48class SearchBoxModelObserverBridge : public SearchBoxModelObserver { 49 public: 50 SearchBoxModelObserverBridge(AppsSearchBoxController* parent); 51 virtual ~SearchBoxModelObserverBridge(); 52 53 void SetSearchText(const base::string16& text); 54 55 virtual void IconChanged() OVERRIDE; 56 virtual void HintTextChanged() OVERRIDE; 57 virtual void SelectionModelChanged() OVERRIDE; 58 virtual void TextChanged() OVERRIDE; 59 60 private: 61 SearchBoxModel* GetModel(); 62 63 AppsSearchBoxController* parent_; // Weak. Owns us. 64 65 DISALLOW_COPY_AND_ASSIGN(SearchBoxModelObserverBridge); 66}; 67 68SearchBoxModelObserverBridge::SearchBoxModelObserverBridge( 69 AppsSearchBoxController* parent) 70 : parent_(parent) { 71 IconChanged(); 72 HintTextChanged(); 73 GetModel()->AddObserver(this); 74} 75 76SearchBoxModelObserverBridge::~SearchBoxModelObserverBridge() { 77 GetModel()->RemoveObserver(this); 78} 79 80SearchBoxModel* SearchBoxModelObserverBridge::GetModel() { 81 SearchBoxModel* searchBoxModel = [[parent_ delegate] searchBoxModel]; 82 DCHECK(searchBoxModel); 83 return searchBoxModel; 84} 85 86void SearchBoxModelObserverBridge::SetSearchText(const base::string16& text) { 87 SearchBoxModel* model = GetModel(); 88 model->RemoveObserver(this); 89 model->SetText(text); 90 // TODO(tapted): See if this should call SetSelectionModel here. 91 model->AddObserver(this); 92} 93 94void SearchBoxModelObserverBridge::IconChanged() { 95 [[parent_ searchImageView] setImage:gfx::NSImageFromImageSkiaWithColorSpace( 96 GetModel()->icon(), base::mac::GetSRGBColorSpace())]; 97} 98 99void SearchBoxModelObserverBridge::HintTextChanged() { 100 [[[parent_ searchTextField] cell] setPlaceholderString: 101 base::SysUTF16ToNSString(GetModel()->hint_text())]; 102} 103 104void SearchBoxModelObserverBridge::SelectionModelChanged() { 105 // TODO(tapted): See if anything needs to be done here for RTL. 106} 107 108void SearchBoxModelObserverBridge::TextChanged() { 109 // Currently the model text is only changed when we are not observing it, or 110 // it is changed in tests to establish a particular state. 111 [[parent_ searchTextField] 112 setStringValue:base::SysUTF16ToNSString(GetModel()->text())]; 113} 114 115} // namespace app_list 116 117@interface SearchTextField : NSTextField { 118 @private 119 NSRect textFrameInset_; 120} 121 122@property(readonly, nonatomic) NSRect textFrameInset; 123 124- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin 125 rightMargin:(CGFloat)rightMargin; 126 127@end 128 129@interface AppListMenuController : MenuController { 130 @private 131 AppsSearchBoxController* searchBoxController_; // Weak. Owns us. 132} 133 134- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent; 135 136@end 137 138@implementation AppsSearchBoxController 139 140@synthesize delegate = delegate_; 141 142- (id)initWithFrame:(NSRect)frame { 143 if ((self = [super init])) { 144 base::scoped_nsobject<NSView> containerView( 145 [[NSView alloc] initWithFrame:frame]); 146 [self setView:containerView]; 147 [self addSubviews]; 148 } 149 return self; 150} 151 152- (void)clearSearch { 153 [searchTextField_ setStringValue:@""]; 154 [self controlTextDidChange:nil]; 155} 156 157- (void)rebuildMenu { 158 if (![delegate_ appListDelegate]) 159 return; 160 161 menuController_.reset([[AppListMenuController alloc] 162 initWithSearchBoxController:self]); 163 [menuButton_ setMenu:[menuController_ menu]]; // Menu will populate here. 164} 165 166- (void)setDelegate:(id<AppsSearchBoxDelegate>)delegate { 167 [[menuButton_ menu] removeAllItems]; 168 menuController_.reset(); 169 appListMenu_.reset(); 170 bridge_.reset(); // Ensure observers are cleared before updating |delegate_|. 171 delegate_ = delegate; 172 if (!delegate_) 173 return; 174 175 bridge_.reset(new app_list::SearchBoxModelObserverBridge(self)); 176 if (![delegate_ appListDelegate]) 177 return; 178 179 appListMenu_.reset(new app_list::AppListMenu([delegate_ appListDelegate])); 180 [self rebuildMenu]; 181} 182 183- (NSTextField*)searchTextField { 184 return searchTextField_; 185} 186 187- (NSPopUpButton*)menuControl { 188 return menuButton_; 189} 190 191- (app_list::AppListMenu*)appListMenu { 192 return appListMenu_.get(); 193} 194 195- (NSImageView*)searchImageView { 196 return searchImageView_; 197} 198 199- (void)addSubviews { 200 NSRect viewBounds = [[self view] bounds]; 201 searchImageView_.reset([[NSImageView alloc] initWithFrame:NSMakeRect( 202 kPadding, 0, kSearchIconDimension, NSHeight(viewBounds))]); 203 204 searchTextField_.reset([[SearchTextField alloc] initWithFrame:viewBounds]); 205 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 206 [searchTextField_ setDelegate:self]; 207 [searchTextField_ setFont:rb.GetFont( 208 ui::ResourceBundle::MediumFont).GetNativeFont()]; 209 [searchTextField_ 210 setMarginsWithLeftMargin:NSMaxX([searchImageView_ frame]) + kPadding 211 rightMargin:kMenuButtonDimension + 2 * kPadding]; 212 213 // Add the drop-down menu, with a custom button. 214 NSRect buttonFrame = NSMakeRect( 215 NSWidth(viewBounds) - kMenuButtonDimension - kPadding, 216 floor(NSMidY(viewBounds) - kMenuButtonDimension / 2), 217 kMenuButtonDimension, 218 kMenuButtonDimension); 219 menuButton_.reset([[HoverImageMenuButton alloc] initWithFrame:buttonFrame 220 pullsDown:YES]); 221 [[menuButton_ hoverImageMenuButtonCell] setDefaultImage: 222 rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_NORMAL).AsNSImage()]; 223 [[menuButton_ hoverImageMenuButtonCell] setAlternateImage: 224 rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_PRESSED).AsNSImage()]; 225 [[menuButton_ hoverImageMenuButtonCell] setHoverImage: 226 rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_HOVER).AsNSImage()]; 227 228 [[self view] addSubview:searchImageView_]; 229 [[self view] addSubview:searchTextField_]; 230 [[self view] addSubview:menuButton_]; 231} 232 233- (BOOL)control:(NSControl*)control 234 textView:(NSTextView*)textView 235 doCommandBySelector:(SEL)command { 236 // Forward the message first, to handle grid or search results navigation. 237 BOOL handled = [delegate_ control:control 238 textView:textView 239 doCommandBySelector:command]; 240 if (handled) 241 return YES; 242 243 // If the delegate did not handle the escape key, it means the window was not 244 // dismissed because there were search results. Clear them. 245 if (command == @selector(complete:)) { 246 [self clearSearch]; 247 return YES; 248 } 249 250 return NO; 251} 252 253- (void)controlTextDidChange:(NSNotification*)notification { 254 if (bridge_) { 255 bridge_->SetSearchText( 256 base::SysNSStringToUTF16([searchTextField_ stringValue])); 257 } 258 259 [delegate_ modelTextDidChange]; 260} 261 262@end 263 264@interface SearchTextFieldCell : NSTextFieldCell; 265 266- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame; 267 268@end 269 270@implementation SearchTextField 271 272@synthesize textFrameInset = textFrameInset_; 273 274+ (Class)cellClass { 275 return [SearchTextFieldCell class]; 276} 277 278- (id)initWithFrame:(NSRect)theFrame { 279 if ((self = [super initWithFrame:theFrame])) { 280 [self setFocusRingType:NSFocusRingTypeNone]; 281 [self setDrawsBackground:NO]; 282 [self setBordered:NO]; 283 } 284 return self; 285} 286 287- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin 288 rightMargin:(CGFloat)rightMargin { 289 // Find the preferred height for the current text properties, and center. 290 NSRect viewBounds = [self bounds]; 291 [self sizeToFit]; 292 NSRect textBounds = [self bounds]; 293 textFrameInset_.origin.x = leftMargin; 294 textFrameInset_.origin.y = floor(NSMidY(viewBounds) - NSMidY(textBounds)); 295 textFrameInset_.size.width = leftMargin + rightMargin; 296 textFrameInset_.size.height = NSHeight(viewBounds) - NSHeight(textBounds); 297 [self setFrame:viewBounds]; 298} 299 300@end 301 302@implementation SearchTextFieldCell 303 304- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame { 305 SearchTextField* searchTextField = 306 base::mac::ObjCCastStrict<SearchTextField>([self controlView]); 307 NSRect insetRect = [searchTextField textFrameInset]; 308 cellFrame.origin.x += insetRect.origin.x; 309 cellFrame.origin.y += insetRect.origin.y; 310 cellFrame.size.width -= insetRect.size.width; 311 cellFrame.size.height -= insetRect.size.height; 312 return cellFrame; 313} 314 315- (NSRect)textFrameForFrame:(NSRect)cellFrame { 316 return [self textFrameForFrameInternal:cellFrame]; 317} 318 319- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame { 320 return [self textFrameForFrameInternal:cellFrame]; 321} 322 323- (void)resetCursorRect:(NSRect)cellFrame 324 inView:(NSView*)controlView { 325 [super resetCursorRect:[self textCursorFrameForFrame:cellFrame] 326 inView:controlView]; 327} 328 329- (NSRect)drawingRectForBounds:(NSRect)theRect { 330 return [super drawingRectForBounds:[self textFrameForFrame:theRect]]; 331} 332 333- (void)editWithFrame:(NSRect)cellFrame 334 inView:(NSView*)controlView 335 editor:(NSText*)editor 336 delegate:(id)delegate 337 event:(NSEvent*)event { 338 [super editWithFrame:[self textFrameForFrame:cellFrame] 339 inView:controlView 340 editor:editor 341 delegate:delegate 342 event:event]; 343} 344 345- (void)selectWithFrame:(NSRect)cellFrame 346 inView:(NSView*)controlView 347 editor:(NSText*)editor 348 delegate:(id)delegate 349 start:(NSInteger)start 350 length:(NSInteger)length { 351 [super selectWithFrame:[self textFrameForFrame:cellFrame] 352 inView:controlView 353 editor:editor 354 delegate:delegate 355 start:start 356 length:length]; 357} 358 359@end 360 361@implementation AppListMenuController 362 363- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent { 364 // Need to initialze super with a NULL model, otherwise it will immediately 365 // try to populate, which can't be done until setting the parent. 366 if ((self = [super initWithModel:NULL 367 useWithPopUpButtonCell:YES])) { 368 searchBoxController_ = parent; 369 [super setModel:[parent appListMenu]->menu_model()]; 370 } 371 return self; 372} 373 374- (void)addItemToMenu:(NSMenu*)menu 375 atIndex:(NSInteger)index 376 fromModel:(ui::MenuModel*)model { 377 [super addItemToMenu:menu 378 atIndex:index 379 fromModel:model]; 380 if (model->GetCommandIdAt(index) != app_list::AppListMenu::CURRENT_USER) 381 return; 382 383 base::scoped_nsobject<NSView> customItemView([[CurrentUserMenuItemView alloc] 384 initWithCurrentUser:[[searchBoxController_ delegate] currentUserName] 385 userEmail:[[searchBoxController_ delegate] currentUserEmail]]); 386 [[menu itemAtIndex:index] setView:customItemView]; 387} 388 389- (NSRect)confinementRectForMenu:(NSMenu*)menu 390 onScreen:(NSScreen*)screen { 391 NSPopUpButton* menuButton = [searchBoxController_ menuControl]; 392 // Ensure the menu comes up below the menu button by trimming the window frame 393 // to a point anchored below the bottom right of the button. 394 NSRect anchorRect = [menuButton convertRect:[menuButton bounds] 395 toView:nil]; 396 NSPoint anchorPoint = [[menuButton window] convertBaseToScreen:NSMakePoint( 397 NSMaxX(anchorRect) + kMenuXOffsetFromButton, 398 NSMinY(anchorRect) - kMenuYOffsetFromButton)]; 399 NSRect confinementRect = [[menuButton window] frame]; 400 confinementRect.size = NSMakeSize(anchorPoint.x - NSMinX(confinementRect), 401 anchorPoint.y - NSMinY(confinementRect)); 402 return confinementRect; 403} 404 405@end 406