apps_search_results_controller.mm revision 7d4cd473f85ac64c3747c96c277f9e506a0d2246
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_results_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "base/strings/sys_string_conversions.h" 9#include "skia/ext/skia_utils_mac.h" 10#include "ui/app_list/app_list_constants.h" 11#include "ui/app_list/app_list_model.h" 12#import "ui/app_list/cocoa/apps_search_results_model_bridge.h" 13#include "ui/app_list/search_result.h" 14#import "ui/base/cocoa/flipped_view.h" 15#include "ui/gfx/image/image_skia_util_mac.h" 16 17namespace { 18 19const CGFloat kPreferredRowHeight = 52; 20const CGFloat kIconDimension = 32; 21const CGFloat kIconPadding = 14; 22const CGFloat kIconViewWidth = kIconDimension + 2 * kIconPadding; 23const CGFloat kTextTrailPadding = kIconPadding; 24 25// Map background styles to represent selection and hover in the results list. 26const NSBackgroundStyle kBackgroundNormal = NSBackgroundStyleLight; 27const NSBackgroundStyle kBackgroundSelected = NSBackgroundStyleDark; 28const NSBackgroundStyle kBackgroundHovered = NSBackgroundStyleRaised; 29 30} // namespace 31 32@interface AppsSearchResultsController () 33 34- (void)loadAndSetViewWithResultsFrameSize:(NSSize)size; 35- (void)mouseDown:(NSEvent*)theEvent; 36- (void)tableViewClicked:(id)sender; 37- (app_list::AppListModel::SearchResults*)searchResults; 38- (void)activateSelection; 39- (BOOL)moveSelectionByDelta:(NSInteger)delta; 40 41@end 42 43@interface AppsSearchResultsCell : NSTextFieldCell 44@end 45 46// Immutable class representing a search result in the NSTableView. 47@interface AppsSearchResultRep : NSObject<NSCopying> { 48 @private 49 scoped_nsobject<NSAttributedString> attributedStringValue_; 50 scoped_nsobject<NSImage> resultIcon_; 51} 52 53@property(readonly, nonatomic) NSAttributedString* attributedStringValue; 54@property(readonly, nonatomic) NSImage* resultIcon; 55 56- (id)initWithSearchResult:(app_list::SearchResult*)result; 57 58- (NSMutableAttributedString*)createRenderText:(const base::string16&)content 59 tags:(const app_list::SearchResult::Tags&)tags; 60 61- (NSAttributedString*)createResultsAttributedStringWithModel 62 :(app_list::SearchResult*)result; 63 64@end 65 66// Simple extension to NSTableView that passes mouseDown events to the 67// delegate so that drag events can be detected. 68@interface AppsSearchResultsTableView : NSTableView 69@end 70 71@implementation AppsSearchResultsController 72 73@synthesize delegate = delegate_; 74 75- (id)initWithAppsSearchResultsFrameSize:(NSSize)size { 76 if ((self = [super init])) { 77 hoveredRowIndex_ = -1; 78 [self loadAndSetViewWithResultsFrameSize:size]; 79 } 80 return self; 81} 82 83- (void)setDelegate:(id<AppsSearchResultsDelegate>)newDelegate { 84 bridge_.reset(); 85 delegate_ = newDelegate; 86 app_list::AppListModel* appListModel = [delegate_ appListModel]; 87 if (!appListModel || !appListModel->results()) { 88 [tableView_ reloadData]; 89 return; 90 } 91 92 bridge_.reset(new app_list::AppsSearchResultsModelBridge( 93 appListModel->results(), tableView_)); 94 [tableView_ reloadData]; 95} 96 97- (BOOL)handleCommandBySelector:(SEL)command { 98 if (command == @selector(insertNewline:) || 99 command == @selector(insertLineBreak:)) { 100 [self activateSelection]; 101 return YES; 102 } 103 104 if (command == @selector(moveUp:)) 105 return [self moveSelectionByDelta:-1]; 106 107 if (command == @selector(moveDown:)) 108 return [self moveSelectionByDelta:1]; 109 110 return NO; 111} 112 113- (NSTableView*)tableView { 114 return tableView_; 115} 116 117- (void)loadAndSetViewWithResultsFrameSize:(NSSize)size { 118 tableView_.reset( 119 [[AppsSearchResultsTableView alloc] initWithFrame:NSZeroRect]); 120 // Refuse first responder so that focus stays with the search text field. 121 [tableView_ setRefusesFirstResponder:YES]; 122 [tableView_ setRowHeight:kPreferredRowHeight]; 123 [tableView_ setGridStyleMask:NSTableViewSolidHorizontalGridLineMask]; 124 [tableView_ setGridColor: 125 gfx::SkColorToCalibratedNSColor(app_list::kResultBorderColor)]; 126 [tableView_ setBackgroundColor:[NSColor clearColor]]; 127 [tableView_ setAction:@selector(tableViewClicked:)]; 128 [tableView_ setDelegate:self]; 129 [tableView_ setDataSource:self]; 130 [tableView_ setTarget:self]; 131 132 // Tracking to highlight an individual row on mouseover. 133 trackingArea_.reset( 134 [[CrTrackingArea alloc] initWithRect:NSZeroRect 135 options:NSTrackingInVisibleRect | 136 NSTrackingMouseEnteredAndExited | 137 NSTrackingMouseMoved | 138 NSTrackingActiveInKeyWindow 139 owner:self 140 userInfo:nil]); 141 [tableView_ addTrackingArea:trackingArea_.get()]; 142 143 scoped_nsobject<NSTableColumn> resultsColumn( 144 [[NSTableColumn alloc] initWithIdentifier:@""]); 145 scoped_nsobject<NSCell> resultsDataCell( 146 [[AppsSearchResultsCell alloc] initTextCell:@""]); 147 [resultsColumn setDataCell:resultsDataCell]; 148 [resultsColumn setWidth:size.width]; 149 [tableView_ addTableColumn:resultsColumn]; 150 151 // An NSTableView is normally put in a NSScrollView, but scrolling is not 152 // used for the app list. Instead, place it in a container with the desired 153 // size; flipped so the table is anchored to the top-left. 154 scoped_nsobject<FlippedView> containerView([[FlippedView alloc] initWithFrame: 155 NSMakeRect(0, 0, size.width, size.height)]); 156 157 // The container is then anchored in an un-flipped view, initially hidden, 158 // so that |containerView| slides in from the top when showing results. 159 scoped_nsobject<NSView> clipView([[NSView alloc] initWithFrame: 160 NSMakeRect(0, 0, size.width, 0)]); 161 162 [containerView addSubview:tableView_]; 163 [clipView addSubview:containerView]; 164 [self setView:clipView]; 165} 166 167- (void)mouseDown:(NSEvent*)theEvent { 168 lastMouseDownInView_ = [tableView_ convertPoint:[theEvent locationInWindow] 169 fromView:nil]; 170} 171 172- (void)tableViewClicked:(id)sender { 173 const CGFloat kDragThreshold = 5; 174 // If the user clicked and then dragged elsewhere, ignore the click. 175 NSEvent* event = [[tableView_ window] currentEvent]; 176 NSPoint pointInView = [tableView_ convertPoint:[event locationInWindow] 177 fromView:nil]; 178 CGFloat deltaX = pointInView.x - lastMouseDownInView_.x; 179 CGFloat deltaY = pointInView.y - lastMouseDownInView_.y; 180 if (deltaX * deltaX + deltaY * deltaY <= kDragThreshold * kDragThreshold) 181 [self activateSelection]; 182 183 // Mouse tracking is suppressed by the NSTableView during a drag, so ensure 184 // any hover state is cleaned up. 185 [self mouseMoved:event]; 186} 187 188- (app_list::AppListModel::SearchResults*)searchResults { 189 app_list::AppListModel* appListModel = [delegate_ appListModel]; 190 DCHECK(bridge_); 191 DCHECK(appListModel); 192 DCHECK(appListModel->results()); 193 return appListModel->results(); 194} 195 196- (void)activateSelection { 197 NSInteger selectedRow = [tableView_ selectedRow]; 198 if (!bridge_ || selectedRow < 0) 199 return; 200 201 [delegate_ openResult:[self searchResults]->GetItemAt(selectedRow)]; 202} 203 204- (BOOL)moveSelectionByDelta:(NSInteger)delta { 205 NSInteger rowCount = [tableView_ numberOfRows]; 206 if (rowCount <= 0) 207 return NO; 208 209 NSInteger selectedRow = [tableView_ selectedRow]; 210 NSInteger targetRow; 211 if (selectedRow == -1) { 212 // No selection. Select first or last, based on direction. 213 targetRow = delta > 0 ? 0 : rowCount - 1; 214 } else { 215 targetRow = (selectedRow + delta) % rowCount; 216 if (targetRow < 0) 217 targetRow += rowCount; 218 } 219 220 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:targetRow] 221 byExtendingSelection:NO]; 222 return YES; 223} 224 225- (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView { 226 return bridge_ ? [self searchResults]->item_count() : 0; 227} 228 229- (id)tableView:(NSTableView*)aTableView 230 objectValueForTableColumn:(NSTableColumn*)aTableColumn 231 row:(NSInteger)rowIndex { 232 // When the results were previously cleared, nothing will be selected. For 233 // that case, select the first row when it appears. 234 if (rowIndex == 0 && [tableView_ selectedRow] == -1) { 235 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:0] 236 byExtendingSelection:NO]; 237 } 238 239 scoped_nsobject<AppsSearchResultRep> resultRep([[AppsSearchResultRep alloc] 240 initWithSearchResult:[self searchResults]->GetItemAt(rowIndex)]); 241 return resultRep.autorelease(); 242} 243 244- (void)tableView:(NSTableView*)tableView 245 willDisplayCell:(id)cell 246 forTableColumn:(NSTableColumn*)tableColumn 247 row:(NSInteger)rowIndex { 248 if (rowIndex == [tableView selectedRow]) 249 [cell setBackgroundStyle:kBackgroundSelected]; 250 else if (rowIndex == hoveredRowIndex_) 251 [cell setBackgroundStyle:kBackgroundHovered]; 252 else 253 [cell setBackgroundStyle:kBackgroundNormal]; 254} 255 256- (void)mouseExited:(NSEvent*)theEvent { 257 if (hoveredRowIndex_ == -1) 258 return; 259 260 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]]; 261 hoveredRowIndex_ = -1; 262} 263 264- (void)mouseMoved:(NSEvent*)theEvent { 265 NSPoint pointInView = [tableView_ convertPoint:[theEvent locationInWindow] 266 fromView:nil]; 267 NSInteger newIndex = [tableView_ rowAtPoint:pointInView]; 268 if (newIndex == hoveredRowIndex_) 269 return; 270 271 if (newIndex != -1) 272 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:newIndex]]; 273 if (hoveredRowIndex_ != -1) 274 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]]; 275 hoveredRowIndex_ = newIndex; 276} 277 278@end 279 280@implementation AppsSearchResultRep 281 282- (NSAttributedString*)attributedStringValue { 283 return attributedStringValue_; 284} 285 286- (NSImage*)resultIcon { 287 return resultIcon_; 288} 289 290- (id)initWithSearchResult:(app_list::SearchResult*)result { 291 if ((self = [super init])) { 292 attributedStringValue_.reset( 293 [[self createResultsAttributedStringWithModel:result] retain]); 294 if (!result->icon().isNull()) 295 resultIcon_.reset([gfx::NSImageFromImageSkia(result->icon()) retain]); 296 } 297 return self; 298} 299 300- (NSMutableAttributedString*)createRenderText:(const base::string16&)content 301 tags:(const app_list::SearchResult::Tags&)tags { 302 NSFont* boldFont = nil; 303 scoped_nsobject<NSMutableParagraphStyle> paragraphStyle( 304 [[NSMutableParagraphStyle alloc] init]); 305 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail]; 306 NSDictionary* defaultAttributes = @{ 307 NSForegroundColorAttributeName: 308 gfx::SkColorToCalibratedNSColor(app_list::kResultDefaultTextColor), 309 NSParagraphStyleAttributeName: paragraphStyle 310 }; 311 312 scoped_nsobject<NSMutableAttributedString> text( 313 [[NSMutableAttributedString alloc] 314 initWithString:base::SysUTF16ToNSString(content) 315 attributes:defaultAttributes]); 316 317 for (app_list::SearchResult::Tags::const_iterator it = tags.begin(); 318 it != tags.end(); ++it) { 319 if (it->styles == app_list::SearchResult::Tag::NONE) 320 continue; 321 322 if (it->styles & app_list::SearchResult::Tag::MATCH) { 323 if (!boldFont) { 324 NSFontManager* fontManager = [NSFontManager sharedFontManager]; 325 boldFont = [fontManager convertFont:[NSFont controlContentFontOfSize:0] 326 toHaveTrait:NSBoldFontMask]; 327 } 328 [text addAttribute:NSFontAttributeName 329 value:boldFont 330 range:it->range.ToNSRange()]; 331 } 332 333 if (it->styles & app_list::SearchResult::Tag::DIM) { 334 NSColor* dimmedColor = 335 gfx::SkColorToCalibratedNSColor(app_list::kResultDimmedTextColor); 336 [text addAttribute:NSForegroundColorAttributeName 337 value:dimmedColor 338 range:it->range.ToNSRange()]; 339 } else if (it->styles & app_list::SearchResult::Tag::URL) { 340 NSColor* urlColor = 341 gfx::SkColorToCalibratedNSColor(app_list::kResultURLTextColor); 342 [text addAttribute:NSForegroundColorAttributeName 343 value:urlColor 344 range:it->range.ToNSRange()]; 345 } 346 } 347 348 return text.autorelease(); 349} 350 351- (NSAttributedString*)createResultsAttributedStringWithModel 352 :(app_list::SearchResult*)result { 353 NSMutableAttributedString* titleText = 354 [self createRenderText:result->title() 355 tags:result->title_tags()]; 356 if (!result->details().empty()) { 357 NSMutableAttributedString* detailText = 358 [self createRenderText:result->details() 359 tags:result->details_tags()]; 360 scoped_nsobject<NSAttributedString> lineBreak( 361 [[NSAttributedString alloc] initWithString:@"\n"]); 362 [titleText appendAttributedString:lineBreak]; 363 [titleText appendAttributedString:detailText]; 364 } 365 return titleText; 366} 367 368- (id)copyWithZone:(NSZone*)zone { 369 return [self retain]; 370} 371 372@end 373 374@implementation AppsSearchResultsTableView 375 376- (void)mouseDown:(NSEvent*)theEvent { 377 [base::mac::ObjCCastStrict<AppsSearchResultsController>([self delegate]) 378 mouseDown:theEvent]; 379 [super mouseDown:theEvent]; 380} 381 382@end 383 384@implementation AppsSearchResultsCell 385 386- (void)drawWithFrame:(NSRect)cellFrame 387 inView:(NSView*)controlView { 388 if ([self backgroundStyle] != kBackgroundNormal) { 389 if ([self backgroundStyle] == kBackgroundSelected) 390 [gfx::SkColorToCalibratedNSColor(app_list::kSelectedColor) set]; 391 else 392 [gfx::SkColorToCalibratedNSColor(app_list::kHighlightedColor) set]; 393 394 // Extend up by one pixel to draw over cell border. 395 NSRect backgroundRect = cellFrame; 396 backgroundRect.origin.y -= 1; 397 backgroundRect.size.height += 1; 398 NSRectFill(backgroundRect); 399 } 400 401 NSAttributedString* titleText = [self attributedStringValue]; 402 NSRect titleRect = cellFrame; 403 titleRect.size.width -= kTextTrailPadding + kIconViewWidth; 404 titleRect.origin.x += kIconViewWidth; 405 titleRect.origin.y += 406 floor(NSHeight(cellFrame) / 2 - [titleText size].height / 2); 407 // Ensure no drawing occurs outside of the cell. 408 titleRect = NSIntersectionRect(titleRect, cellFrame); 409 410 [titleText drawInRect:titleRect]; 411 412 NSImage* resultIcon = [[self objectValue] resultIcon]; 413 if (!resultIcon) 414 return; 415 416 NSSize iconSize = [resultIcon size]; 417 NSRect iconRect = NSMakeRect( 418 floor(NSMinX(cellFrame) + kIconViewWidth / 2 - iconSize.width / 2), 419 floor(NSMinY(cellFrame) + kPreferredRowHeight / 2 - iconSize.height / 2), 420 std::min(iconSize.width, kIconDimension), 421 std::min(iconSize.height, kIconDimension)); 422 [resultIcon drawInRect:iconRect 423 fromRect:NSZeroRect 424 operation:NSCompositeSourceOver 425 fraction:1.0 426 respectFlipped:YES 427 hints:nil]; 428} 429 430@end 431