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/search/search.h"
12 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
13 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_cell.h"
14 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_separator_view.h"
15 #include "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h"
16 #include "components/omnibox/browser/autocomplete_match.h"
17 #include "components/omnibox/browser/autocomplete_match_type.h"
18 #include "components/omnibox/browser/omnibox_edit_model.h"
19 #include "components/omnibox/browser/omnibox_popup_model.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/gfx/geometry/rect.h"
27 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
28 #include "ui/gfx/text_elider.h"
32 // Padding between matrix and the top and bottom of the popup window.
33 const CGFloat kPopupPaddingVertical = 5.0;
35 // Animation duration when animating the popup window smaller.
36 const NSTimeInterval kShrinkAnimationDuration = 0.1;
38 // Background colors for different states of the popup elements.
39 NSColor* BackgroundColor() {
40 return [NSColor controlBackgroundColor];
45 OmniboxPopupViewMac::OmniboxPopupViewMac(OmniboxView* omnibox_view,
46 OmniboxEditModel* edit_model,
48 : omnibox_view_(omnibox_view),
49 model_(new OmniboxPopupModel(this, edit_model)),
52 target_popup_frame_(NSZeroRect) {
57 OmniboxPopupViewMac::~OmniboxPopupViewMac() {
58 // Destroy the popup model before this object is destroyed, because
59 // it can call back to us in the destructor.
62 // Break references to |this| because the popup may not be
63 // deallocated immediately.
64 [matrix_ setObserver:NULL];
67 bool OmniboxPopupViewMac::IsOpen() const {
71 void OmniboxPopupViewMac::UpdatePopupAppearance() {
72 DCHECK([NSThread isMainThread]);
73 const AutocompleteResult& result = GetResult();
74 const size_t rows = result.size();
76 [[popup_ parentWindow] removeChildWindow:popup_];
77 [popup_ orderOut:nil];
79 // Break references to |this| because the popup may not be
80 // deallocated immediately.
81 [matrix_ setObserver:NULL];
86 target_popup_frame_ = NSZeroRect;
91 CreatePopupIfNeeded();
93 NSImage* answerImage = nil;
94 if (!model_->answer_bitmap().isNull()) {
96 gfx::Image::CreateFrom1xBitmap(model_->answer_bitmap()).CopyNSImage();
98 [matrix_ setController:[[OmniboxPopupTableController alloc]
99 initWithMatchResults:result
102 answerImage:answerImage]];
103 [matrix_ setSeparator:[OmniboxPopupCell createSeparatorString]];
105 // Update the selection before placing (and displaying) the window.
108 // Calculate the matrix size manually rather than using -sizeToCells
109 // because actually resizing the matrix messed up the popup size
111 DCHECK_EQ([matrix_ intercellSpacing].height, 0.0);
112 PositionPopup(NSHeight([matrix_ frame]));
115 gfx::Rect OmniboxPopupViewMac::GetTargetBounds() {
116 // Flip the coordinate system before returning.
117 NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
118 NSRect monitor_frame = [screen frame];
119 gfx::Rect bounds(NSRectToCGRect(target_popup_frame_));
120 bounds.set_y(monitor_frame.size.height - bounds.y() - bounds.height());
124 // This is only called by model in SetSelectedLine() after updating
125 // everything. Popup should already be visible.
126 void OmniboxPopupViewMac::PaintUpdatesNow() {
127 [matrix_ selectRowIndex:model_->selected_line()];
130 void OmniboxPopupViewMac::OnMatrixRowSelected(OmniboxPopupMatrix* matrix,
132 model_->SetSelectedLine(row, false, false);
135 void OmniboxPopupViewMac::OnMatrixRowClicked(OmniboxPopupMatrix* matrix,
138 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]));
141 void OmniboxPopupViewMac::OnMatrixRowMiddleClicked(OmniboxPopupMatrix* matrix,
143 OpenURLForRow(row, NEW_BACKGROUND_TAB);
146 const AutocompleteResult& OmniboxPopupViewMac::GetResult() const {
147 return model_->result();
150 void OmniboxPopupViewMac::CreatePopupIfNeeded() {
153 [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
154 styleMask:NSBorderlessWindowMask
155 backing:NSBackingStoreBuffered
157 [popup_ setBackgroundColor:[NSColor clearColor]];
158 [popup_ setOpaque:NO];
160 // Use a flipped view to pin the matrix top the top left. This is needed
161 // for animated resize.
162 base::scoped_nsobject<FlippedView> contentView(
163 [[FlippedView alloc] initWithFrame:NSZeroRect]);
164 [popup_ setContentView:contentView];
166 // View to draw a background beneath the matrix.
167 background_view_.reset([[NSBox alloc] initWithFrame:NSZeroRect]);
168 [background_view_ setBoxType:NSBoxCustom];
169 [background_view_ setBorderType:NSNoBorder];
170 [background_view_ setFillColor:BackgroundColor()];
171 [background_view_ setContentViewMargins:NSZeroSize];
172 [contentView addSubview:background_view_];
174 matrix_.reset([[OmniboxPopupMatrix alloc] initWithObserver:this]);
175 [background_view_ addSubview:matrix_];
177 top_separator_view_.reset(
178 [[OmniboxPopupTopSeparatorView alloc] initWithFrame:NSZeroRect]);
179 [contentView addSubview:top_separator_view_];
181 bottom_separator_view_.reset(
182 [[OmniboxPopupBottomSeparatorView alloc] initWithFrame:NSZeroRect]);
183 [contentView addSubview:bottom_separator_view_];
185 // TODO(dtseng): Ignore until we provide NSAccessibility support.
186 [popup_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
187 forAttribute:NSAccessibilityRoleAttribute];
191 void OmniboxPopupViewMac::PositionPopup(const CGFloat matrixHeight) {
192 BrowserWindowController* controller =
193 [BrowserWindowController browserWindowControllerForView:field_];
194 NSRect anchor_rect_base = [controller omniboxPopupAnchorRect];
196 // Calculate the popup's position on the screen.
197 NSRect popup_frame = anchor_rect_base;
198 // Size to fit the matrix and shift down by the size.
199 popup_frame.size.height = matrixHeight + kPopupPaddingVertical * 2.0;
200 popup_frame.size.height += [OmniboxPopupTopSeparatorView preferredHeight];
201 popup_frame.size.height += [OmniboxPopupBottomSeparatorView preferredHeight];
202 popup_frame.origin.y -= NSHeight(popup_frame);
203 // Shift to screen coordinates.
205 [[controller window] convertBaseToScreen:popup_frame.origin];
208 NSRect top_separator_frame = NSZeroRect;
209 top_separator_frame.size.width = NSWidth(popup_frame);
210 top_separator_frame.size.height =
211 [OmniboxPopupTopSeparatorView preferredHeight];
212 [top_separator_view_ setFrame:top_separator_frame];
215 NSRect bottom_separator_frame = NSZeroRect;
216 bottom_separator_frame.size.width = NSWidth(popup_frame);
217 bottom_separator_frame.size.height =
218 [OmniboxPopupBottomSeparatorView preferredHeight];
219 bottom_separator_frame.origin.y =
220 NSHeight(popup_frame) - NSHeight(bottom_separator_frame);
221 [bottom_separator_view_ setFrame:bottom_separator_frame];
224 NSRect background_rect = NSZeroRect;
225 background_rect.size.width = NSWidth(popup_frame);
226 background_rect.size.height = NSHeight(popup_frame) -
227 NSHeight(top_separator_frame) - NSHeight(bottom_separator_frame);
228 background_rect.origin.y = NSMaxY(top_separator_frame);
229 [background_view_ setFrame:background_rect];
231 // Calculate the width of the table based on backing out the popup's border
232 // from the width of the field.
233 const CGFloat tableWidth = NSWidth([field_ bounds]);
234 DCHECK_GT(tableWidth, 0.0);
237 NSPoint field_origin_base =
238 [field_ convertPoint:[field_ bounds].origin toView:nil];
239 NSRect matrix_frame = NSZeroRect;
240 matrix_frame.origin.x = field_origin_base.x - NSMinX(anchor_rect_base);
241 matrix_frame.origin.y = kPopupPaddingVertical;
242 matrix_frame.size.width = tableWidth;
243 matrix_frame.size.height = matrixHeight;
244 [matrix_ setFrame:matrix_frame];
245 [[[matrix_ tableColumns] objectAtIndex:0] setWidth:tableWidth];
247 // Don't play animation games on first display.
248 target_popup_frame_ = popup_frame;
249 if (![popup_ parentWindow]) {
250 DCHECK(![popup_ isVisible]);
251 [popup_ setFrame:popup_frame display:NO];
252 [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove];
255 DCHECK([popup_ isVisible]);
257 // Animate the frame change if the only change is that the height got smaller.
258 // Otherwise, resize immediately.
259 NSRect current_popup_frame = [popup_ frame];
260 bool animate = (NSHeight(popup_frame) < NSHeight(current_popup_frame) &&
261 NSWidth(popup_frame) == NSWidth(current_popup_frame));
263 base::scoped_nsobject<NSDictionary> savedAnimations;
265 // In an ideal world, running a zero-length animation would cancel any
266 // running animations and set the new frame value immediately. In practice,
267 // zero-length animations are ignored entirely. Work around this AppKit bug
268 // by explicitly setting an NSNull animation for the "frame" key and then
269 // running the animation with a non-zero(!!) duration. This somehow
270 // convinces AppKit to do the right thing. Save off the current animations
271 // dictionary so it can be restored later.
272 savedAnimations.reset([[popup_ animations] copy]);
273 [popup_ setAnimations:@{@"frame" : [NSNull null]}];
276 [NSAnimationContext beginGrouping];
277 // Don't use the GTM addition for the "Steve" slowdown because this can happen
278 // async from user actions and the effects could be a surprise.
279 [[NSAnimationContext currentContext] setDuration:kShrinkAnimationDuration];
280 [[popup_ animator] setFrame:popup_frame display:YES];
281 [NSAnimationContext endGrouping];
284 // Restore the original animations dictionary. This does not reinstate any
285 // previously running animations.
286 [popup_ setAnimations:savedAnimations];
290 NSImage* OmniboxPopupViewMac::ImageForMatch(
291 const AutocompleteMatch& match) const {
292 gfx::Image image = model_->GetIconIfExtensionMatch(match);
293 if (!image.IsEmpty())
294 return image.AsNSImage();
296 const int resource_id = model_->IsStarredMatch(match) ?
297 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type);
298 return OmniboxViewMac::ImageForResource(resource_id);
301 void OmniboxPopupViewMac::OpenURLForRow(size_t row,
302 WindowOpenDisposition disposition) {
303 DCHECK_LT(row, GetResult().size());
304 omnibox_view_->OpenMatch(GetResult().match_at(row), disposition, GURL(),
305 base::string16(), row);