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#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
11#include "ui/app_list/app_list_menu.h"
12#include "ui/app_list/app_list_model.h"
13#include "ui/app_list/search_box_model.h"
14#include "ui/app_list/search_box_model_observer.h"
15#import "ui/base/cocoa/controls/hover_image_menu_button.h"
16#import "ui/base/cocoa/controls/hover_image_menu_button_cell.h"
17#import "ui/base/cocoa/menu_controller.h"
18#include "ui/base/resource/resource_bundle.h"
19#include "ui/gfx/image/image_skia_util_mac.h"
20#include "ui/resources/grit/ui_resources.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 SpeechRecognitionButtonPropChanged() OVERRIDE;
57  virtual void HintTextChanged() OVERRIDE;
58  virtual void SelectionModelChanged() OVERRIDE;
59  virtual void TextChanged() OVERRIDE;
60
61 private:
62  SearchBoxModel* GetModel();
63
64  AppsSearchBoxController* parent_;  // Weak. Owns us.
65
66  DISALLOW_COPY_AND_ASSIGN(SearchBoxModelObserverBridge);
67};
68
69SearchBoxModelObserverBridge::SearchBoxModelObserverBridge(
70    AppsSearchBoxController* parent)
71    : parent_(parent) {
72  IconChanged();
73  HintTextChanged();
74  GetModel()->AddObserver(this);
75}
76
77SearchBoxModelObserverBridge::~SearchBoxModelObserverBridge() {
78  GetModel()->RemoveObserver(this);
79}
80
81SearchBoxModel* SearchBoxModelObserverBridge::GetModel() {
82  SearchBoxModel* searchBoxModel = [[parent_ delegate] searchBoxModel];
83  DCHECK(searchBoxModel);
84  return searchBoxModel;
85}
86
87void 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);
93}
94
95void SearchBoxModelObserverBridge::IconChanged() {
96  [[parent_ searchImageView] setImage:gfx::NSImageFromImageSkiaWithColorSpace(
97      GetModel()->icon(), base::mac::GetSRGBColorSpace())];
98}
99
100void SearchBoxModelObserverBridge::SpeechRecognitionButtonPropChanged() {
101  // TODO(mukai): implement.
102  NOTIMPLEMENTED();
103}
104
105void SearchBoxModelObserverBridge::HintTextChanged() {
106  [[[parent_ searchTextField] cell] setPlaceholderString:
107      base::SysUTF16ToNSString(GetModel()->hint_text())];
108}
109
110void SearchBoxModelObserverBridge::SelectionModelChanged() {
111  // TODO(tapted): See if anything needs to be done here for RTL.
112}
113
114void 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];
120}
121
122}  // namespace app_list
123
124@interface SearchTextField : NSTextField {
125 @private
126  NSRect textFrameInset_;
127}
128
129@property(readonly, nonatomic) NSRect textFrameInset;
130
131- (void)setMarginsWithLeftMargin:(CGFloat)leftMargin
132                     rightMargin:(CGFloat)rightMargin;
133
134@end
135
136@interface AppListMenuController : MenuController {
137 @private
138  AppsSearchBoxController* searchBoxController_;  // Weak. Owns us.
139}
140
141- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent;
142
143@end
144
145@implementation AppsSearchBoxController
146
147@synthesize delegate = delegate_;
148
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;
157}
158
159- (void)clearSearch {
160  [searchTextField_ setStringValue:@""];
161  [self controlTextDidChange:nil];
162}
163
164- (void)rebuildMenu {
165  if (![delegate_ appListDelegate])
166    return;
167
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.
174}
175
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;
184
185  bridge_.reset(new app_list::SearchBoxModelObserverBridge(self));
186  [self rebuildMenu];
187}
188
189- (NSTextField*)searchTextField {
190  return searchTextField_;
191}
192
193- (NSPopUpButton*)menuControl {
194  return menuButton_;
195}
196
197- (app_list::AppListMenu*)appListMenu {
198  return appListMenu_.get();
199}
200
201- (NSImageView*)searchImageView {
202  return searchImageView_;
203}
204
205- (void)addSubviews {
206  NSRect viewBounds = [[self view] bounds];
207  searchImageView_.reset([[NSImageView alloc] initWithFrame:NSMakeRect(
208      kPadding, 0, kSearchIconDimension, NSHeight(viewBounds))]);
209
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];
218
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()];
233
234  [[self view] addSubview:searchImageView_];
235  [[self view] addSubview:searchTextField_];
236  [[self view] addSubview:menuButton_];
237}
238
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;
248
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  }
255
256  return NO;
257}
258
259- (void)controlTextDidChange:(NSNotification*)notification {
260  if (bridge_) {
261    bridge_->SetSearchText(
262        base::SysNSStringToUTF16([searchTextField_ stringValue]));
263  }
264
265  [delegate_ modelTextDidChange];
266}
267
268@end
269
270@interface SearchTextFieldCell : NSTextFieldCell;
271
272- (NSRect)textFrameForFrameInternal:(NSRect)cellFrame;
273
274@end
275
276@implementation SearchTextField
277
278@synthesize textFrameInset = textFrameInset_;
279
280+ (Class)cellClass {
281  return [SearchTextFieldCell class];
282}
283
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;
291}
292
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];
304}
305
306@end
307
308@implementation SearchTextFieldCell
309
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;
319}
320
321- (NSRect)textFrameForFrame:(NSRect)cellFrame {
322  return [self textFrameForFrameInternal:cellFrame];
323}
324
325- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
326  return [self textFrameForFrameInternal:cellFrame];
327}
328
329- (void)resetCursorRect:(NSRect)cellFrame
330                 inView:(NSView*)controlView {
331  [super resetCursorRect:[self textCursorFrameForFrame:cellFrame]
332                  inView:controlView];
333}
334
335- (NSRect)drawingRectForBounds:(NSRect)theRect {
336  return [super drawingRectForBounds:[self textFrameForFrame:theRect]];
337}
338
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];
349}
350
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];
363}
364
365@end
366
367@implementation AppListMenuController
368
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;
378}
379
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;
394}
395
396@end
397