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