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