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