// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_matrix.h" #include "base/logging.h" #include "base/mac/foundation_util.h" #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_cell.h" #include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h" #include "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h" #include "components/omnibox/browser/autocomplete_result.h" namespace { // NSEvent -buttonNumber for middle mouse button. const NSInteger kMiddleButtonNumber = 2; } // namespace @interface OmniboxPopupMatrix () - (OmniboxPopupTableController*)controller; - (void)resetTrackingArea; - (void)highlightRowUnder:(NSEvent*)theEvent; - (BOOL)selectCellForEvent:(NSEvent*)theEvent; @end @implementation OmniboxPopupTableController - (instancetype)initWithMatchResults:(const AutocompleteResult&)result tableView:(OmniboxPopupMatrix*)tableView popupView:(const OmniboxPopupViewMac&)popupView answerImage:(NSImage*)answerImage { base::scoped_nsobject array([[NSMutableArray alloc] init]); CGFloat max_match_contents_width = 0.0f; CGFloat contentsOffset = -1.0f; for (const AutocompleteMatch& match : result) { if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL && contentsOffset < 0.0f) contentsOffset = [OmniboxPopupCell computeContentsOffset:match]; base::scoped_nsobject cellData( [[OmniboxPopupCellData alloc] initWithMatch:match contentsOffset:contentsOffset image:popupView.ImageForMatch(match) answerImage:(match.answer ? answerImage : nil)]); [array addObject:cellData]; if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) { max_match_contents_width = std::max(max_match_contents_width, [cellData getMatchContentsWidth]); } } [tableView setMaxMatchContentsWidth:max_match_contents_width]; return [self initWithArray:array]; } - (instancetype)initWithArray:(NSArray*)array { if ((self = [super init])) { hoveredIndex_ = -1; array_.reset([array copy]); } return self; } - (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView { return [array_ count]; } - (id)tableView:(NSTableView*)tableView objectValueForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex { return [array_ objectAtIndex:rowIndex]; } - (void)tableView:(NSTableView*)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex { NOTREACHED(); } - (void)tableView:(NSTableView*)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex { OmniboxPopupCell* popupCell = base::mac::ObjCCastStrict(cell); [popupCell setState:([tableView selectedRow] == rowIndex) ? NSOnState : NSOffState]; [popupCell highlight:(hoveredIndex_ == rowIndex) withFrame:[tableView bounds] inView:tableView]; } - (NSInteger)highlightedRow { return hoveredIndex_; } - (void)setHighlightedRow:(NSInteger)rowIndex { hoveredIndex_ = rowIndex; } - (CGFloat)tableView:(NSTableView*)tableView heightOfRow:(NSInteger)row { CGFloat height = kContentLineHeight; if ([[array_ objectAtIndex:row] isAnswer]) { OmniboxPopupMatrix* matrix = base::mac::ObjCCastStrict(tableView); height += [matrix answerLineHeight]; } return height; } @end @implementation OmniboxPopupMatrix @synthesize separator = separator_; @synthesize maxMatchContentsWidth = maxMatchContentsWidth_; @synthesize answerLineHeight = answerLineHeight_; - (instancetype)initWithObserver:(OmniboxPopupMatrixObserver*)observer { if ((self = [super initWithFrame:NSZeroRect])) { observer_ = observer; base::scoped_nsobject column( [[NSTableColumn alloc] initWithIdentifier:@"MainColumn"]); [column setDataCell:[[[OmniboxPopupCell alloc] init] autorelease]]; [self addTableColumn:column]; // Cells pack with no spacing. [self setIntercellSpacing:NSMakeSize(0.0, 0.0)]; [self setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone]; [self setBackgroundColor:[NSColor controlBackgroundColor]]; [self setAllowsEmptySelection:YES]; [self deselectAll:self]; [self resetTrackingArea]; base::scoped_nsobject layoutManager( [[NSLayoutManager alloc] init]); answerLineHeight_ = [layoutManager defaultLineHeightForFont:OmniboxViewMac::GetLargeFont( gfx::Font::NORMAL)]; } return self; } - (OmniboxPopupTableController*)controller { return base::mac::ObjCCastStrict( [self delegate]); } - (void)setObserver:(OmniboxPopupMatrixObserver*)observer { observer_ = observer; } - (void)updateTrackingAreas { [self resetTrackingArea]; [super updateTrackingAreas]; } // Callbacks from tracking area. - (void)mouseMoved:(NSEvent*)theEvent { [self highlightRowUnder:theEvent]; } - (void)mouseExited:(NSEvent*)theEvent { [self highlightRowUnder:theEvent]; } // The tracking area events aren't forwarded during a drag, so handle // highlighting manually for middle-click and middle-drag. - (void)otherMouseDown:(NSEvent*)theEvent { if ([theEvent buttonNumber] == kMiddleButtonNumber) { [self highlightRowUnder:theEvent]; } [super otherMouseDown:theEvent]; } - (void)otherMouseDragged:(NSEvent*)theEvent { if ([theEvent buttonNumber] == kMiddleButtonNumber) { [self highlightRowUnder:theEvent]; } [super otherMouseDragged:theEvent]; } - (void)otherMouseUp:(NSEvent*)theEvent { // Only intercept middle button. if ([theEvent buttonNumber] != kMiddleButtonNumber) { [super otherMouseUp:theEvent]; return; } // -otherMouseDragged: should always have been called at this location, but // make sure the user is getting the right feedback. [self highlightRowUnder:theEvent]; const NSInteger highlightedRow = [[self controller] highlightedRow]; if (highlightedRow != -1) { DCHECK(observer_); observer_->OnMatrixRowMiddleClicked(self, highlightedRow); } } // Track the mouse until released, keeping the cell under the mouse selected. // If the mouse wanders off-view, revert to the originally-selected cell. If // the mouse is released over a cell, call the delegate to open the row's URL. - (void)mouseDown:(NSEvent*)theEvent { NSCell* selectedCell = [self selectedCell]; // Clear any existing highlight. [[self controller] setHighlightedRow:-1]; do { if (![self selectCellForEvent:theEvent]) { [self selectCell:selectedCell]; } const NSUInteger mask = NSLeftMouseUpMask | NSLeftMouseDraggedMask; theEvent = [[self window] nextEventMatchingMask:mask]; } while ([theEvent type] == NSLeftMouseDragged); // Do not message the delegate if released outside view. if (![self selectCellForEvent:theEvent]) { [self selectCell:selectedCell]; } else { const NSInteger selectedRow = [self selectedRow]; // No row could be selected if the model failed to update. if (selectedRow == -1) { NOTREACHED(); return; } DCHECK(observer_); observer_->OnMatrixRowClicked(self, selectedRow); } } - (void)selectRowIndex:(NSInteger)rowIndex { NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:rowIndex]; [self selectRowIndexes:indexSet byExtendingSelection:NO]; } - (NSInteger)highlightedRow { return [[self controller] highlightedRow]; } - (void)setController:(OmniboxPopupTableController*)controller { matrixController_.reset([controller retain]); [self setDelegate:controller]; [self setDataSource:controller]; [self reloadData]; } - (void)resetTrackingArea { if (trackingArea_.get()) [self removeTrackingArea:trackingArea_.get()]; trackingArea_.reset([[CrTrackingArea alloc] initWithRect:[self frame] options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInActiveApp | NSTrackingInVisibleRect owner:self userInfo:nil]); [self addTrackingArea:trackingArea_.get()]; } - (void)highlightRowUnder:(NSEvent*)theEvent { NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSInteger oldRow = [[self controller] highlightedRow]; NSInteger newRow = [self rowAtPoint:point]; if (oldRow != newRow) { [[self controller] setHighlightedRow:newRow]; [self setNeedsDisplayInRect:[self rectOfRow:oldRow]]; [self setNeedsDisplayInRect:[self rectOfRow:newRow]]; } } - (BOOL)selectCellForEvent:(NSEvent*)theEvent { NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSInteger row = [self rowAtPoint:point]; [self selectRowIndex:row]; if (row != -1) { DCHECK(observer_); observer_->OnMatrixRowSelected(self, row); return YES; } return NO; } @end