Views Omnibox: tolerate minor click-to-select-all dragging.
[chromium-blink-merge.git] / ui / app_list / cocoa / apps_search_box_controller.mm
blob93962823b46df7fc3aa85c75c2f36f3afe50d842
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.
5 #import "ui/app_list/cocoa/apps_search_box_controller.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/mac/mac_util.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "grit/ui_resources.h"
11 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
12 #include "ui/app_list/app_list_menu.h"
13 #include "ui/app_list/app_list_model.h"
14 #include "ui/app_list/search_box_model.h"
15 #include "ui/app_list/search_box_model_observer.h"
16 #import "ui/base/cocoa/controls/hover_image_menu_button.h"
17 #import "ui/base/cocoa/controls/hover_image_menu_button_cell.h"
18 #import "ui/base/cocoa/menu_controller.h"
19 #include "ui/base/resource/resource_bundle.h"
20 #include "ui/gfx/image/image_skia_util_mac.h"
22 namespace {
24 // Padding either side of the search icon and menu button.
25 const CGFloat kPadding = 14;
27 // Size of the search icon.
28 const CGFloat kSearchIconDimension = 32;
30 // Size of the menu button on the right.
31 const CGFloat kMenuButtonDimension = 29;
33 // Menu offset relative to the bottom-right corner of the menu button.
34 const CGFloat kMenuYOffsetFromButton = -4;
35 const CGFloat kMenuXOffsetFromButton = -7;
39 @interface AppsSearchBoxController ()
41 - (NSImageView*)searchImageView;
42 - (void)addSubviews;
44 @end
46 namespace app_list {
48 class SearchBoxModelObserverBridge : public SearchBoxModelObserver {
49  public:
50   SearchBoxModelObserverBridge(AppsSearchBoxController* parent);
51   virtual ~SearchBoxModelObserverBridge();
53   void SetSearchText(const base::string16& text);
55   virtual void IconChanged() OVERRIDE;
56   virtual void SpeechRecognitionButtonPropChanged() OVERRIDE;
57   virtual void HintTextChanged() OVERRIDE;
58   virtual void SelectionModelChanged() OVERRIDE;
59   virtual void TextChanged() OVERRIDE;
61  private:
62   SearchBoxModel* GetModel();
64   AppsSearchBoxController* parent_;  // Weak. Owns us.
66   DISALLOW_COPY_AND_ASSIGN(SearchBoxModelObserverBridge);
69 SearchBoxModelObserverBridge::SearchBoxModelObserverBridge(
70     AppsSearchBoxController* parent)
71     : parent_(parent) {
72   IconChanged();
73   HintTextChanged();
74   GetModel()->AddObserver(this);
77 SearchBoxModelObserverBridge::~SearchBoxModelObserverBridge() {
78   GetModel()->RemoveObserver(this);
81 SearchBoxModel* SearchBoxModelObserverBridge::GetModel() {
82   SearchBoxModel* searchBoxModel = [[parent_ delegate] searchBoxModel];
83   DCHECK(searchBoxModel);
84   return searchBoxModel;
87 void SearchBoxModelObserverBridge::SetSearchText(const base::string16& text) {
88   SearchBoxModel* model = GetModel();
89   model->RemoveObserver(this);
90   model->SetText(text);
91   // TODO(tapted): See if this should call SetSelectionModel here.
92   model->AddObserver(this);
95 void SearchBoxModelObserverBridge::IconChanged() {
96   [[parent_ searchImageView] setImage:gfx::NSImageFromImageSkiaWithColorSpace(
97       GetModel()->icon(), base::mac::GetSRGBColorSpace())];
100 void SearchBoxModelObserverBridge::SpeechRecognitionButtonPropChanged() {
101   // TODO(mukai): implement.
102   NOTIMPLEMENTED();
105 void SearchBoxModelObserverBridge::HintTextChanged() {
106   [[[parent_ searchTextField] cell] setPlaceholderString:
107       base::SysUTF16ToNSString(GetModel()->hint_text())];
110 void SearchBoxModelObserverBridge::SelectionModelChanged() {
111   // TODO(tapted): See if anything needs to be done here for RTL.
114 void SearchBoxModelObserverBridge::TextChanged() {
115   // Currently the model text is only changed when we are not observing it, or
116   // it is changed in tests to establish a particular state.
117   [[parent_ searchTextField]
118       setStringValue:base::SysUTF16ToNSString(GetModel()->text())];
119   [[parent_ delegate] modelTextDidChange];
122 }  // namespace app_list
124 @interface SearchTextField : NSTextField {
125  @private
126   NSRect textFrameInset_;
129 @property(readonly, nonatomic) NSRect textFrameInset;
131 - (void)setMarginsWithLeftMargin:(CGFloat)leftMargin
132                      rightMargin:(CGFloat)rightMargin;
134 @end
136 @interface AppListMenuController : MenuController {
137  @private
138   AppsSearchBoxController* searchBoxController_;  // Weak. Owns us.
141 - (id)initWithSearchBoxController:(AppsSearchBoxController*)parent;
143 @end
145 @implementation AppsSearchBoxController
147 @synthesize delegate = delegate_;
149 - (id)initWithFrame:(NSRect)frame {
150   if ((self = [super init])) {
151     base::scoped_nsobject<NSView> containerView(
152         [[NSView alloc] initWithFrame:frame]);
153     [self setView:containerView];
154     [self addSubviews];
155   }
156   return self;
159 - (void)clearSearch {
160   [searchTextField_ setStringValue:@""];
161   [self controlTextDidChange:nil];
164 - (void)rebuildMenu {
165   if (![delegate_ appListDelegate])
166     return;
168   menuController_.reset();
169   appListMenu_.reset(
170       new app_list::AppListMenu([delegate_ appListDelegate]));
171   menuController_.reset([[AppListMenuController alloc]
172       initWithSearchBoxController:self]);
173   [menuButton_ setMenu:[menuController_ menu]];  // Menu will populate here.
176 - (void)setDelegate:(id<AppsSearchBoxDelegate>)delegate {
177   [[menuButton_ menu] removeAllItems];
178   menuController_.reset();
179   appListMenu_.reset();
180   bridge_.reset();  // Ensure observers are cleared before updating |delegate_|.
181   delegate_ = delegate;
182   if (!delegate_)
183     return;
185   bridge_.reset(new app_list::SearchBoxModelObserverBridge(self));
186   [self rebuildMenu];
189 - (NSTextField*)searchTextField {
190   return searchTextField_;
193 - (NSPopUpButton*)menuControl {
194   return menuButton_;
197 - (app_list::AppListMenu*)appListMenu {
198   return appListMenu_.get();
201 - (NSImageView*)searchImageView {
202   return searchImageView_;
205 - (void)addSubviews {
206   NSRect viewBounds = [[self view] bounds];
207   searchImageView_.reset([[NSImageView alloc] initWithFrame:NSMakeRect(
208       kPadding, 0, kSearchIconDimension, NSHeight(viewBounds))]);
210   searchTextField_.reset([[SearchTextField alloc] initWithFrame:viewBounds]);
211   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
212   [searchTextField_ setDelegate:self];
213   [searchTextField_ setFont:rb.GetFont(
214       ui::ResourceBundle::MediumFont).GetNativeFont()];
215   [searchTextField_
216       setMarginsWithLeftMargin:NSMaxX([searchImageView_ frame]) + kPadding
217                    rightMargin:kMenuButtonDimension + 2 * kPadding];
219   // Add the drop-down menu, with a custom button.
220   NSRect buttonFrame = NSMakeRect(
221       NSWidth(viewBounds) - kMenuButtonDimension - kPadding,
222       floor(NSMidY(viewBounds) - kMenuButtonDimension / 2),
223       kMenuButtonDimension,
224       kMenuButtonDimension);
225   menuButton_.reset([[HoverImageMenuButton alloc] initWithFrame:buttonFrame
226                                                       pullsDown:YES]);
227   [[menuButton_ hoverImageMenuButtonCell] setDefaultImage:
228       rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_NORMAL).AsNSImage()];
229   [[menuButton_ hoverImageMenuButtonCell] setAlternateImage:
230       rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_PRESSED).AsNSImage()];
231   [[menuButton_ hoverImageMenuButtonCell] setHoverImage:
232       rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_HOVER).AsNSImage()];
234   [[self view] addSubview:searchImageView_];
235   [[self view] addSubview:searchTextField_];
236   [[self view] addSubview:menuButton_];
239 - (BOOL)control:(NSControl*)control
240                textView:(NSTextView*)textView
241     doCommandBySelector:(SEL)command {
242   // Forward the message first, to handle grid or search results navigation.
243   BOOL handled = [delegate_ control:control
244                            textView:textView
245                 doCommandBySelector:command];
246   if (handled)
247     return YES;
249   // If the delegate did not handle the escape key, it means the window was not
250   // dismissed because there were search results. Clear them.
251   if (command == @selector(complete:)) {
252     [self clearSearch];
253     return YES;
254   }
256   return NO;
259 - (void)controlTextDidChange:(NSNotification*)notification {
260   if (bridge_) {
261     bridge_->SetSearchText(
262         base::SysNSStringToUTF16([searchTextField_ stringValue]));
263   }
265   [delegate_ modelTextDidChange];
268 @end
270 @interface SearchTextFieldCell : NSTextFieldCell;
272 - (NSRect)textFrameForFrameInternal:(NSRect)cellFrame;
274 @end
276 @implementation SearchTextField
278 @synthesize textFrameInset = textFrameInset_;
280 + (Class)cellClass {
281   return [SearchTextFieldCell class];
284 - (id)initWithFrame:(NSRect)theFrame {
285   if ((self = [super initWithFrame:theFrame])) {
286     [self setFocusRingType:NSFocusRingTypeNone];
287     [self setDrawsBackground:NO];
288     [self setBordered:NO];
289   }
290   return self;
293 - (void)setMarginsWithLeftMargin:(CGFloat)leftMargin
294                      rightMargin:(CGFloat)rightMargin {
295   // Find the preferred height for the current text properties, and center.
296   NSRect viewBounds = [self bounds];
297   [self sizeToFit];
298   NSRect textBounds = [self bounds];
299   textFrameInset_.origin.x = leftMargin;
300   textFrameInset_.origin.y = floor(NSMidY(viewBounds) - NSMidY(textBounds));
301   textFrameInset_.size.width = leftMargin + rightMargin;
302   textFrameInset_.size.height = NSHeight(viewBounds) - NSHeight(textBounds);
303   [self setFrame:viewBounds];
306 @end
308 @implementation SearchTextFieldCell
310 - (NSRect)textFrameForFrameInternal:(NSRect)cellFrame {
311   SearchTextField* searchTextField =
312       base::mac::ObjCCastStrict<SearchTextField>([self controlView]);
313   NSRect insetRect = [searchTextField textFrameInset];
314   cellFrame.origin.x += insetRect.origin.x;
315   cellFrame.origin.y += insetRect.origin.y;
316   cellFrame.size.width -= insetRect.size.width;
317   cellFrame.size.height -= insetRect.size.height;
318   return cellFrame;
321 - (NSRect)textFrameForFrame:(NSRect)cellFrame {
322   return [self textFrameForFrameInternal:cellFrame];
325 - (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
326   return [self textFrameForFrameInternal:cellFrame];
329 - (void)resetCursorRect:(NSRect)cellFrame
330                  inView:(NSView*)controlView {
331   [super resetCursorRect:[self textCursorFrameForFrame:cellFrame]
332                   inView:controlView];
335 - (NSRect)drawingRectForBounds:(NSRect)theRect {
336   return [super drawingRectForBounds:[self textFrameForFrame:theRect]];
339 - (void)editWithFrame:(NSRect)cellFrame
340                inView:(NSView*)controlView
341                editor:(NSText*)editor
342              delegate:(id)delegate
343                 event:(NSEvent*)event {
344   [super editWithFrame:[self textFrameForFrame:cellFrame]
345                 inView:controlView
346                 editor:editor
347               delegate:delegate
348                  event:event];
351 - (void)selectWithFrame:(NSRect)cellFrame
352                  inView:(NSView*)controlView
353                  editor:(NSText*)editor
354                delegate:(id)delegate
355                   start:(NSInteger)start
356                  length:(NSInteger)length {
357   [super selectWithFrame:[self textFrameForFrame:cellFrame]
358                   inView:controlView
359                   editor:editor
360                 delegate:delegate
361                    start:start
362                   length:length];
365 @end
367 @implementation AppListMenuController
369 - (id)initWithSearchBoxController:(AppsSearchBoxController*)parent {
370   // Need to initialze super with a NULL model, otherwise it will immediately
371   // try to populate, which can't be done until setting the parent.
372   if ((self = [super initWithModel:NULL
373             useWithPopUpButtonCell:YES])) {
374     searchBoxController_ = parent;
375     [super setModel:[parent appListMenu]->menu_model()];
376   }
377   return self;
380 - (NSRect)confinementRectForMenu:(NSMenu*)menu
381                         onScreen:(NSScreen*)screen {
382   NSPopUpButton* menuButton = [searchBoxController_ menuControl];
383   // Ensure the menu comes up below the menu button by trimming the window frame
384   // to a point anchored below the bottom right of the button.
385   NSRect anchorRect = [menuButton convertRect:[menuButton bounds]
386                                        toView:nil];
387   NSPoint anchorPoint = [[menuButton window] convertBaseToScreen:NSMakePoint(
388       NSMaxX(anchorRect) + kMenuXOffsetFromButton,
389       NSMinY(anchorRect) - kMenuYOffsetFromButton)];
390   NSRect confinementRect = [[menuButton window] frame];
391   confinementRect.size = NSMakeSize(anchorPoint.x - NSMinX(confinementRect),
392                                     anchorPoint.y - NSMinY(confinementRect));
393   return confinementRect;
396 @end