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.
5 #import "ui/app_list/cocoa/apps_search_results_controller.h"
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"
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;
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;
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);
39 @interface AppsSearchResultsController ()
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;
51 @interface AppsSearchResultsCell : NSTextFieldCell
54 // Immutable class representing a search result in the NSTableView.
55 @interface AppsSearchResultRep : NSObject<NSCopying> {
57 base::scoped_nsobject<NSAttributedString> attributedStringValue_;
58 base::scoped_nsobject<NSImage> resultIcon_;
61 @property(readonly, nonatomic) NSAttributedString* attributedStringValue;
62 @property(readonly, nonatomic) NSImage* resultIcon;
64 - (id)initWithSearchResult:(app_list::SearchResult*)result;
66 - (NSMutableAttributedString*)createRenderText:(const base::string16&)content
67 tags:(const app_list::SearchResult::Tags&)tags;
69 - (NSAttributedString*)createResultsAttributedStringWithModel
70 :(app_list::SearchResult*)result;
74 // Simple extension to NSTableView that passes mouseDown events to the
75 // delegate so that drag events can be detected, and forwards requests for
77 @interface AppsSearchResultsTableView : NSTableView
79 - (AppsSearchResultsController*)controller;
83 @implementation AppsSearchResultsController
85 @synthesize delegate = delegate_;
87 - (id)initWithAppsSearchResultsFrameSize:(NSSize)size {
88 if ((self = [super init])) {
89 hoveredRowIndex_ = -1;
90 [self loadAndSetViewWithResultsFrameSize:size];
95 - (app_list::AppListModel::SearchResults*)results {
96 DCHECK([delegate_ appListModel]);
97 return [delegate_ appListModel]->results();
100 - (NSTableView*)tableView {
104 - (void)setDelegate:(id<AppsSearchResultsDelegate>)newDelegate {
106 delegate_ = newDelegate;
107 app_list::AppListModel* appListModel = [delegate_ appListModel];
108 if (!appListModel || !appListModel->results()) {
109 [tableView_ reloadData];
113 bridge_.reset(new app_list::AppsSearchResultsModelBridge(self));
114 [tableView_ reloadData];
117 - (BOOL)handleCommandBySelector:(SEL)command {
118 if (command == @selector(insertNewline:) ||
119 command == @selector(insertLineBreak:)) {
120 [self activateSelection];
124 if (command == @selector(moveUp:))
125 return [self moveSelectionByDelta:-1];
127 if (command == @selector(moveDown:))
128 return [self moveSelectionByDelta:1];
133 - (void)loadAndSetViewWithResultsFrameSize:(NSSize)size {
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_ setGridColor:
141 gfx::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];
148 // Tracking to highlight an individual row on mouseover.
150 [[CrTrackingArea alloc] initWithRect:NSZeroRect
151 options:NSTrackingInVisibleRect |
152 NSTrackingMouseEnteredAndExited |
153 NSTrackingMouseMoved |
154 NSTrackingActiveInKeyWindow
157 [tableView_ addTrackingArea:trackingArea_.get()];
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];
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)]);
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)]);
178 [containerView addSubview:tableView_];
179 [clipView addSubview:containerView];
180 [self setView:clipView];
183 - (void)mouseDown:(NSEvent*)theEvent {
184 lastMouseDownInView_ = [tableView_ convertPoint:[theEvent locationInWindow]
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]
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];
199 // Mouse tracking is suppressed by the NSTableView during a drag, so ensure
200 // any hover state is cleaned up.
201 [self mouseMoved:event];
204 - (app_list::AppListModel::SearchResults*)searchResults {
205 app_list::AppListModel* appListModel = [delegate_ appListModel];
207 DCHECK(appListModel);
208 DCHECK(appListModel->results());
209 return appListModel->results();
212 - (void)activateSelection {
213 NSInteger selectedRow = [tableView_ selectedRow];
214 if (!bridge_ || selectedRow < 0)
217 [delegate_ openResult:[self searchResults]->GetItemAt(selectedRow)];
220 - (BOOL)moveSelectionByDelta:(NSInteger)delta {
221 NSInteger rowCount = [tableView_ numberOfRows];
225 NSInteger selectedRow = [tableView_ selectedRow];
227 if (selectedRow == -1) {
228 // No selection. Select first or last, based on direction.
229 targetRow = delta > 0 ? 0 : rowCount - 1;
231 targetRow = (selectedRow + delta) % rowCount;
233 targetRow += rowCount;
236 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:targetRow]
237 byExtendingSelection:NO];
241 - (NSMenu*)contextMenuForRow:(NSInteger)rowIndex {
246 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:rowIndex]
247 byExtendingSelection:NO];
248 return bridge_->MenuForItem(rowIndex);
251 - (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView {
252 return bridge_ ? [self searchResults]->item_count() : 0;
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];
265 base::scoped_nsobject<AppsSearchResultRep> resultRep(
266 [[AppsSearchResultRep alloc]
267 initWithSearchResult:[self searchResults]->GetItemAt(rowIndex)]);
268 return resultRep.autorelease();
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];
280 [cell setBackgroundStyle:kBackgroundNormal];
283 - (void)mouseExited:(NSEvent*)theEvent {
284 if (hoveredRowIndex_ == -1)
287 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
288 hoveredRowIndex_ = -1;
291 - (void)mouseMoved:(NSEvent*)theEvent {
292 NSPoint pointInView = [tableView_ convertPoint:[theEvent locationInWindow]
294 NSInteger newIndex = [tableView_ rowAtPoint:pointInView];
295 if (newIndex == hoveredRowIndex_)
299 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:newIndex]];
300 if (hoveredRowIndex_ != -1)
301 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
302 hoveredRowIndex_ = newIndex;
307 @implementation AppsSearchResultRep
309 - (NSAttributedString*)attributedStringValue {
310 return attributedStringValue_;
313 - (NSImage*)resultIcon {
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]);
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 gfx::SkColorToSRGBNSColor(app_list::kResultDefaultTextColor),
338 NSParagraphStyleAttributeName: paragraphStyle
341 base::scoped_nsobject<NSMutableAttributedString> text(
342 [[NSMutableAttributedString alloc]
343 initWithString:base::SysUTF16ToNSString(content)
344 attributes:defaultAttributes]);
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)
351 if (it->styles & app_list::SearchResult::Tag::MATCH) {
353 NSFontManager* fontManager = [NSFontManager sharedFontManager];
354 boldFont = [fontManager convertFont:[NSFont controlContentFontOfSize:0]
355 toHaveTrait:NSBoldFontMask];
357 [text addAttribute:NSFontAttributeName
359 range:it->range.ToNSRange()];
362 if (it->styles & app_list::SearchResult::Tag::DIM) {
363 NSColor* dimmedColor =
364 gfx::SkColorToSRGBNSColor(app_list::kResultDimmedTextColor);
365 [text addAttribute:NSForegroundColorAttributeName
367 range:it->range.ToNSRange()];
368 } else if (it->styles & app_list::SearchResult::Tag::URL) {
370 gfx::SkColorToSRGBNSColor(app_list::kResultURLTextColor);
371 [text addAttribute:NSForegroundColorAttributeName
373 range:it->range.ToNSRange()];
377 return text.autorelease();
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];
397 - (id)copyWithZone:(NSZone*)zone {
398 return [self retain];
403 @implementation AppsSearchResultsTableView
405 - (AppsSearchResultsController*)controller {
406 return base::mac::ObjCCastStrict<AppsSearchResultsController>(
410 - (void)mouseDown:(NSEvent*)theEvent {
411 [[self controller] mouseDown:theEvent];
412 [super mouseDown:theEvent];
415 - (NSMenu*)menuForEvent:(NSEvent*)theEvent {
416 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
418 return [[self controller] contextMenuForRow:[self rowAtPoint:pointInView]];
423 @implementation AppsSearchResultsCell
425 - (void)drawWithFrame:(NSRect)cellFrame
426 inView:(NSView*)controlView {
427 if ([self backgroundStyle] != kBackgroundNormal) {
428 if ([self backgroundStyle] == kBackgroundSelected)
429 [gfx::SkColorToSRGBNSColor(kSelectedRowColor) set];
431 [gfx::SkColorToSRGBNSColor(kHighlightedRowColor) set];
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);
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);
449 [titleText drawInRect:titleRect];
451 NSImage* resultIcon = [[self objectValue] resultIcon];
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
463 operation:NSCompositeSourceOver