1 // Copyright (c) 2012 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 #include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
9 #include "base/stl_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/autocomplete/autocomplete_match.h"
12 #include "chrome/browser/search/search.h"
13 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
14 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_cell.h"
15 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_separator_view.h"
16 #include "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h"
17 #include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
18 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
19 #include "chrome/common/autocomplete_match_type.h"
20 #include "grit/theme_resources.h"
21 #include "skia/ext/skia_utils_mac.h"
22 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
23 #import "ui/base/cocoa/cocoa_base_utils.h"
24 #import "ui/base/cocoa/flipped_view.h"
25 #include "ui/base/cocoa/window_size_constants.h"
26 #include "ui/base/resource/resource_bundle.h"
27 #include "ui/gfx/rect.h"
28 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
29 #include "ui/gfx/text_elider.h"
33 // How much to adjust the cell sizing up from the default determined
35 const CGFloat kCellHeightAdjust = 6.0;
37 // Padding between matrix and the top and bottom of the popup window.
38 const CGFloat kPopupPaddingVertical = 5.0;
40 // Animation duration when animating the popup window smaller.
41 const NSTimeInterval kShrinkAnimationDuration = 0.1;
43 // Background colors for different states of the popup elements.
44 NSColor* BackgroundColor() {
45 return [NSColor controlBackgroundColor];
50 OmniboxPopupViewMac::OmniboxPopupViewMac(OmniboxView* omnibox_view,
51 OmniboxEditModel* edit_model,
53 : omnibox_view_(omnibox_view),
54 model_(new OmniboxPopupModel(this, edit_model)),
57 target_popup_frame_(NSZeroRect) {
62 OmniboxPopupViewMac::~OmniboxPopupViewMac() {
63 // Destroy the popup model before this object is destroyed, because
64 // it can call back to us in the destructor.
67 // Break references to |this| because the popup may not be
68 // deallocated immediately.
69 [matrix_ setDelegate:NULL];
72 bool OmniboxPopupViewMac::IsOpen() const {
76 void OmniboxPopupViewMac::UpdatePopupAppearance() {
77 DCHECK([NSThread isMainThread]);
78 const AutocompleteResult& result = GetResult();
79 const size_t start_match = result.ShouldHideTopMatch() ? 1 : 0;
80 const size_t rows = result.size() - start_match;
82 [[popup_ parentWindow] removeChildWindow:popup_];
83 [popup_ orderOut:nil];
85 // Break references to |this| because the popup may not be
86 // deallocated immediately.
87 [matrix_ setDelegate:nil];
92 target_popup_frame_ = NSZeroRect;
97 CreatePopupIfNeeded();
99 // Calculate the width of the matrix based on backing out the popup's border
100 // from the width of the field.
101 const CGFloat matrix_width = NSWidth([field_ bounds]);
102 DCHECK_GT(matrix_width, 0.0);
104 // Load the results into the popup's matrix.
106 [matrix_ renewRows:rows columns:1];
107 CGFloat max_match_contents_width = 0.0f;
108 CGFloat contents_offset = -1.0f;
109 for (size_t ii = 0; ii < rows; ++ii) {
110 OmniboxPopupCell* cell = [matrix_ cellAtRow:ii column:0];
111 const AutocompleteMatch& match = GetResult().match_at(ii + start_match);
112 [cell setImage:ImageForMatch(match)];
113 [cell setMatch:match];
114 if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) {
115 max_match_contents_width = std::max(max_match_contents_width,
116 [cell getMatchContentsWidth]);
117 if (contents_offset < 0.0f) {
118 contents_offset = [OmniboxPopupCell computeContentsOffset:match];
120 [cell setContentsOffset:contents_offset];
124 for (size_t ii = 0; ii < rows; ++ii) {
125 OmniboxPopupCell* cell = [matrix_ cellAtRow:ii column:0];
126 [cell setMaxMatchContentsWidth:max_match_contents_width];
129 // Set the cell size to fit a line of text in the cell's font. All
130 // cells should use the same font and each should layout in one
131 // line, so they should all be about the same height.
132 const NSSize cell_size = [[matrix_ cellAtRow:0 column:0] cellSize];
133 DCHECK_GT(cell_size.height, 0.0);
134 const CGFloat cell_height = cell_size.height + kCellHeightAdjust;
135 [matrix_ setCellSize:NSMakeSize(matrix_width, cell_height)];
137 // Update the selection before placing (and displaying) the window.
140 // Calculate the matrix size manually rather than using -sizeToCells
141 // because actually resizing the matrix messed up the popup size
143 DCHECK_EQ([matrix_ intercellSpacing].height, 0.0);
144 PositionPopup(rows * cell_height);
147 gfx::Rect OmniboxPopupViewMac::GetTargetBounds() {
148 // Flip the coordinate system before returning.
149 NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
150 NSRect monitor_frame = [screen frame];
151 gfx::Rect bounds(NSRectToCGRect(target_popup_frame_));
152 bounds.set_y(monitor_frame.size.height - bounds.y() - bounds.height());
156 // This is only called by model in SetSelectedLine() after updating
157 // everything. Popup should already be visible.
158 void OmniboxPopupViewMac::PaintUpdatesNow() {
159 size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
160 if (start_match > model_->selected_line()) {
161 [matrix_ deselectAllCells];
163 [matrix_ selectCellAtRow:model_->selected_line() - start_match column:0];
168 void OmniboxPopupViewMac::OnMatrixRowSelected(OmniboxPopupMatrix* matrix,
170 size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
171 model_->SetSelectedLine(row + start_match, false, false);
174 void OmniboxPopupViewMac::OnMatrixRowClicked(OmniboxPopupMatrix* matrix,
177 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]));
180 void OmniboxPopupViewMac::OnMatrixRowMiddleClicked(OmniboxPopupMatrix* matrix,
182 OpenURLForRow(row, NEW_BACKGROUND_TAB);
185 const AutocompleteResult& OmniboxPopupViewMac::GetResult() const {
186 return model_->result();
189 void OmniboxPopupViewMac::CreatePopupIfNeeded() {
192 [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
193 styleMask:NSBorderlessWindowMask
194 backing:NSBackingStoreBuffered
196 [popup_ setBackgroundColor:[NSColor clearColor]];
197 [popup_ setOpaque:NO];
199 // Use a flipped view to pin the matrix top the top left. This is needed
200 // for animated resize.
201 base::scoped_nsobject<FlippedView> contentView(
202 [[FlippedView alloc] initWithFrame:NSZeroRect]);
203 [popup_ setContentView:contentView];
205 // View to draw a background beneath the matrix.
206 background_view_.reset([[NSBox alloc] initWithFrame:NSZeroRect]);
207 [background_view_ setBoxType:NSBoxCustom];
208 [background_view_ setBorderType:NSNoBorder];
209 [background_view_ setFillColor:BackgroundColor()];
210 [background_view_ setContentViewMargins:NSZeroSize];
211 [contentView addSubview:background_view_];
213 matrix_.reset([[OmniboxPopupMatrix alloc] initWithDelegate:this]);
214 [background_view_ addSubview:matrix_];
216 top_separator_view_.reset(
217 [[OmniboxPopupTopSeparatorView alloc] initWithFrame:NSZeroRect]);
218 [contentView addSubview:top_separator_view_];
220 bottom_separator_view_.reset(
221 [[OmniboxPopupBottomSeparatorView alloc] initWithFrame:NSZeroRect]);
222 [contentView addSubview:bottom_separator_view_];
224 // TODO(dtseng): Ignore until we provide NSAccessibility support.
225 [popup_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
226 forAttribute:NSAccessibilityRoleAttribute];
230 void OmniboxPopupViewMac::PositionPopup(const CGFloat matrixHeight) {
231 BrowserWindowController* controller =
232 [BrowserWindowController browserWindowControllerForView:field_];
233 NSRect anchor_rect_base = [controller omniboxPopupAnchorRect];
235 // Calculate the popup's position on the screen.
236 NSRect popup_frame = anchor_rect_base;
237 // Size to fit the matrix and shift down by the size.
238 popup_frame.size.height = matrixHeight + kPopupPaddingVertical * 2.0;
239 popup_frame.size.height += [OmniboxPopupTopSeparatorView preferredHeight];
240 popup_frame.size.height += [OmniboxPopupBottomSeparatorView preferredHeight];
241 popup_frame.origin.y -= NSHeight(popup_frame);
242 // Shift to screen coordinates.
244 [[controller window] convertBaseToScreen:popup_frame.origin];
246 // Do nothing if the popup is already animating to the given |frame|.
247 if (NSEqualRects(popup_frame, target_popup_frame_))
251 NSRect top_separator_frame = NSZeroRect;
252 top_separator_frame.size.width = NSWidth(popup_frame);
253 top_separator_frame.size.height =
254 [OmniboxPopupTopSeparatorView preferredHeight];
255 [top_separator_view_ setFrame:top_separator_frame];
258 NSRect bottom_separator_frame = NSZeroRect;
259 bottom_separator_frame.size.width = NSWidth(popup_frame);
260 bottom_separator_frame.size.height =
261 [OmniboxPopupBottomSeparatorView preferredHeight];
262 bottom_separator_frame.origin.y =
263 NSHeight(popup_frame) - NSHeight(bottom_separator_frame);
264 [bottom_separator_view_ setFrame:bottom_separator_frame];
267 NSRect background_rect = NSZeroRect;
268 background_rect.size.width = NSWidth(popup_frame);
269 background_rect.size.height = NSHeight(popup_frame) -
270 NSHeight(top_separator_frame) - NSHeight(bottom_separator_frame);
271 background_rect.origin.y = NSMaxY(top_separator_frame);
272 [background_view_ setFrame:background_rect];
275 NSPoint field_origin_base =
276 [field_ convertPoint:[field_ bounds].origin toView:nil];
277 NSRect matrix_frame = NSZeroRect;
278 matrix_frame.origin.x = field_origin_base.x - NSMinX(anchor_rect_base);
279 matrix_frame.origin.y = kPopupPaddingVertical;
280 matrix_frame.size.width = [matrix_ cellSize].width;
281 matrix_frame.size.height = matrixHeight;
282 [matrix_ setFrame:matrix_frame];
284 NSRect current_poup_frame = [popup_ frame];
285 target_popup_frame_ = popup_frame;
287 // Animate the frame change if the only change is that the height got smaller.
288 // Otherwise, resize immediately.
289 bool animate = (NSHeight(popup_frame) < NSHeight(current_poup_frame) &&
290 NSWidth(popup_frame) == NSWidth(current_poup_frame));
292 base::scoped_nsobject<NSDictionary> savedAnimations;
294 // In an ideal world, running a zero-length animation would cancel any
295 // running animations and set the new frame value immediately. In practice,
296 // zero-length animations are ignored entirely. Work around this AppKit bug
297 // by explicitly setting an NSNull animation for the "frame" key and then
298 // running the animation with a non-zero(!!) duration. This somehow
299 // convinces AppKit to do the right thing. Save off the current animations
300 // dictionary so it can be restored later.
301 savedAnimations.reset([[popup_ animations] copy]);
302 [popup_ setAnimations:@{@"frame" : [NSNull null]}];
305 [NSAnimationContext beginGrouping];
306 // Don't use the GTM additon for the "Steve" slowdown because this can happen
307 // async from user actions and the effects could be a surprise.
308 [[NSAnimationContext currentContext] setDuration:kShrinkAnimationDuration];
309 [[popup_ animator] setFrame:popup_frame display:YES];
310 [NSAnimationContext endGrouping];
313 // Restore the original animations dictionary. This does not reinstate any
314 // previously running animations.
315 [popup_ setAnimations:savedAnimations];
318 if (![popup_ isVisible])
319 [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove];
322 NSImage* OmniboxPopupViewMac::ImageForMatch(const AutocompleteMatch& match) {
323 gfx::Image image = model_->GetIconIfExtensionMatch(match);
324 if (!image.IsEmpty())
325 return image.AsNSImage();
327 const int resource_id = match.starred ?
328 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type);
329 return OmniboxViewMac::ImageForResource(resource_id);
332 void OmniboxPopupViewMac::OpenURLForRow(size_t row,
333 WindowOpenDisposition disposition) {
334 size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
336 DCHECK_LT(row, GetResult().size());
337 omnibox_view_->OpenMatch(GetResult().match_at(row), disposition, GURL(),
338 base::string16(), row);