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;
34 @interface AppsSearchResultsController ()
36 - (void)loadAndSetViewWithResultsFrameSize:(NSSize)size;
37 - (void)mouseDown:(NSEvent*)theEvent;
38 - (void)tableViewClicked:(id)sender;
39 - (app_list::AppListModel::SearchResults*)searchResults;
40 - (void)activateSelection;
41 - (BOOL)moveSelectionByDelta:(NSInteger)delta;
42 - (NSMenu*)contextMenuForRow:(NSInteger)rowIndex;
46 @interface AppsSearchResultsCell : NSTextFieldCell
49 // Immutable class representing a search result in the NSTableView.
50 @interface AppsSearchResultRep : NSObject<NSCopying> {
52 base::scoped_nsobject<NSAttributedString> attributedStringValue_;
53 base::scoped_nsobject<NSImage> resultIcon_;
56 @property(readonly, nonatomic) NSAttributedString* attributedStringValue;
57 @property(readonly, nonatomic) NSImage* resultIcon;
59 - (id)initWithSearchResult:(app_list::SearchResult*)result;
61 - (NSMutableAttributedString*)createRenderText:(const base::string16&)content
62 tags:(const app_list::SearchResult::Tags&)tags;
64 - (NSAttributedString*)createResultsAttributedStringWithModel
65 :(app_list::SearchResult*)result;
69 // Simple extension to NSTableView that passes mouseDown events to the
70 // delegate so that drag events can be detected, and forwards requests for
72 @interface AppsSearchResultsTableView : NSTableView
74 - (AppsSearchResultsController*)controller;
78 @implementation AppsSearchResultsController
80 @synthesize delegate = delegate_;
82 - (id)initWithAppsSearchResultsFrameSize:(NSSize)size {
83 if ((self = [super init])) {
84 hoveredRowIndex_ = -1;
85 [self loadAndSetViewWithResultsFrameSize:size];
90 - (app_list::AppListModel::SearchResults*)results {
91 DCHECK([delegate_ appListModel]);
92 return [delegate_ appListModel]->results();
95 - (NSTableView*)tableView {
99 - (void)setDelegate:(id<AppsSearchResultsDelegate>)newDelegate {
101 delegate_ = newDelegate;
102 app_list::AppListModel* appListModel = [delegate_ appListModel];
103 if (!appListModel || !appListModel->results()) {
104 [tableView_ reloadData];
108 bridge_.reset(new app_list::AppsSearchResultsModelBridge(self));
109 [tableView_ reloadData];
112 - (BOOL)handleCommandBySelector:(SEL)command {
113 if (command == @selector(insertNewline:) ||
114 command == @selector(insertLineBreak:)) {
115 [self activateSelection];
119 if (command == @selector(moveUp:))
120 return [self moveSelectionByDelta:-1];
122 if (command == @selector(moveDown:))
123 return [self moveSelectionByDelta:1];
128 - (void)loadAndSetViewWithResultsFrameSize:(NSSize)size {
130 [[AppsSearchResultsTableView alloc] initWithFrame:NSZeroRect]);
131 // Refuse first responder so that focus stays with the search text field.
132 [tableView_ setRefusesFirstResponder:YES];
133 [tableView_ setRowHeight:kPreferredRowHeight];
134 [tableView_ setGridStyleMask:NSTableViewSolidHorizontalGridLineMask];
135 [tableView_ setGridColor:
136 gfx::SkColorToSRGBNSColor(app_list::kResultBorderColor)];
137 [tableView_ setBackgroundColor:[NSColor clearColor]];
138 [tableView_ setAction:@selector(tableViewClicked:)];
139 [tableView_ setDelegate:self];
140 [tableView_ setDataSource:self];
141 [tableView_ setTarget:self];
143 // Tracking to highlight an individual row on mouseover.
145 [[CrTrackingArea alloc] initWithRect:NSZeroRect
146 options:NSTrackingInVisibleRect |
147 NSTrackingMouseEnteredAndExited |
148 NSTrackingMouseMoved |
149 NSTrackingActiveInKeyWindow
152 [tableView_ addTrackingArea:trackingArea_.get()];
154 base::scoped_nsobject<NSTableColumn> resultsColumn(
155 [[NSTableColumn alloc] initWithIdentifier:@""]);
156 base::scoped_nsobject<NSCell> resultsDataCell(
157 [[AppsSearchResultsCell alloc] initTextCell:@""]);
158 [resultsColumn setDataCell:resultsDataCell];
159 [resultsColumn setWidth:size.width];
160 [tableView_ addTableColumn:resultsColumn];
162 // An NSTableView is normally put in a NSScrollView, but scrolling is not
163 // used for the app list. Instead, place it in a container with the desired
164 // size; flipped so the table is anchored to the top-left.
165 base::scoped_nsobject<FlippedView> containerView([[FlippedView alloc]
166 initWithFrame:NSMakeRect(0, 0, size.width, size.height)]);
168 // The container is then anchored in an un-flipped view, initially hidden,
169 // so that |containerView| slides in from the top when showing results.
170 base::scoped_nsobject<NSView> clipView(
171 [[NSView alloc] initWithFrame:NSMakeRect(0, 0, size.width, 0)]);
173 [containerView addSubview:tableView_];
174 [clipView addSubview:containerView];
175 [self setView:clipView];
178 - (void)mouseDown:(NSEvent*)theEvent {
179 lastMouseDownInView_ = [tableView_ convertPoint:[theEvent locationInWindow]
183 - (void)tableViewClicked:(id)sender {
184 const CGFloat kDragThreshold = 5;
185 // If the user clicked and then dragged elsewhere, ignore the click.
186 NSEvent* event = [[tableView_ window] currentEvent];
187 NSPoint pointInView = [tableView_ convertPoint:[event locationInWindow]
189 CGFloat deltaX = pointInView.x - lastMouseDownInView_.x;
190 CGFloat deltaY = pointInView.y - lastMouseDownInView_.y;
191 if (deltaX * deltaX + deltaY * deltaY <= kDragThreshold * kDragThreshold)
192 [self activateSelection];
194 // Mouse tracking is suppressed by the NSTableView during a drag, so ensure
195 // any hover state is cleaned up.
196 [self mouseMoved:event];
199 - (app_list::AppListModel::SearchResults*)searchResults {
200 app_list::AppListModel* appListModel = [delegate_ appListModel];
202 DCHECK(appListModel);
203 DCHECK(appListModel->results());
204 return appListModel->results();
207 - (void)activateSelection {
208 NSInteger selectedRow = [tableView_ selectedRow];
209 if (!bridge_ || selectedRow < 0)
212 [delegate_ openResult:[self searchResults]->GetItemAt(selectedRow)];
215 - (BOOL)moveSelectionByDelta:(NSInteger)delta {
216 NSInteger rowCount = [tableView_ numberOfRows];
220 NSInteger selectedRow = [tableView_ selectedRow];
222 if (selectedRow == -1) {
223 // No selection. Select first or last, based on direction.
224 targetRow = delta > 0 ? 0 : rowCount - 1;
226 targetRow = (selectedRow + delta) % rowCount;
228 targetRow += rowCount;
231 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:targetRow]
232 byExtendingSelection:NO];
236 - (NSMenu*)contextMenuForRow:(NSInteger)rowIndex {
241 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:rowIndex]
242 byExtendingSelection:NO];
243 return bridge_->MenuForItem(rowIndex);
246 - (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView {
247 return bridge_ ? [self searchResults]->item_count() : 0;
250 - (id)tableView:(NSTableView*)aTableView
251 objectValueForTableColumn:(NSTableColumn*)aTableColumn
252 row:(NSInteger)rowIndex {
253 // When the results were previously cleared, nothing will be selected. For
254 // that case, select the first row when it appears.
255 if (rowIndex == 0 && [tableView_ selectedRow] == -1) {
256 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:0]
257 byExtendingSelection:NO];
260 base::scoped_nsobject<AppsSearchResultRep> resultRep(
261 [[AppsSearchResultRep alloc]
262 initWithSearchResult:[self searchResults]->GetItemAt(rowIndex)]);
263 return resultRep.autorelease();
266 - (void)tableView:(NSTableView*)tableView
267 willDisplayCell:(id)cell
268 forTableColumn:(NSTableColumn*)tableColumn
269 row:(NSInteger)rowIndex {
270 if (rowIndex == [tableView selectedRow])
271 [cell setBackgroundStyle:kBackgroundSelected];
272 else if (rowIndex == hoveredRowIndex_)
273 [cell setBackgroundStyle:kBackgroundHovered];
275 [cell setBackgroundStyle:kBackgroundNormal];
278 - (void)mouseExited:(NSEvent*)theEvent {
279 if (hoveredRowIndex_ == -1)
282 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
283 hoveredRowIndex_ = -1;
286 - (void)mouseMoved:(NSEvent*)theEvent {
287 NSPoint pointInView = [tableView_ convertPoint:[theEvent locationInWindow]
289 NSInteger newIndex = [tableView_ rowAtPoint:pointInView];
290 if (newIndex == hoveredRowIndex_)
294 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:newIndex]];
295 if (hoveredRowIndex_ != -1)
296 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
297 hoveredRowIndex_ = newIndex;
302 @implementation AppsSearchResultRep
304 - (NSAttributedString*)attributedStringValue {
305 return attributedStringValue_;
308 - (NSImage*)resultIcon {
312 - (id)initWithSearchResult:(app_list::SearchResult*)result {
313 if ((self = [super init])) {
314 attributedStringValue_.reset(
315 [[self createResultsAttributedStringWithModel:result] retain]);
316 if (!result->icon().isNull()) {
317 resultIcon_.reset([gfx::NSImageFromImageSkiaWithColorSpace(
318 result->icon(), base::mac::GetSRGBColorSpace()) retain]);
324 - (NSMutableAttributedString*)createRenderText:(const base::string16&)content
325 tags:(const app_list::SearchResult::Tags&)tags {
326 NSFont* boldFont = nil;
327 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
328 [[NSMutableParagraphStyle alloc] init]);
329 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
330 NSDictionary* defaultAttributes = @{
331 NSForegroundColorAttributeName:
332 gfx::SkColorToSRGBNSColor(app_list::kResultDefaultTextColor),
333 NSParagraphStyleAttributeName: paragraphStyle
336 base::scoped_nsobject<NSMutableAttributedString> text(
337 [[NSMutableAttributedString alloc]
338 initWithString:base::SysUTF16ToNSString(content)
339 attributes:defaultAttributes]);
341 for (app_list::SearchResult::Tags::const_iterator it = tags.begin();
342 it != tags.end(); ++it) {
343 if (it->styles == app_list::SearchResult::Tag::NONE)
346 if (it->styles & app_list::SearchResult::Tag::MATCH) {
348 NSFontManager* fontManager = [NSFontManager sharedFontManager];
349 boldFont = [fontManager convertFont:[NSFont controlContentFontOfSize:0]
350 toHaveTrait:NSBoldFontMask];
352 [text addAttribute:NSFontAttributeName
354 range:it->range.ToNSRange()];
357 if (it->styles & app_list::SearchResult::Tag::DIM) {
358 NSColor* dimmedColor =
359 gfx::SkColorToSRGBNSColor(app_list::kResultDimmedTextColor);
360 [text addAttribute:NSForegroundColorAttributeName
362 range:it->range.ToNSRange()];
363 } else if (it->styles & app_list::SearchResult::Tag::URL) {
365 gfx::SkColorToSRGBNSColor(app_list::kResultURLTextColor);
366 [text addAttribute:NSForegroundColorAttributeName
368 range:it->range.ToNSRange()];
372 return text.autorelease();
375 - (NSAttributedString*)createResultsAttributedStringWithModel
376 :(app_list::SearchResult*)result {
377 NSMutableAttributedString* titleText =
378 [self createRenderText:result->title()
379 tags:result->title_tags()];
380 if (!result->details().empty()) {
381 NSMutableAttributedString* detailText =
382 [self createRenderText:result->details()
383 tags:result->details_tags()];
384 base::scoped_nsobject<NSAttributedString> lineBreak(
385 [[NSAttributedString alloc] initWithString:@"\n"]);
386 [titleText appendAttributedString:lineBreak];
387 [titleText appendAttributedString:detailText];
392 - (id)copyWithZone:(NSZone*)zone {
393 return [self retain];
398 @implementation AppsSearchResultsTableView
400 - (AppsSearchResultsController*)controller {
401 return base::mac::ObjCCastStrict<AppsSearchResultsController>(
405 - (void)mouseDown:(NSEvent*)theEvent {
406 [[self controller] mouseDown:theEvent];
407 [super mouseDown:theEvent];
410 - (NSMenu*)menuForEvent:(NSEvent*)theEvent {
411 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
413 return [[self controller] contextMenuForRow:[self rowAtPoint:pointInView]];
418 @implementation AppsSearchResultsCell
420 - (void)drawWithFrame:(NSRect)cellFrame
421 inView:(NSView*)controlView {
422 if ([self backgroundStyle] != kBackgroundNormal) {
423 if ([self backgroundStyle] == kBackgroundSelected)
424 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
426 [gfx::SkColorToSRGBNSColor(app_list::kHighlightedColor) set];
428 // Extend up by one pixel to draw over cell border.
429 NSRect backgroundRect = cellFrame;
430 backgroundRect.origin.y -= 1;
431 backgroundRect.size.height += 1;
432 NSRectFill(backgroundRect);
435 NSAttributedString* titleText = [self attributedStringValue];
436 NSRect titleRect = cellFrame;
437 titleRect.size.width -= kTextTrailPadding + kIconViewWidth;
438 titleRect.origin.x += kIconViewWidth;
439 titleRect.origin.y +=
440 floor(NSHeight(cellFrame) / 2 - [titleText size].height / 2);
441 // Ensure no drawing occurs outside of the cell.
442 titleRect = NSIntersectionRect(titleRect, cellFrame);
444 [titleText drawInRect:titleRect];
446 NSImage* resultIcon = [[self objectValue] resultIcon];
450 NSSize iconSize = [resultIcon size];
451 NSRect iconRect = NSMakeRect(
452 floor(NSMinX(cellFrame) + kIconViewWidth / 2 - iconSize.width / 2),
453 floor(NSMinY(cellFrame) + kPreferredRowHeight / 2 - iconSize.height / 2),
454 std::min(iconSize.width, kIconDimension),
455 std::min(iconSize.height, kIconDimension));
456 [resultIcon drawInRect:iconRect
458 operation:NSCompositeSourceOver