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/strings/sys_string_conversions.h"
10 #include "skia/ext/skia_utils_mac.h"
11 #include "ui/app_list/app_list_constants.h"
12 #include "ui/app_list/app_list_model.h"
13 #import "ui/app_list/cocoa/apps_search_results_model_bridge.h"
14 #include "ui/app_list/search_result.h"
15 #import "ui/base/cocoa/flipped_view.h"
16 #include "ui/gfx/image/image_skia_util_mac.h"
20 const CGFloat kPreferredRowHeight = 52;
21 const CGFloat kIconDimension = 32;
22 const CGFloat kIconPadding = 14;
23 const CGFloat kIconViewWidth = kIconDimension + 2 * kIconPadding;
24 const CGFloat kTextTrailPadding = kIconPadding;
26 // Map background styles to represent selection and hover in the results list.
27 const NSBackgroundStyle kBackgroundNormal = NSBackgroundStyleLight;
28 const NSBackgroundStyle kBackgroundSelected = NSBackgroundStyleDark;
29 const NSBackgroundStyle kBackgroundHovered = NSBackgroundStyleRaised;
33 @interface AppsSearchResultsController ()
35 - (void)loadAndSetViewWithResultsFrameSize:(NSSize)size;
36 - (void)mouseDown:(NSEvent*)theEvent;
37 - (void)tableViewClicked:(id)sender;
38 - (app_list::AppListModel::SearchResults*)searchResults;
39 - (void)activateSelection;
40 - (BOOL)moveSelectionByDelta:(NSInteger)delta;
41 - (NSMenu*)contextMenuForRow:(NSInteger)rowIndex;
45 @interface AppsSearchResultsCell : NSTextFieldCell
48 // Immutable class representing a search result in the NSTableView.
49 @interface AppsSearchResultRep : NSObject<NSCopying> {
51 base::scoped_nsobject<NSAttributedString> attributedStringValue_;
52 base::scoped_nsobject<NSImage> resultIcon_;
55 @property(readonly, nonatomic) NSAttributedString* attributedStringValue;
56 @property(readonly, nonatomic) NSImage* resultIcon;
58 - (id)initWithSearchResult:(app_list::SearchResult*)result;
60 - (NSMutableAttributedString*)createRenderText:(const base::string16&)content
61 tags:(const app_list::SearchResult::Tags&)tags;
63 - (NSAttributedString*)createResultsAttributedStringWithModel
64 :(app_list::SearchResult*)result;
68 // Simple extension to NSTableView that passes mouseDown events to the
69 // delegate so that drag events can be detected, and forwards requests for
71 @interface AppsSearchResultsTableView : NSTableView
73 - (AppsSearchResultsController*)controller;
77 @implementation AppsSearchResultsController
79 @synthesize delegate = delegate_;
81 - (id)initWithAppsSearchResultsFrameSize:(NSSize)size {
82 if ((self = [super init])) {
83 hoveredRowIndex_ = -1;
84 [self loadAndSetViewWithResultsFrameSize:size];
89 - (app_list::AppListModel::SearchResults*)results {
90 DCHECK([delegate_ appListModel]);
91 return [delegate_ appListModel]->results();
94 - (NSTableView*)tableView {
98 - (void)setDelegate:(id<AppsSearchResultsDelegate>)newDelegate {
100 delegate_ = newDelegate;
101 app_list::AppListModel* appListModel = [delegate_ appListModel];
102 if (!appListModel || !appListModel->results()) {
103 [tableView_ reloadData];
107 bridge_.reset(new app_list::AppsSearchResultsModelBridge(self));
108 [tableView_ reloadData];
111 - (BOOL)handleCommandBySelector:(SEL)command {
112 if (command == @selector(insertNewline:) ||
113 command == @selector(insertLineBreak:)) {
114 [self activateSelection];
118 if (command == @selector(moveUp:))
119 return [self moveSelectionByDelta:-1];
121 if (command == @selector(moveDown:))
122 return [self moveSelectionByDelta:1];
127 - (void)loadAndSetViewWithResultsFrameSize:(NSSize)size {
129 [[AppsSearchResultsTableView alloc] initWithFrame:NSZeroRect]);
130 // Refuse first responder so that focus stays with the search text field.
131 [tableView_ setRefusesFirstResponder:YES];
132 [tableView_ setRowHeight:kPreferredRowHeight];
133 [tableView_ setGridStyleMask:NSTableViewSolidHorizontalGridLineMask];
134 [tableView_ setGridColor:
135 gfx::SkColorToSRGBNSColor(app_list::kResultBorderColor)];
136 [tableView_ setBackgroundColor:[NSColor clearColor]];
137 [tableView_ setAction:@selector(tableViewClicked:)];
138 [tableView_ setDelegate:self];
139 [tableView_ setDataSource:self];
140 [tableView_ setTarget:self];
142 // Tracking to highlight an individual row on mouseover.
144 [[CrTrackingArea alloc] initWithRect:NSZeroRect
145 options:NSTrackingInVisibleRect |
146 NSTrackingMouseEnteredAndExited |
147 NSTrackingMouseMoved |
148 NSTrackingActiveInKeyWindow
151 [tableView_ addTrackingArea:trackingArea_.get()];
153 base::scoped_nsobject<NSTableColumn> resultsColumn(
154 [[NSTableColumn alloc] initWithIdentifier:@""]);
155 base::scoped_nsobject<NSCell> resultsDataCell(
156 [[AppsSearchResultsCell alloc] initTextCell:@""]);
157 [resultsColumn setDataCell:resultsDataCell];
158 [resultsColumn setWidth:size.width];
159 [tableView_ addTableColumn:resultsColumn];
161 // An NSTableView is normally put in a NSScrollView, but scrolling is not
162 // used for the app list. Instead, place it in a container with the desired
163 // size; flipped so the table is anchored to the top-left.
164 base::scoped_nsobject<FlippedView> containerView([[FlippedView alloc]
165 initWithFrame:NSMakeRect(0, 0, size.width, size.height)]);
167 // The container is then anchored in an un-flipped view, initially hidden,
168 // so that |containerView| slides in from the top when showing results.
169 base::scoped_nsobject<NSView> clipView(
170 [[NSView alloc] initWithFrame:NSMakeRect(0, 0, size.width, 0)]);
172 [containerView addSubview:tableView_];
173 [clipView addSubview:containerView];
174 [self setView:clipView];
177 - (void)mouseDown:(NSEvent*)theEvent {
178 lastMouseDownInView_ = [tableView_ convertPoint:[theEvent locationInWindow]
182 - (void)tableViewClicked:(id)sender {
183 const CGFloat kDragThreshold = 5;
184 // If the user clicked and then dragged elsewhere, ignore the click.
185 NSEvent* event = [[tableView_ window] currentEvent];
186 NSPoint pointInView = [tableView_ convertPoint:[event locationInWindow]
188 CGFloat deltaX = pointInView.x - lastMouseDownInView_.x;
189 CGFloat deltaY = pointInView.y - lastMouseDownInView_.y;
190 if (deltaX * deltaX + deltaY * deltaY <= kDragThreshold * kDragThreshold)
191 [self activateSelection];
193 // Mouse tracking is suppressed by the NSTableView during a drag, so ensure
194 // any hover state is cleaned up.
195 [self mouseMoved:event];
198 - (app_list::AppListModel::SearchResults*)searchResults {
199 app_list::AppListModel* appListModel = [delegate_ appListModel];
201 DCHECK(appListModel);
202 DCHECK(appListModel->results());
203 return appListModel->results();
206 - (void)activateSelection {
207 NSInteger selectedRow = [tableView_ selectedRow];
208 if (!bridge_ || selectedRow < 0)
211 [delegate_ openResult:[self searchResults]->GetItemAt(selectedRow)];
214 - (BOOL)moveSelectionByDelta:(NSInteger)delta {
215 NSInteger rowCount = [tableView_ numberOfRows];
219 NSInteger selectedRow = [tableView_ selectedRow];
221 if (selectedRow == -1) {
222 // No selection. Select first or last, based on direction.
223 targetRow = delta > 0 ? 0 : rowCount - 1;
225 targetRow = (selectedRow + delta) % rowCount;
227 targetRow += rowCount;
230 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:targetRow]
231 byExtendingSelection:NO];
235 - (NSMenu*)contextMenuForRow:(NSInteger)rowIndex {
240 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:rowIndex]
241 byExtendingSelection:NO];
242 return bridge_->MenuForItem(rowIndex);
245 - (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView {
246 return bridge_ ? [self searchResults]->item_count() : 0;
249 - (id)tableView:(NSTableView*)aTableView
250 objectValueForTableColumn:(NSTableColumn*)aTableColumn
251 row:(NSInteger)rowIndex {
252 // When the results were previously cleared, nothing will be selected. For
253 // that case, select the first row when it appears.
254 if (rowIndex == 0 && [tableView_ selectedRow] == -1) {
255 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:0]
256 byExtendingSelection:NO];
259 base::scoped_nsobject<AppsSearchResultRep> resultRep(
260 [[AppsSearchResultRep alloc]
261 initWithSearchResult:[self searchResults]->GetItemAt(rowIndex)]);
262 return resultRep.autorelease();
265 - (void)tableView:(NSTableView*)tableView
266 willDisplayCell:(id)cell
267 forTableColumn:(NSTableColumn*)tableColumn
268 row:(NSInteger)rowIndex {
269 if (rowIndex == [tableView selectedRow])
270 [cell setBackgroundStyle:kBackgroundSelected];
271 else if (rowIndex == hoveredRowIndex_)
272 [cell setBackgroundStyle:kBackgroundHovered];
274 [cell setBackgroundStyle:kBackgroundNormal];
277 - (void)mouseExited:(NSEvent*)theEvent {
278 if (hoveredRowIndex_ == -1)
281 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
282 hoveredRowIndex_ = -1;
285 - (void)mouseMoved:(NSEvent*)theEvent {
286 NSPoint pointInView = [tableView_ convertPoint:[theEvent locationInWindow]
288 NSInteger newIndex = [tableView_ rowAtPoint:pointInView];
289 if (newIndex == hoveredRowIndex_)
293 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:newIndex]];
294 if (hoveredRowIndex_ != -1)
295 [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
296 hoveredRowIndex_ = newIndex;
301 @implementation AppsSearchResultRep
303 - (NSAttributedString*)attributedStringValue {
304 return attributedStringValue_;
307 - (NSImage*)resultIcon {
311 - (id)initWithSearchResult:(app_list::SearchResult*)result {
312 if ((self = [super init])) {
313 attributedStringValue_.reset(
314 [[self createResultsAttributedStringWithModel:result] retain]);
315 if (!result->icon().isNull()) {
316 resultIcon_.reset([gfx::NSImageFromImageSkiaWithColorSpace(
317 result->icon(), base::mac::GetSRGBColorSpace()) retain]);
323 - (NSMutableAttributedString*)createRenderText:(const base::string16&)content
324 tags:(const app_list::SearchResult::Tags&)tags {
325 NSFont* boldFont = nil;
326 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
327 [[NSMutableParagraphStyle alloc] init]);
328 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
329 NSDictionary* defaultAttributes = @{
330 NSForegroundColorAttributeName:
331 gfx::SkColorToSRGBNSColor(app_list::kResultDefaultTextColor),
332 NSParagraphStyleAttributeName: paragraphStyle
335 base::scoped_nsobject<NSMutableAttributedString> text(
336 [[NSMutableAttributedString alloc]
337 initWithString:base::SysUTF16ToNSString(content)
338 attributes:defaultAttributes]);
340 for (app_list::SearchResult::Tags::const_iterator it = tags.begin();
341 it != tags.end(); ++it) {
342 if (it->styles == app_list::SearchResult::Tag::NONE)
345 if (it->styles & app_list::SearchResult::Tag::MATCH) {
347 NSFontManager* fontManager = [NSFontManager sharedFontManager];
348 boldFont = [fontManager convertFont:[NSFont controlContentFontOfSize:0]
349 toHaveTrait:NSBoldFontMask];
351 [text addAttribute:NSFontAttributeName
353 range:it->range.ToNSRange()];
356 if (it->styles & app_list::SearchResult::Tag::DIM) {
357 NSColor* dimmedColor =
358 gfx::SkColorToSRGBNSColor(app_list::kResultDimmedTextColor);
359 [text addAttribute:NSForegroundColorAttributeName
361 range:it->range.ToNSRange()];
362 } else if (it->styles & app_list::SearchResult::Tag::URL) {
364 gfx::SkColorToSRGBNSColor(app_list::kResultURLTextColor);
365 [text addAttribute:NSForegroundColorAttributeName
367 range:it->range.ToNSRange()];
371 return text.autorelease();
374 - (NSAttributedString*)createResultsAttributedStringWithModel
375 :(app_list::SearchResult*)result {
376 NSMutableAttributedString* titleText =
377 [self createRenderText:result->title()
378 tags:result->title_tags()];
379 if (!result->details().empty()) {
380 NSMutableAttributedString* detailText =
381 [self createRenderText:result->details()
382 tags:result->details_tags()];
383 base::scoped_nsobject<NSAttributedString> lineBreak(
384 [[NSAttributedString alloc] initWithString:@"\n"]);
385 [titleText appendAttributedString:lineBreak];
386 [titleText appendAttributedString:detailText];
391 - (id)copyWithZone:(NSZone*)zone {
392 return [self retain];
397 @implementation AppsSearchResultsTableView
399 - (AppsSearchResultsController*)controller {
400 return base::mac::ObjCCastStrict<AppsSearchResultsController>(
404 - (void)mouseDown:(NSEvent*)theEvent {
405 [[self controller] mouseDown:theEvent];
406 [super mouseDown:theEvent];
409 - (NSMenu*)menuForEvent:(NSEvent*)theEvent {
410 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
412 return [[self controller] contextMenuForRow:[self rowAtPoint:pointInView]];
417 @implementation AppsSearchResultsCell
419 - (void)drawWithFrame:(NSRect)cellFrame
420 inView:(NSView*)controlView {
421 if ([self backgroundStyle] != kBackgroundNormal) {
422 if ([self backgroundStyle] == kBackgroundSelected)
423 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
425 [gfx::SkColorToSRGBNSColor(app_list::kHighlightedColor) set];
427 // Extend up by one pixel to draw over cell border.
428 NSRect backgroundRect = cellFrame;
429 backgroundRect.origin.y -= 1;
430 backgroundRect.size.height += 1;
431 NSRectFill(backgroundRect);
434 NSAttributedString* titleText = [self attributedStringValue];
435 NSRect titleRect = cellFrame;
436 titleRect.size.width -= kTextTrailPadding + kIconViewWidth;
437 titleRect.origin.x += kIconViewWidth;
438 titleRect.origin.y +=
439 floor(NSHeight(cellFrame) / 2 - [titleText size].height / 2);
440 // Ensure no drawing occurs outside of the cell.
441 titleRect = NSIntersectionRect(titleRect, cellFrame);
443 [titleText drawInRect:titleRect];
445 NSImage* resultIcon = [[self objectValue] resultIcon];
449 NSSize iconSize = [resultIcon size];
450 NSRect iconRect = NSMakeRect(
451 floor(NSMinX(cellFrame) + kIconViewWidth / 2 - iconSize.width / 2),
452 floor(NSMinY(cellFrame) + kPreferredRowHeight / 2 - iconSize.height / 2),
453 std::min(iconSize.width, kIconDimension),
454 std::min(iconSize.height, kIconDimension));
455 [resultIcon drawInRect:iconRect
457 operation:NSCompositeSourceOver