1// Copyright (c) 2011 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 "chrome/browser/ui/cocoa/search_engine_dialog_controller.h"
6
7#include <algorithm>
8
9#include "base/mac/mac_util.h"
10#include "base/sys_string_conversions.h"
11#include "base/time.h"
12#include "chrome/browser/profiles/profile.h"
13#include "chrome/browser/search_engines/template_url.h"
14#include "chrome/browser/search_engines/template_url_model.h"
15#include "chrome/browser/search_engines/template_url_model_observer.h"
16#include "grit/generated_resources.h"
17#include "grit/theme_resources.h"
18#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
19#include "ui/base/l10n/l10n_util_mac.h"
20#include "ui/base/resource/resource_bundle.h"
21#include "ui/gfx/image.h"
22
23// Horizontal spacing between search engine choices.
24const int kSearchEngineSpacing = 20;
25
26// Vertical spacing between the search engine logo and the button underneath.
27const int kLogoButtonSpacing = 10;
28
29// Width of a label used in place of a logo.
30const int kLogoLabelWidth = 170;
31
32// Height of a label used in place of a logo.
33const int kLogoLabelHeight = 25;
34
35@interface SearchEngineDialogController (Private)
36- (void)onTemplateURLModelChanged;
37- (void)buildSearchEngineView;
38- (NSView*)viewForSearchEngine:(const TemplateURL*)engine
39                       atIndex:(size_t)index;
40- (IBAction)searchEngineSelected:(id)sender;
41@end
42
43class SearchEngineDialogControllerBridge :
44    public base::RefCounted<SearchEngineDialogControllerBridge>,
45    public TemplateURLModelObserver {
46 public:
47  SearchEngineDialogControllerBridge(SearchEngineDialogController* controller);
48
49  // TemplateURLModelObserver
50  virtual void OnTemplateURLModelChanged();
51
52 private:
53  SearchEngineDialogController* controller_;
54};
55
56SearchEngineDialogControllerBridge::SearchEngineDialogControllerBridge(
57    SearchEngineDialogController* controller) : controller_(controller) {
58}
59
60void SearchEngineDialogControllerBridge::OnTemplateURLModelChanged() {
61  [controller_ onTemplateURLModelChanged];
62  MessageLoop::current()->QuitNow();
63}
64
65@implementation SearchEngineDialogController
66
67@synthesize profile = profile_;
68@synthesize randomize = randomize_;
69
70- (id)init {
71  NSString* nibpath =
72      [base::mac::MainAppBundle() pathForResource:@"SearchEngineDialog"
73                                          ofType:@"nib"];
74  self = [super initWithWindowNibPath:nibpath owner:self];
75  if (self != nil) {
76    bridge_ = new SearchEngineDialogControllerBridge(self);
77  }
78  return self;
79}
80
81- (void)dealloc {
82  [super dealloc];
83}
84
85- (IBAction)showWindow:(id)sender {
86  searchEnginesModel_ = profile_->GetTemplateURLModel();
87  searchEnginesModel_->AddObserver(bridge_.get());
88
89  if (searchEnginesModel_->loaded()) {
90    MessageLoop::current()->PostTask(
91        FROM_HERE,
92        NewRunnableMethod(
93            bridge_.get(),
94            &SearchEngineDialogControllerBridge::OnTemplateURLModelChanged));
95  } else {
96    searchEnginesModel_->Load();
97  }
98  MessageLoop::current()->Run();
99}
100
101- (void)onTemplateURLModelChanged {
102  searchEnginesModel_->RemoveObserver(bridge_.get());
103
104  // Add the search engines in the search_engines_model_ to the buttons list.
105  // The first three will always be from prepopulated data.
106  std::vector<const TemplateURL*> templateUrls =
107      searchEnginesModel_->GetTemplateURLs();
108
109  // If we have fewer than two search engines, end the search engine dialog
110  // immediately, leaving the imported default search engine setting intact.
111  if (templateUrls.size() < 2) {
112    return;
113  }
114
115  NSWindow* win = [self window];
116
117  [win setBackgroundColor:[NSColor whiteColor]];
118
119  NSImage* headerImage = ResourceBundle::GetSharedInstance().
120      GetNativeImageNamed(IDR_SEARCH_ENGINE_DIALOG_TOP);
121  [headerImageView_ setImage:headerImage];
122
123  // Is the user's default search engine included in the first three
124  // prepopulated set? If not, we need to expand the dialog to include a fourth
125  // engine.
126  const TemplateURL* defaultSearchEngine =
127      searchEnginesModel_->GetDefaultSearchProvider();
128
129  std::vector<const TemplateURL*>::iterator engineIter =
130      templateUrls.begin();
131  for (int i = 0; engineIter != templateUrls.end(); ++i, ++engineIter) {
132    if (i < 3) {
133      choices_.push_back(*engineIter);
134    } else {
135      if (*engineIter == defaultSearchEngine)
136        choices_.push_back(*engineIter);
137    }
138  }
139
140  // Randomize the order of the logos if the option has been set.
141  if (randomize_) {
142    int seed = static_cast<int>(base::Time::Now().ToInternalValue());
143    srand(seed);
144    std::random_shuffle(choices_.begin(), choices_.end());
145  }
146
147  [self buildSearchEngineView];
148
149  // Display the dialog.
150  NSInteger choice = [NSApp runModalForWindow:win];
151  searchEnginesModel_->SetDefaultSearchProvider(choices_.at(choice));
152}
153
154- (void)buildSearchEngineView {
155  scoped_nsobject<NSMutableArray> searchEngineViews
156      ([[NSMutableArray alloc] init]);
157
158  for (size_t i = 0; i < choices_.size(); ++i)
159    [searchEngineViews addObject:[self viewForSearchEngine:choices_.at(i)
160                                                   atIndex:i]];
161
162  NSSize newOverallSize = NSZeroSize;
163  for (NSView* view in searchEngineViews.get()) {
164    NSRect engineFrame = [view frame];
165    engineFrame.origin = NSMakePoint(newOverallSize.width, 0);
166    [searchEngineView_ addSubview:view];
167    [view setFrame:engineFrame];
168    newOverallSize = NSMakeSize(
169        newOverallSize.width + NSWidth(engineFrame) + kSearchEngineSpacing,
170        std::max(newOverallSize.height, NSHeight(engineFrame)));
171  }
172  newOverallSize.width -= kSearchEngineSpacing;
173
174  // Resize the window to fit (and because it's bound on all sides it will
175  // resize the search engine view).
176  NSSize currentOverallSize = [searchEngineView_ bounds].size;
177  NSSize deltaSize = NSMakeSize(
178      newOverallSize.width - currentOverallSize.width,
179      newOverallSize.height - currentOverallSize.height);
180  NSSize windowDeltaSize = [searchEngineView_ convertSize:deltaSize toView:nil];
181  NSRect windowFrame = [[self window] frame];
182  windowFrame.size.width += windowDeltaSize.width;
183  windowFrame.size.height += windowDeltaSize.height;
184  [[self window] setFrame:windowFrame display:NO];
185}
186
187- (NSView*)viewForSearchEngine:(const TemplateURL*)engine
188                       atIndex:(size_t)index {
189  bool useImages = false;
190#if defined(GOOGLE_CHROME_BUILD)
191  useImages = true;
192#endif
193
194  // Make the engine identifier.
195  NSView* engineIdentifier = nil;  // either the logo or the text label
196
197  int logoId = engine->logo_id();
198  if (useImages && logoId > 0) {
199    NSImage* logoImage =
200        ResourceBundle::GetSharedInstance().GetNativeImageNamed(logoId);
201    NSRect logoBounds = NSZeroRect;
202    logoBounds.size = [logoImage size];
203    NSImageView* logoView =
204        [[[NSImageView alloc] initWithFrame:logoBounds] autorelease];
205    [logoView setImage:logoImage];
206    [logoView setEditable:NO];
207
208    // Tooltip text provides accessibility.
209    [logoView setToolTip:base::SysUTF16ToNSString(engine->short_name())];
210    engineIdentifier = logoView;
211  } else {
212    // No logo -- we must show a text label.
213    NSRect labelBounds = NSMakeRect(0, 0, kLogoLabelWidth, kLogoLabelHeight);
214    NSTextField* labelField =
215        [[[NSTextField alloc] initWithFrame:labelBounds] autorelease];
216    [labelField setBezeled:NO];
217    [labelField setEditable:NO];
218    [labelField setSelectable:NO];
219
220    scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
221        [[NSMutableParagraphStyle alloc] init]);
222    [paragraphStyle setAlignment:NSCenterTextAlignment];
223    NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:
224        [NSFont boldSystemFontOfSize:13], NSFontAttributeName,
225        paragraphStyle.get(), NSParagraphStyleAttributeName,
226        nil];
227
228    NSString* value = base::SysUTF16ToNSString(engine->short_name());
229    scoped_nsobject<NSAttributedString> attrValue(
230        [[NSAttributedString alloc] initWithString:value
231                                        attributes:attrs]);
232
233    [labelField setAttributedStringValue:attrValue.get()];
234
235    engineIdentifier = labelField;
236  }
237
238  // Make the "Choose" button.
239  scoped_nsobject<NSButton> chooseButton(
240      [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 100, 34)]);
241  [chooseButton setBezelStyle:NSRoundedBezelStyle];
242  [[chooseButton cell] setFont:[NSFont systemFontOfSize:
243      [NSFont systemFontSizeForControlSize:NSRegularControlSize]]];
244  [chooseButton setTitle:l10n_util::GetNSStringWithFixup(IDS_FR_SEARCH_CHOOSE)];
245  [GTMUILocalizerAndLayoutTweaker sizeToFitView:chooseButton.get()];
246  [chooseButton setTag:index];
247  [chooseButton setTarget:self];
248  [chooseButton setAction:@selector(searchEngineSelected:)];
249
250  // Put 'em together.
251  NSRect engineIdentifierFrame = [engineIdentifier frame];
252  NSRect chooseButtonFrame = [chooseButton frame];
253
254  NSRect containingViewFrame = NSZeroRect;
255  containingViewFrame.size.width += engineIdentifierFrame.size.width;
256  containingViewFrame.size.height += engineIdentifierFrame.size.height;
257  containingViewFrame.size.height += kLogoButtonSpacing;
258  containingViewFrame.size.height += chooseButtonFrame.size.height;
259
260  NSView* containingView =
261      [[[NSView alloc] initWithFrame:containingViewFrame] autorelease];
262
263  [containingView addSubview:engineIdentifier];
264  engineIdentifierFrame.origin.y =
265      chooseButtonFrame.size.height + kLogoButtonSpacing;
266  [engineIdentifier setFrame:engineIdentifierFrame];
267
268  [containingView addSubview:chooseButton];
269  chooseButtonFrame.origin.x =
270      int((containingViewFrame.size.width - chooseButtonFrame.size.width) / 2);
271  [chooseButton setFrame:chooseButtonFrame];
272
273  return containingView;
274}
275
276- (NSFont*)mainLabelFont {
277  return [NSFont boldSystemFontOfSize:13];
278}
279
280- (IBAction)searchEngineSelected:(id)sender {
281  [[self window] close];
282  [NSApp stopModalWithCode:[sender tag]];
283}
284
285@end
286