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