- add sources.
[platform/framework/web/crosswalk.git] / src / ui / app_list / cocoa / apps_search_results_controller.mm
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/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"
17
18 namespace {
19
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;
25
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;
30
31 }  // namespace
32
33 @interface AppsSearchResultsController ()
34
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;
42
43 @end
44
45 @interface AppsSearchResultsCell : NSTextFieldCell
46 @end
47
48 // Immutable class representing a search result in the NSTableView.
49 @interface AppsSearchResultRep : NSObject<NSCopying> {
50  @private
51   base::scoped_nsobject<NSAttributedString> attributedStringValue_;
52   base::scoped_nsobject<NSImage> resultIcon_;
53 }
54
55 @property(readonly, nonatomic) NSAttributedString* attributedStringValue;
56 @property(readonly, nonatomic) NSImage* resultIcon;
57
58 - (id)initWithSearchResult:(app_list::SearchResult*)result;
59
60 - (NSMutableAttributedString*)createRenderText:(const base::string16&)content
61     tags:(const app_list::SearchResult::Tags&)tags;
62
63 - (NSAttributedString*)createResultsAttributedStringWithModel
64     :(app_list::SearchResult*)result;
65
66 @end
67
68 // Simple extension to NSTableView that passes mouseDown events to the
69 // delegate so that drag events can be detected, and forwards requests for
70 // context menus.
71 @interface AppsSearchResultsTableView : NSTableView
72
73 - (AppsSearchResultsController*)controller;
74
75 @end
76
77 @implementation AppsSearchResultsController
78
79 @synthesize delegate = delegate_;
80
81 - (id)initWithAppsSearchResultsFrameSize:(NSSize)size {
82   if ((self = [super init])) {
83     hoveredRowIndex_ = -1;
84     [self loadAndSetViewWithResultsFrameSize:size];
85   }
86   return self;
87 }
88
89 - (app_list::AppListModel::SearchResults*)results {
90   DCHECK([delegate_ appListModel]);
91   return [delegate_ appListModel]->results();
92 }
93
94 - (NSTableView*)tableView {
95   return tableView_;
96 }
97
98 - (void)setDelegate:(id<AppsSearchResultsDelegate>)newDelegate {
99   bridge_.reset();
100   delegate_ = newDelegate;
101   app_list::AppListModel* appListModel = [delegate_ appListModel];
102   if (!appListModel || !appListModel->results()) {
103     [tableView_ reloadData];
104     return;
105   }
106
107   bridge_.reset(new app_list::AppsSearchResultsModelBridge(self));
108   [tableView_ reloadData];
109 }
110
111 - (BOOL)handleCommandBySelector:(SEL)command {
112   if (command == @selector(insertNewline:) ||
113       command == @selector(insertLineBreak:)) {
114     [self activateSelection];
115     return YES;
116   }
117
118   if (command == @selector(moveUp:))
119     return [self moveSelectionByDelta:-1];
120
121   if (command == @selector(moveDown:))
122     return [self moveSelectionByDelta:1];
123
124   return NO;
125 }
126
127 - (void)loadAndSetViewWithResultsFrameSize:(NSSize)size {
128   tableView_.reset(
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];
141
142   // Tracking to highlight an individual row on mouseover.
143   trackingArea_.reset(
144     [[CrTrackingArea alloc] initWithRect:NSZeroRect
145                                  options:NSTrackingInVisibleRect |
146                                          NSTrackingMouseEnteredAndExited |
147                                          NSTrackingMouseMoved |
148                                          NSTrackingActiveInKeyWindow
149                                    owner:self
150                                 userInfo:nil]);
151   [tableView_ addTrackingArea:trackingArea_.get()];
152
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];
160
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)]);
166
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)]);
171
172   [containerView addSubview:tableView_];
173   [clipView addSubview:containerView];
174   [self setView:clipView];
175 }
176
177 - (void)mouseDown:(NSEvent*)theEvent {
178   lastMouseDownInView_ = [tableView_ convertPoint:[theEvent locationInWindow]
179                                          fromView:nil];
180 }
181
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]
187                                         fromView:nil];
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];
192
193   // Mouse tracking is suppressed by the NSTableView during a drag, so ensure
194   // any hover state is cleaned up.
195   [self mouseMoved:event];
196 }
197
198 - (app_list::AppListModel::SearchResults*)searchResults {
199   app_list::AppListModel* appListModel = [delegate_ appListModel];
200   DCHECK(bridge_);
201   DCHECK(appListModel);
202   DCHECK(appListModel->results());
203   return appListModel->results();
204 }
205
206 - (void)activateSelection {
207   NSInteger selectedRow = [tableView_ selectedRow];
208   if (!bridge_ || selectedRow < 0)
209     return;
210
211   [delegate_ openResult:[self searchResults]->GetItemAt(selectedRow)];
212 }
213
214 - (BOOL)moveSelectionByDelta:(NSInteger)delta {
215   NSInteger rowCount = [tableView_ numberOfRows];
216   if (rowCount <= 0)
217     return NO;
218
219   NSInteger selectedRow = [tableView_ selectedRow];
220   NSInteger targetRow;
221   if (selectedRow == -1) {
222     // No selection. Select first or last, based on direction.
223     targetRow = delta > 0 ? 0 : rowCount - 1;
224   } else {
225     targetRow = (selectedRow + delta) % rowCount;
226     if (targetRow < 0)
227       targetRow += rowCount;
228   }
229
230   [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:targetRow]
231           byExtendingSelection:NO];
232   return YES;
233 }
234
235 - (NSMenu*)contextMenuForRow:(NSInteger)rowIndex {
236   DCHECK(bridge_);
237   if (rowIndex < 0)
238     return nil;
239
240   [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:rowIndex]
241           byExtendingSelection:NO];
242   return bridge_->MenuForItem(rowIndex);
243 }
244
245 - (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView {
246   return bridge_ ? [self searchResults]->item_count() : 0;
247 }
248
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];
257   }
258
259   base::scoped_nsobject<AppsSearchResultRep> resultRep(
260       [[AppsSearchResultRep alloc]
261           initWithSearchResult:[self searchResults]->GetItemAt(rowIndex)]);
262   return resultRep.autorelease();
263 }
264
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];
273   else
274     [cell setBackgroundStyle:kBackgroundNormal];
275 }
276
277 - (void)mouseExited:(NSEvent*)theEvent {
278   if (hoveredRowIndex_ == -1)
279     return;
280
281   [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
282   hoveredRowIndex_ = -1;
283 }
284
285 - (void)mouseMoved:(NSEvent*)theEvent {
286   NSPoint pointInView = [tableView_ convertPoint:[theEvent locationInWindow]
287                                         fromView:nil];
288   NSInteger newIndex = [tableView_ rowAtPoint:pointInView];
289   if (newIndex == hoveredRowIndex_)
290     return;
291
292   if (newIndex != -1)
293     [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:newIndex]];
294   if (hoveredRowIndex_ != -1)
295     [tableView_ setNeedsDisplayInRect:[tableView_ rectOfRow:hoveredRowIndex_]];
296   hoveredRowIndex_ = newIndex;
297 }
298
299 @end
300
301 @implementation AppsSearchResultRep
302
303 - (NSAttributedString*)attributedStringValue {
304   return attributedStringValue_;
305 }
306
307 - (NSImage*)resultIcon {
308   return resultIcon_;
309 }
310
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]);
318     }
319   }
320   return self;
321 }
322
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
333   };
334
335   base::scoped_nsobject<NSMutableAttributedString> text(
336       [[NSMutableAttributedString alloc]
337           initWithString:base::SysUTF16ToNSString(content)
338               attributes:defaultAttributes]);
339
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)
343       continue;
344
345     if (it->styles & app_list::SearchResult::Tag::MATCH) {
346       if (!boldFont) {
347         NSFontManager* fontManager = [NSFontManager sharedFontManager];
348         boldFont = [fontManager convertFont:[NSFont controlContentFontOfSize:0]
349                                 toHaveTrait:NSBoldFontMask];
350       }
351       [text addAttribute:NSFontAttributeName
352                    value:boldFont
353                    range:it->range.ToNSRange()];
354     }
355
356     if (it->styles & app_list::SearchResult::Tag::DIM) {
357       NSColor* dimmedColor =
358           gfx::SkColorToSRGBNSColor(app_list::kResultDimmedTextColor);
359       [text addAttribute:NSForegroundColorAttributeName
360                    value:dimmedColor
361                    range:it->range.ToNSRange()];
362     } else if (it->styles & app_list::SearchResult::Tag::URL) {
363       NSColor* urlColor =
364           gfx::SkColorToSRGBNSColor(app_list::kResultURLTextColor);
365       [text addAttribute:NSForegroundColorAttributeName
366                    value:urlColor
367                    range:it->range.ToNSRange()];
368     }
369   }
370
371   return text.autorelease();
372 }
373
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];
387   }
388   return titleText;
389 }
390
391 - (id)copyWithZone:(NSZone*)zone {
392   return [self retain];
393 }
394
395 @end
396
397 @implementation AppsSearchResultsTableView
398
399 - (AppsSearchResultsController*)controller {
400   return base::mac::ObjCCastStrict<AppsSearchResultsController>(
401       [self delegate]);
402 }
403
404 - (void)mouseDown:(NSEvent*)theEvent {
405   [[self controller] mouseDown:theEvent];
406   [super mouseDown:theEvent];
407 }
408
409 - (NSMenu*)menuForEvent:(NSEvent*)theEvent {
410   NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
411                                   fromView:nil];
412   return [[self controller] contextMenuForRow:[self rowAtPoint:pointInView]];
413 }
414
415 @end
416
417 @implementation AppsSearchResultsCell
418
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];
424     else
425       [gfx::SkColorToSRGBNSColor(app_list::kHighlightedColor) set];
426
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);
432   }
433
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);
442
443   [titleText drawInRect:titleRect];
444
445   NSImage* resultIcon = [[self objectValue] resultIcon];
446   if (!resultIcon)
447     return;
448
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
456                 fromRect:NSZeroRect
457                operation:NSCompositeSourceOver
458                 fraction:1.0
459           respectFlipped:YES
460                    hints:nil];
461 }
462
463 @end