apps_search_box_controller.mm revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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#include "ui/app_list/app_list_model.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 [[parent_ delegate] modelTextDidChange]; 114} 115 116} // namespace app_list 117 118@interface SearchTextField : NSTextField { 119 @private 120 NSRect textFrameInset_; 121} 122 123@property(readonly, nonatomic) NSRect textFrameInset; 124 125- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin 126 rightMargin:(CGFloat)rightMargin; 127 128@end 129 130@interface AppListMenuController : MenuController { 131 @private 132 AppsSearchBoxController* searchBoxController_; // Weak. Owns us. 133} 134 135- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent; 136 137@end 138 139@implementation AppsSearchBoxController 140 141@synthesize delegate = delegate_; 142 143- (id)initWithFrame:(NSRect)frame { 144 if ((self = [super init])) { 145 base::scoped_nsobject<NSView> containerView( 146 [[NSView alloc] initWithFrame:frame]); 147 [self setView:containerView]; 148 [self addSubviews]; 149 } 150 return self; 151} 152 153- (void)clearSearch { 154 [searchTextField_ setStringValue:@""]; 155 [self controlTextDidChange:nil]; 156} 157 158- (void)rebuildMenu { 159 if (![delegate_ appListDelegate]) 160 return; 161 162 menuController_.reset(); 163 appListMenu_.reset( 164 new app_list::AppListMenu([delegate_ appListDelegate], 165 [delegate_ appListModel]->users())); 166 menuController_.reset([[AppListMenuController alloc] 167 initWithSearchBoxController:self]); 168 [menuButton_ setMenu:[menuController_ menu]]; // Menu will populate here. 169} 170 171- (void)setDelegate:(id<AppsSearchBoxDelegate>)delegate { 172 [[menuButton_ menu] removeAllItems]; 173 menuController_.reset(); 174 appListMenu_.reset(); 175 bridge_.reset(); // Ensure observers are cleared before updating |delegate_|. 176 delegate_ = delegate; 177 if (!delegate_) 178 return; 179 180 bridge_.reset(new app_list::SearchBoxModelObserverBridge(self)); 181 [self rebuildMenu]; 182} 183 184- (NSTextField*)searchTextField { 185 return searchTextField_; 186} 187 188- (NSPopUpButton*)menuControl { 189 return menuButton_; 190} 191 192- (app_list::AppListMenu*)appListMenu { 193 return appListMenu_.get(); 194} 195 196- (NSImageView*)searchImageView { 197 return searchImageView_; 198} 199 200- (void)addSubviews { 201 NSRect viewBounds = [[self view] bounds]; 202 searchImageView_.reset([[NSImageView alloc] initWithFrame:NSMakeRect( 203 kPadding, 0, kSearchIconDimension, NSHeight(viewBounds))]); 204 205 searchTextField_.reset([[SearchTextField alloc] initWithFrame:viewBounds]); 206 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 207 [searchTextField_ setDelegate:self]; 208 [searchTextField_ setFont:rb.GetFont( 209 ui::ResourceBundle::MediumFont).GetNativeFont()]; 210 [searchTextField_ 211 setMarginsWithLeftMargin:NSMaxX([searchImageView_ frame]) + kPadding 212 rightMargin:kMenuButtonDimension + 2 * kPadding]; 213 214 // Add the drop-down menu, with a custom button. 215 NSRect buttonFrame = NSMakeRect( 216 NSWidth(viewBounds) - kMenuButtonDimension - kPadding, 217 floor(NSMidY(viewBounds) - kMenuButtonDimension / 2), 218 kMenuButtonDimension, 219 kMenuButtonDimension); 220 menuButton_.reset([[HoverImageMenuButton alloc] initWithFrame:buttonFrame 221 pullsDown:YES]); 222 [[menuButton_ hoverImageMenuButtonCell] setDefaultImage: 223 rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_NORMAL).AsNSImage()]; 224 [[menuButton_ hoverImageMenuButtonCell] setAlternateImage: 225 rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_PRESSED).AsNSImage()]; 226 [[menuButton_ hoverImageMenuButtonCell] setHoverImage: 227 rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_HOVER).AsNSImage()]; 228 229 [[self view] addSubview:searchImageView_]; 230 [[self view] addSubview:searchTextField_]; 231 [[self view] addSubview:menuButton_]; 232} 233 234- (BOOL)control:(NSControl*)control 235 textView:(NSTextView*)textView 236 doCommandBySelector:(SEL)command { 237 // Forward the message first, to handle grid or search results navigation. 238 BOOL handled = [delegate_ control:control 239 textView:textView 240 doCommandBySelector:command]; 241 if (handled) 242 return YES; 243 244 // If the delegate did not handle the escape key, it means the window was not 245 // dismissed because there were search results. Clear them. 246 if (command == @selector(complete:)) { 247 [self clearSearch]; 248 return YES; 249 } 250 251 return NO; 252} 253 254- (void)controlTextDidChange:(NSNotification*)notification { 255 if (bridge_) { 256 bridge_->SetSearchText( 257 base::SysNSStringToUTF16([searchTextField_ stringValue])); 258 } 259 260 [delegate_ modelTextDidChange]; 261} 262 263@end 264 265@interface SearchTextFieldCell : NSTextFieldCell; 266 267- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame; 268 269@end 270 271@implementation SearchTextField 272 273@synthesize textFrameInset = textFrameInset_; 274 275+ (Class)cellClass { 276 return [SearchTextFieldCell class]; 277} 278 279- (id)initWithFrame:(NSRect)theFrame { 280 if ((self = [super initWithFrame:theFrame])) { 281 [self setFocusRingType:NSFocusRingTypeNone]; 282 [self setDrawsBackground:NO]; 283 [self setBordered:NO]; 284 } 285 return self; 286} 287 288- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin 289 rightMargin:(CGFloat)rightMargin { 290 // Find the preferred height for the current text properties, and center. 291 NSRect viewBounds = [self bounds]; 292 [self sizeToFit]; 293 NSRect textBounds = [self bounds]; 294 textFrameInset_.origin.x = leftMargin; 295 textFrameInset_.origin.y = floor(NSMidY(viewBounds) - NSMidY(textBounds)); 296 textFrameInset_.size.width = leftMargin + rightMargin; 297 textFrameInset_.size.height = NSHeight(viewBounds) - NSHeight(textBounds); 298 [self setFrame:viewBounds]; 299} 300 301@end 302 303@implementation SearchTextFieldCell 304 305- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame { 306 SearchTextField* searchTextField = 307 base::mac::ObjCCastStrict<SearchTextField>([self controlView]); 308 NSRect insetRect = [searchTextField textFrameInset]; 309 cellFrame.origin.x += insetRect.origin.x; 310 cellFrame.origin.y += insetRect.origin.y; 311 cellFrame.size.width -= insetRect.size.width; 312 cellFrame.size.height -= insetRect.size.height; 313 return cellFrame; 314} 315 316- (NSRect)textFrameForFrame:(NSRect)cellFrame { 317 return [self textFrameForFrameInternal:cellFrame]; 318} 319 320- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame { 321 return [self textFrameForFrameInternal:cellFrame]; 322} 323 324- (void)resetCursorRect:(NSRect)cellFrame 325 inView:(NSView*)controlView { 326 [super resetCursorRect:[self textCursorFrameForFrame:cellFrame] 327 inView:controlView]; 328} 329 330- (NSRect)drawingRectForBounds:(NSRect)theRect { 331 return [super drawingRectForBounds:[self textFrameForFrame:theRect]]; 332} 333 334- (void)editWithFrame:(NSRect)cellFrame 335 inView:(NSView*)controlView 336 editor:(NSText*)editor 337 delegate:(id)delegate 338 event:(NSEvent*)event { 339 [super editWithFrame:[self textFrameForFrame:cellFrame] 340 inView:controlView 341 editor:editor 342 delegate:delegate 343 event:event]; 344} 345 346- (void)selectWithFrame:(NSRect)cellFrame 347 inView:(NSView*)controlView 348 editor:(NSText*)editor 349 delegate:(id)delegate 350 start:(NSInteger)start 351 length:(NSInteger)length { 352 [super selectWithFrame:[self textFrameForFrame:cellFrame] 353 inView:controlView 354 editor:editor 355 delegate:delegate 356 start:start 357 length:length]; 358} 359 360@end 361 362@implementation AppListMenuController 363 364- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent { 365 // Need to initialze super with a NULL model, otherwise it will immediately 366 // try to populate, which can't be done until setting the parent. 367 if ((self = [super initWithModel:NULL 368 useWithPopUpButtonCell:YES])) { 369 searchBoxController_ = parent; 370 [super setModel:[parent appListMenu]->menu_model()]; 371 } 372 return self; 373} 374 375- (NSRect)confinementRectForMenu:(NSMenu*)menu 376 onScreen:(NSScreen*)screen { 377 NSPopUpButton* menuButton = [searchBoxController_ menuControl]; 378 // Ensure the menu comes up below the menu button by trimming the window frame 379 // to a point anchored below the bottom right of the button. 380 NSRect anchorRect = [menuButton convertRect:[menuButton bounds] 381 toView:nil]; 382 NSPoint anchorPoint = [[menuButton window] convertBaseToScreen:NSMakePoint( 383 NSMaxX(anchorRect) + kMenuXOffsetFromButton, 384 NSMinY(anchorRect) - kMenuYOffsetFromButton)]; 385 NSRect confinementRect = [[menuButton window] frame]; 386 confinementRect.size = NSMakeSize(anchorPoint.x - NSMinX(confinementRect), 387 anchorPoint.y - NSMinY(confinementRect)); 388 return confinementRect; 389} 390 391@end 392