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