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