apps_search_box_controller.mm revision 3551c9c881056c480085172ff9840cab31610854
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.
4
5#import "ui/app_list/cocoa/apps_search_box_controller.h"
6
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/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
12#include "ui/app_list/app_list_menu.h"
13#import "ui/app_list/cocoa/current_user_menu_item_view.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"
21
22namespace {
23
24// Padding either side of the search icon and menu button.
25const CGFloat kPadding = 14;
26
27// Size of the search icon.
28const CGFloat kSearchIconDimension = 32;
29
30// Size of the menu button on the right.
31const CGFloat kMenuButtonDimension = 29;
32
33// Menu offset relative to the bottom-right corner of the menu button.
34const CGFloat kMenuYOffsetFromButton = -4;
35const CGFloat kMenuXOffsetFromButton = -7;
36
37}
38
39@interface AppsSearchBoxController ()
40
41- (NSImageView*)searchImageView;
42- (void)addSubviews;
43
44@end
45
46namespace app_list {
47
48class SearchBoxModelObserverBridge : public SearchBoxModelObserver {
49 public:
50  SearchBoxModelObserverBridge(AppsSearchBoxController* parent);
51  virtual ~SearchBoxModelObserverBridge();
52
53  void SetSearchText(const base::string16& text);
54
55  virtual void IconChanged() OVERRIDE;
56  virtual void HintTextChanged() OVERRIDE;
57  virtual void SelectionModelChanged() OVERRIDE;
58  virtual void TextChanged() OVERRIDE;
59
60 private:
61  SearchBoxModel* GetModel();
62
63  AppsSearchBoxController* parent_;  // Weak. Owns us.
64
65  DISALLOW_COPY_AND_ASSIGN(SearchBoxModelObserverBridge);
66};
67
68SearchBoxModelObserverBridge::SearchBoxModelObserverBridge(
69    AppsSearchBoxController* parent)
70    : parent_(parent) {
71  IconChanged();
72  HintTextChanged();
73  GetModel()->AddObserver(this);
74}
75
76SearchBoxModelObserverBridge::~SearchBoxModelObserverBridge() {
77  GetModel()->RemoveObserver(this);
78}
79
80SearchBoxModel* SearchBoxModelObserverBridge::GetModel() {
81  SearchBoxModel* searchBoxModel = [[parent_ delegate] searchBoxModel];
82  DCHECK(searchBoxModel);
83  return searchBoxModel;
84}
85
86void SearchBoxModelObserverBridge::SetSearchText(const base::string16& text) {
87  SearchBoxModel* model = GetModel();
88  model->RemoveObserver(this);
89  model->SetText(text);
90  // TODO(tapted): See if this should call SetSelectionModel here.
91  model->AddObserver(this);
92}
93
94void SearchBoxModelObserverBridge::IconChanged() {
95  [[parent_ searchImageView] setImage:gfx::NSImageFromImageSkiaWithColorSpace(
96      GetModel()->icon(), base::mac::GetSRGBColorSpace())];
97}
98
99void SearchBoxModelObserverBridge::HintTextChanged() {
100  [[[parent_ searchTextField] cell] setPlaceholderString:
101      base::SysUTF16ToNSString(GetModel()->hint_text())];
102}
103
104void SearchBoxModelObserverBridge::SelectionModelChanged() {
105  // TODO(tapted): See if anything needs to be done here for RTL.
106}
107
108void SearchBoxModelObserverBridge::TextChanged() {
109  // Currently the model text is only changed when we are not observing it, or
110  // it is changed in tests to establish a particular state.
111  [[parent_ searchTextField]
112      setStringValue:base::SysUTF16ToNSString(GetModel()->text())];
113}
114
115}  // namespace app_list
116
117@interface SearchTextField : NSTextField {
118 @private
119  NSRect textFrameInset_;
120}
121
122@property(readonly, nonatomic) NSRect textFrameInset;
123
124- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin
125                     rightMargin:(CGFloat)rightMargin;
126
127@end
128
129@interface AppListMenuController : MenuController {
130 @private
131  AppsSearchBoxController* searchBoxController_;  // Weak. Owns us.
132}
133
134- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent;
135
136@end
137
138@implementation AppsSearchBoxController
139
140@synthesize delegate = delegate_;
141
142- (id)initWithFrame:(NSRect)frame {
143  if ((self = [super init])) {
144    base::scoped_nsobject<NSView> containerView(
145        [[NSView alloc] initWithFrame:frame]);
146    [self setView:containerView];
147    [self addSubviews];
148  }
149  return self;
150}
151
152- (void)clearSearch {
153  [searchTextField_ setStringValue:@""];
154  [self controlTextDidChange:nil];
155}
156
157- (void)rebuildMenu {
158  if (![delegate_ appListDelegate])
159    return;
160
161  menuController_.reset([[AppListMenuController alloc]
162      initWithSearchBoxController:self]);
163  [menuButton_ setMenu:[menuController_ menu]];  // Menu will populate here.
164}
165
166- (void)setDelegate:(id<AppsSearchBoxDelegate>)delegate {
167  [[menuButton_ menu] removeAllItems];
168  menuController_.reset();
169  appListMenu_.reset();
170  bridge_.reset();  // Ensure observers are cleared before updating |delegate_|.
171  delegate_ = delegate;
172  if (!delegate_)
173    return;
174
175  bridge_.reset(new app_list::SearchBoxModelObserverBridge(self));
176  if (![delegate_ appListDelegate])
177    return;
178
179  appListMenu_.reset(new app_list::AppListMenu([delegate_ appListDelegate]));
180  [self rebuildMenu];
181}
182
183- (NSTextField*)searchTextField {
184  return searchTextField_;
185}
186
187- (NSPopUpButton*)menuControl {
188  return menuButton_;
189}
190
191- (app_list::AppListMenu*)appListMenu {
192  return appListMenu_.get();
193}
194
195- (NSImageView*)searchImageView {
196  return searchImageView_;
197}
198
199- (void)addSubviews {
200  NSRect viewBounds = [[self view] bounds];
201  searchImageView_.reset([[NSImageView alloc] initWithFrame:NSMakeRect(
202      kPadding, 0, kSearchIconDimension, NSHeight(viewBounds))]);
203
204  searchTextField_.reset([[SearchTextField alloc] initWithFrame:viewBounds]);
205  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
206  [searchTextField_ setDelegate:self];
207  [searchTextField_ setFont:rb.GetFont(
208      ui::ResourceBundle::MediumFont).GetNativeFont()];
209  [searchTextField_
210      setMarginsWithLeftMargin:NSMaxX([searchImageView_ frame]) + kPadding
211                   rightMargin:kMenuButtonDimension + 2 * kPadding];
212
213  // Add the drop-down menu, with a custom button.
214  NSRect buttonFrame = NSMakeRect(
215      NSWidth(viewBounds) - kMenuButtonDimension - kPadding,
216      floor(NSMidY(viewBounds) - kMenuButtonDimension / 2),
217      kMenuButtonDimension,
218      kMenuButtonDimension);
219  menuButton_.reset([[HoverImageMenuButton alloc] initWithFrame:buttonFrame
220                                                      pullsDown:YES]);
221  [[menuButton_ hoverImageMenuButtonCell] setDefaultImage:
222      rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_NORMAL).AsNSImage()];
223  [[menuButton_ hoverImageMenuButtonCell] setAlternateImage:
224      rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_PRESSED).AsNSImage()];
225  [[menuButton_ hoverImageMenuButtonCell] setHoverImage:
226      rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_HOVER).AsNSImage()];
227
228  [[self view] addSubview:searchImageView_];
229  [[self view] addSubview:searchTextField_];
230  [[self view] addSubview:menuButton_];
231}
232
233- (BOOL)control:(NSControl*)control
234               textView:(NSTextView*)textView
235    doCommandBySelector:(SEL)command {
236  // Forward the message first, to handle grid or search results navigation.
237  BOOL handled = [delegate_ control:control
238                           textView:textView
239                doCommandBySelector:command];
240  if (handled)
241    return YES;
242
243  // If the delegate did not handle the escape key, it means the window was not
244  // dismissed because there were search results. Clear them.
245  if (command == @selector(complete:)) {
246    [self clearSearch];
247    return YES;
248  }
249
250  return NO;
251}
252
253- (void)controlTextDidChange:(NSNotification*)notification {
254  if (bridge_) {
255    bridge_->SetSearchText(
256        base::SysNSStringToUTF16([searchTextField_ stringValue]));
257  }
258
259  [delegate_ modelTextDidChange];
260}
261
262@end
263
264@interface SearchTextFieldCell : NSTextFieldCell;
265
266- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame;
267
268@end
269
270@implementation SearchTextField
271
272@synthesize textFrameInset = textFrameInset_;
273
274+ (Class)cellClass {
275  return [SearchTextFieldCell class];
276}
277
278- (id)initWithFrame:(NSRect)theFrame {
279  if ((self = [super initWithFrame:theFrame])) {
280    [self setFocusRingType:NSFocusRingTypeNone];
281    [self setDrawsBackground:NO];
282    [self setBordered:NO];
283  }
284  return self;
285}
286
287- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin
288                     rightMargin:(CGFloat)rightMargin {
289  // Find the preferred height for the current text properties, and center.
290  NSRect viewBounds = [self bounds];
291  [self sizeToFit];
292  NSRect textBounds = [self bounds];
293  textFrameInset_.origin.x = leftMargin;
294  textFrameInset_.origin.y = floor(NSMidY(viewBounds) - NSMidY(textBounds));
295  textFrameInset_.size.width = leftMargin + rightMargin;
296  textFrameInset_.size.height = NSHeight(viewBounds) - NSHeight(textBounds);
297  [self setFrame:viewBounds];
298}
299
300@end
301
302@implementation SearchTextFieldCell
303
304- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame {
305  SearchTextField* searchTextField =
306      base::mac::ObjCCastStrict<SearchTextField>([self controlView]);
307  NSRect insetRect = [searchTextField textFrameInset];
308  cellFrame.origin.x += insetRect.origin.x;
309  cellFrame.origin.y += insetRect.origin.y;
310  cellFrame.size.width -= insetRect.size.width;
311  cellFrame.size.height -= insetRect.size.height;
312  return cellFrame;
313}
314
315- (NSRect)textFrameForFrame:(NSRect)cellFrame {
316  return [self textFrameForFrameInternal:cellFrame];
317}
318
319- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
320  return [self textFrameForFrameInternal:cellFrame];
321}
322
323- (void)resetCursorRect:(NSRect)cellFrame
324                 inView:(NSView*)controlView {
325  [super resetCursorRect:[self textCursorFrameForFrame:cellFrame]
326                  inView:controlView];
327}
328
329- (NSRect)drawingRectForBounds:(NSRect)theRect {
330  return [super drawingRectForBounds:[self textFrameForFrame:theRect]];
331}
332
333- (void)editWithFrame:(NSRect)cellFrame
334               inView:(NSView*)controlView
335               editor:(NSText*)editor
336             delegate:(id)delegate
337                event:(NSEvent*)event {
338  [super editWithFrame:[self textFrameForFrame:cellFrame]
339                inView:controlView
340                editor:editor
341              delegate:delegate
342                 event:event];
343}
344
345- (void)selectWithFrame:(NSRect)cellFrame
346                 inView:(NSView*)controlView
347                 editor:(NSText*)editor
348               delegate:(id)delegate
349                  start:(NSInteger)start
350                 length:(NSInteger)length {
351  [super selectWithFrame:[self textFrameForFrame:cellFrame]
352                  inView:controlView
353                  editor:editor
354                delegate:delegate
355                   start:start
356                  length:length];
357}
358
359@end
360
361@implementation AppListMenuController
362
363- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent {
364  // Need to initialze super with a NULL model, otherwise it will immediately
365  // try to populate, which can't be done until setting the parent.
366  if ((self = [super initWithModel:NULL
367            useWithPopUpButtonCell:YES])) {
368    searchBoxController_ = parent;
369    [super setModel:[parent appListMenu]->menu_model()];
370  }
371  return self;
372}
373
374- (void)addItemToMenu:(NSMenu*)menu
375              atIndex:(NSInteger)index
376            fromModel:(ui::MenuModel*)model {
377  [super addItemToMenu:menu
378               atIndex:index
379             fromModel:model];
380  if (model->GetCommandIdAt(index) != app_list::AppListMenu::CURRENT_USER)
381    return;
382
383  base::scoped_nsobject<NSView> customItemView([[CurrentUserMenuItemView alloc]
384      initWithCurrentUser:[[searchBoxController_ delegate] currentUserName]
385                userEmail:[[searchBoxController_ delegate] currentUserEmail]]);
386  [[menu itemAtIndex:index] setView:customItemView];
387}
388
389- (NSRect)confinementRectForMenu:(NSMenu*)menu
390                        onScreen:(NSScreen*)screen {
391  NSPopUpButton* menuButton = [searchBoxController_ menuControl];
392  // Ensure the menu comes up below the menu button by trimming the window frame
393  // to a point anchored below the bottom right of the button.
394  NSRect anchorRect = [menuButton convertRect:[menuButton bounds]
395                                       toView:nil];
396  NSPoint anchorPoint = [[menuButton window] convertBaseToScreen:NSMakePoint(
397      NSMaxX(anchorRect) + kMenuXOffsetFromButton,
398      NSMinY(anchorRect) - kMenuYOffsetFromButton)];
399  NSRect confinementRect = [[menuButton window] frame];
400  confinementRect.size = NSMakeSize(anchorPoint.x - NSMinX(confinementRect),
401                                    anchorPoint.y - NSMinY(confinementRect));
402  return confinementRect;
403}
404
405@end
406