BookmarkManager: Fix 'new folder text field size changes on clicking it' issue.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / omnibox / omnibox_popup_view_mac.mm
blob9786b9bb468bd6a5b148f44e1efd76f29c5ca859
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"
7 #include <cmath>
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"
30 namespace {
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];
43 }  // namespace
45 OmniboxPopupViewMac::OmniboxPopupViewMac(OmniboxView* omnibox_view,
46                                          OmniboxEditModel* edit_model,
47                                          NSTextField* field)
48     : omnibox_view_(omnibox_view),
49       model_(new OmniboxPopupModel(this, edit_model)),
50       field_(field),
51       popup_(nil),
52       target_popup_frame_(NSZeroRect) {
53   DCHECK(omnibox_view);
54   DCHECK(edit_model);
57 OmniboxPopupViewMac::~OmniboxPopupViewMac() {
58   // Destroy the popup model before this object is destroyed, because
59   // it can call back to us in the destructor.
60   model_.reset();
62   // Break references to |this| because the popup may not be
63   // deallocated immediately.
64   [matrix_ setObserver:NULL];
67 bool OmniboxPopupViewMac::IsOpen() const {
68   return popup_ != nil;
71 void OmniboxPopupViewMac::UpdatePopupAppearance() {
72   DCHECK([NSThread isMainThread]);
73   const AutocompleteResult& result = GetResult();
74   const size_t rows = result.size();
75   if (rows == 0) {
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];
82     matrix_.reset();
84     popup_.reset(nil);
86     target_popup_frame_ = NSZeroRect;
88     return;
89   }
91   CreatePopupIfNeeded();
93   NSImage* answerImage = nil;
94   if (!model_->answer_bitmap().isNull()) {
95     answerImage =
96         gfx::Image::CreateFrom1xBitmap(model_->answer_bitmap()).CopyNSImage();
97   }
98   [matrix_ setController:[[OmniboxPopupTableController alloc]
99                              initWithMatchResults:result
100                                         tableView:matrix_
101                                         popupView:*this
102                                       answerImage:answerImage]];
103   [matrix_ setSeparator:[OmniboxPopupCell createSeparatorString]];
105   // Update the selection before placing (and displaying) the window.
106   PaintUpdatesNow();
108   // Calculate the matrix size manually rather than using -sizeToCells
109   // because actually resizing the matrix messed up the popup size
110   // animation.
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());
121   return bounds;
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,
131                                               size_t row) {
132   model_->SetSelectedLine(row, false, false);
135 void OmniboxPopupViewMac::OnMatrixRowClicked(OmniboxPopupMatrix* matrix,
136                                              size_t row) {
137   OpenURLForRow(row,
138                 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]));
141 void OmniboxPopupViewMac::OnMatrixRowMiddleClicked(OmniboxPopupMatrix* matrix,
142                                                    size_t row) {
143   OpenURLForRow(row, NEW_BACKGROUND_TAB);
146 const AutocompleteResult& OmniboxPopupViewMac::GetResult() const {
147   return model_->result();
150 void OmniboxPopupViewMac::CreatePopupIfNeeded() {
151   if (!popup_) {
152     popup_.reset(
153         [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
154                                     styleMask:NSBorderlessWindowMask
155                                       backing:NSBackingStoreBuffered
156                                         defer:NO]);
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];
188   }
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.
204   popup_frame.origin =
205       [[controller window] convertBaseToScreen:popup_frame.origin];
207   // Top separator.
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];
214   // Bottom separator.
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];
223   // Background view.
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);
236   // Matrix.
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];
253     return;
254   }
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;
264   if (!animate) {
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]}];
274   }
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];
283   if (!animate) {
284     // Restore the original animations dictionary.  This does not reinstate any
285     // previously running animations.
286     [popup_ setAnimations:savedAnimations];
287   }
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);