1 // Copyright (c) 2012 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 "chrome/browser/ui/cocoa/infobars/translate_infobar_base.h"
7 #include "base/logging.h"
8 #include "base/strings/sys_string_conversions.h"
9 #include "chrome/app/chrome_command_ids.h"
10 #include "chrome/browser/translate/translate_infobar_delegate.h"
11 #import "chrome/browser/ui/cocoa/hover_close_button.h"
12 #include "chrome/browser/ui/cocoa/infobars/after_translate_infobar_controller.h"
13 #import "chrome/browser/ui/cocoa/infobars/before_translate_infobar_controller.h"
14 #include "chrome/browser/ui/cocoa/infobars/infobar_cocoa.h"
15 #import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
16 #import "chrome/browser/ui/cocoa/infobars/infobar_controller.h"
17 #import "chrome/browser/ui/cocoa/infobars/infobar_gradient_view.h"
18 #import "chrome/browser/ui/cocoa/infobars/infobar_utilities.h"
19 #include "chrome/browser/ui/cocoa/infobars/translate_message_infobar_controller.h"
20 #include "grit/component_strings.h"
21 #include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
22 #include "ui/base/l10n/l10n_util.h"
24 using InfoBarUtilities::MoveControl;
25 using InfoBarUtilities::VerticallyCenterView;
26 using InfoBarUtilities::VerifyControlOrderAndSpacing;
27 using InfoBarUtilities::CreateLabel;
28 using InfoBarUtilities::AddMenuItem;
31 scoped_ptr<infobars::InfoBar> TranslateInfoBarDelegate::CreateInfoBar(
32 scoped_ptr<TranslateInfoBarDelegate> delegate) {
33 scoped_ptr<InfoBarCocoa> infobar(
34 new InfoBarCocoa(delegate.PassAs<infobars::InfoBarDelegate>()));
35 base::scoped_nsobject<TranslateInfoBarControllerBase> infobar_controller;
36 switch (infobar->delegate()->AsTranslateInfoBarDelegate()->translate_step()) {
37 case translate::TRANSLATE_STEP_BEFORE_TRANSLATE:
38 infobar_controller.reset([[BeforeTranslateInfobarController alloc]
39 initWithInfoBar:infobar.get()]);
41 case translate::TRANSLATE_STEP_AFTER_TRANSLATE:
42 infobar_controller.reset([[AfterTranslateInfobarController alloc]
43 initWithInfoBar:infobar.get()]);
45 case translate::TRANSLATE_STEP_TRANSLATING:
46 case translate::TRANSLATE_STEP_TRANSLATE_ERROR:
47 infobar_controller.reset([[TranslateMessageInfobarController alloc]
48 initWithInfoBar:infobar.get()]);
53 infobar->set_controller(infobar_controller);
54 return infobar.PassAs<infobars::InfoBar>();
57 @implementation TranslateInfoBarControllerBase (FrameChangeObserver)
59 // Triggered when the frame changes. This will figure out what size and
60 // visibility the options popup should be.
61 - (void)didChangeFrame:(NSNotification*)notification {
62 [self adjustOptionsButtonSizeAndVisibilityForView:
63 [[self visibleControls] lastObject]];
69 @interface TranslateInfoBarControllerBase (Private)
71 // Removes all controls so that layout can add in only the controls
73 - (void)clearAllControls;
75 // Create all the various controls we need for the toolbar.
76 - (void)constructViews;
78 // Reloads text for all labels for the current state.
79 - (void)loadLabelText:(TranslateErrors::Type)error;
81 // Main function to update the toolbar graphic state and data model after
82 // the state has changed.
83 // Controls are moved around as needed and visibility changed to match the
87 // Called when the source or target language selection changes in a menu.
88 // |newLanguageIdx| is the index of the newly selected item in the appropriate
90 - (void)sourceLanguageModified:(NSInteger)newLanguageIdx;
91 - (void)targetLanguageModified:(NSInteger)newLanguageIdx;
93 // Completely rebuild "from" and "to" language menus from the data model.
94 - (void)populateLanguageMenus;
98 #pragma mark TranslateInfoBarController class
100 @implementation TranslateInfoBarControllerBase
102 - (TranslateInfoBarDelegate*)delegate {
103 return reinterpret_cast<TranslateInfoBarDelegate*>([super delegate]);
106 - (void)constructViews {
107 // Using a zero or very large frame causes GTMUILocalizerAndLayoutTweaker
108 // to not resize the view properly so we take the bounds of the first label
109 // which is contained in the nib.
110 NSRect bogusFrame = [label_ frame];
111 label1_.reset(CreateLabel(bogusFrame));
112 label2_.reset(CreateLabel(bogusFrame));
113 label3_.reset(CreateLabel(bogusFrame));
115 optionsPopUp_.reset([[NSPopUpButton alloc] initWithFrame:bogusFrame
117 fromLanguagePopUp_.reset([[NSPopUpButton alloc] initWithFrame:bogusFrame
119 toLanguagePopUp_.reset([[NSPopUpButton alloc] initWithFrame:bogusFrame
121 showOriginalButton_.reset([[NSButton alloc] init]);
122 translateMessageButton_.reset([[NSButton alloc] init]);
125 - (void)sourceLanguageModified:(NSInteger)newLanguageIdx {
126 size_t newLanguageIdxSizeT = static_cast<size_t>(newLanguageIdx);
127 DCHECK_NE(TranslateInfoBarDelegate::kNoIndex, newLanguageIdxSizeT);
128 if (newLanguageIdxSizeT == [self delegate]->original_language_index())
130 [self delegate]->UpdateOriginalLanguageIndex(newLanguageIdxSizeT);
131 if ([self delegate]->translate_step() ==
132 translate::TRANSLATE_STEP_AFTER_TRANSLATE)
133 [self delegate]->Translate();
134 int commandId = IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE + newLanguageIdx;
135 int newMenuIdx = [fromLanguagePopUp_ indexOfItemWithTag:commandId];
136 [fromLanguagePopUp_ selectItemAtIndex:newMenuIdx];
139 - (void)targetLanguageModified:(NSInteger)newLanguageIdx {
140 size_t newLanguageIdxSizeT = static_cast<size_t>(newLanguageIdx);
141 DCHECK_NE(TranslateInfoBarDelegate::kNoIndex, newLanguageIdxSizeT);
142 if (newLanguageIdxSizeT == [self delegate]->target_language_index())
144 [self delegate]->UpdateTargetLanguageIndex(newLanguageIdxSizeT);
145 if ([self delegate]->translate_step() ==
146 translate::TRANSLATE_STEP_AFTER_TRANSLATE)
147 [self delegate]->Translate();
148 int commandId = IDC_TRANSLATE_TARGET_LANGUAGE_BASE + newLanguageIdx;
149 int newMenuIdx = [toLanguagePopUp_ indexOfItemWithTag:commandId];
150 [toLanguagePopUp_ selectItemAtIndex:newMenuIdx];
153 - (void)loadLabelText {
154 // Do nothing by default, should be implemented by subclasses.
157 - (void)updateState {
158 [self loadLabelText];
159 [self clearAllControls];
160 [self showVisibleControls:[self visibleControls]];
161 [optionsPopUp_ setHidden:![self shouldShowOptionsPopUp]];
163 [self adjustOptionsButtonSizeAndVisibilityForView:
164 [[self visibleControls] lastObject]];
167 - (void)removeOkCancelButtons {
168 // Removing okButton_ & cancelButton_ from the view may cause them
169 // to be released and since we can still access them from other areas
170 // in the code later, we need them to be nil when this happens.
171 [okButton_ removeFromSuperview];
173 [cancelButton_ removeFromSuperview];
177 - (void)clearAllControls {
178 // Step 1: remove all controls from the infobar so we have a clean slate.
179 NSArray *allControls = [self allControls];
181 for (NSControl* control in allControls) {
182 if ([control superview])
183 [control removeFromSuperview];
187 - (void)showVisibleControls:(NSArray*)visibleControls {
188 NSRect optionsFrame = [optionsPopUp_ frame];
189 for (NSControl* control in visibleControls) {
190 [GTMUILocalizerAndLayoutTweaker sizeToFitView:control];
191 [control setAutoresizingMask:NSViewMaxXMargin];
193 // Need to check if a view is already attached since |label1_| is always
194 // parented and we don't want to add it again.
195 if (![control superview])
196 [infoBarView_ addSubview:control];
198 if ([control isKindOfClass:[NSButton class]])
199 VerticallyCenterView(control);
201 // Make "from" and "to" language popup menus the same size as the options
203 // We don't autosize since some languages names are really long causing
204 // the toolbar to overflow.
205 if ([control isKindOfClass:[NSPopUpButton class]])
206 [control setFrame:optionsFrame];
214 - (NSArray*)visibleControls {
215 return [NSArray array];
218 - (void)rebuildOptionsMenu:(BOOL)hideTitle {
219 if (![self shouldShowOptionsPopUp])
222 // The options model doesn't know how to handle state transitions, so rebuild
223 // it each time through here.
224 optionsMenuModel_.reset(new OptionsMenuModel([self delegate]));
226 [optionsPopUp_ removeAllItems];
228 NSString* optionsLabel = hideTitle ? @"" :
229 l10n_util::GetNSString(IDS_TRANSLATE_INFOBAR_OPTIONS);
230 [optionsPopUp_ addItemWithTitle:optionsLabel];
232 // Populate options menu.
233 NSMenu* optionsMenu = [optionsPopUp_ menu];
234 [optionsMenu setAutoenablesItems:NO];
235 for (int i = 0; i < optionsMenuModel_->GetItemCount(); ++i) {
236 AddMenuItem(optionsMenu,
238 @selector(optionsMenuChanged:),
239 base::SysUTF16ToNSString(optionsMenuModel_->GetLabelAt(i)),
240 optionsMenuModel_->GetCommandIdAt(i),
241 optionsMenuModel_->IsEnabledAt(i),
242 optionsMenuModel_->IsItemCheckedAt(i));
246 - (BOOL)shouldShowOptionsPopUp {
250 - (void)populateLanguageMenus {
251 NSMenu* originalLanguageMenu = [fromLanguagePopUp_ menu];
252 [originalLanguageMenu setAutoenablesItems:NO];
253 NSMenu* targetLanguageMenu = [toLanguagePopUp_ menu];
254 [targetLanguageMenu setAutoenablesItems:NO];
255 for (size_t i = 0; i < [self delegate]->num_languages(); ++i) {
257 base::SysUTF16ToNSString([self delegate]->language_name_at(i));
258 AddMenuItem(originalLanguageMenu,
260 @selector(languageMenuChanged:),
262 IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE + i,
263 i != [self delegate]->target_language_index(),
264 i == [self delegate]->original_language_index());
265 AddMenuItem(targetLanguageMenu,
267 @selector(languageMenuChanged:),
269 IDC_TRANSLATE_TARGET_LANGUAGE_BASE + i,
270 i != [self delegate]->original_language_index(),
271 i == [self delegate]->target_language_index());
273 if ([self delegate]->original_language_index() !=
274 TranslateInfoBarDelegate::kNoIndex) {
276 selectItemAtIndex:([self delegate]->original_language_index())];
279 selectItemAtIndex:([self delegate]->target_language_index())];
282 - (void)addAdditionalControls {
283 using l10n_util::GetNSString;
284 using l10n_util::GetNSStringWithFixup;
286 // Get layout information from the NIB.
287 NSRect okButtonFrame = [okButton_ frame];
288 NSRect cancelButtonFrame = [cancelButton_ frame];
290 DCHECK(NSMaxX(cancelButtonFrame) < NSMinX(okButtonFrame))
291 << "Ok button expected to be on the right of the Cancel button in nib";
293 spaceBetweenControls_ = NSMinX(okButtonFrame) - NSMaxX(cancelButtonFrame);
295 // Instantiate additional controls.
296 [self constructViews];
298 // Set ourselves as the delegate for the options menu so we can populate it
300 [[optionsPopUp_ menu] setDelegate:self];
302 // Replace label_ with label1_ so we get a consistent look between all the
303 // labels we display in the translate view.
304 [[label_ superview] replaceSubview:label_ with:label1_.get()];
305 label_.reset(); // Now released.
307 // Populate contextual menus.
308 [self rebuildOptionsMenu:NO];
309 [self populateLanguageMenus];
311 // Set OK & Cancel text.
312 [okButton_ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_ACCEPT)];
313 [cancelButton_ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_DENY)];
315 // Set up "Show original" and "Try again" buttons.
316 [showOriginalButton_ setFrame:okButtonFrame];
318 // Set each of the buttons and popups to the NSTexturedRoundedBezelStyle
319 // (metal-looking) style.
320 NSArray* allControls = [self allControls];
321 for (NSControl* control in allControls) {
322 if (![control isKindOfClass:[NSButton class]])
324 NSButton* button = (NSButton*)control;
325 [button setBezelStyle:NSTexturedRoundedBezelStyle];
326 if ([button isKindOfClass:[NSPopUpButton class]]) {
327 [[button cell] setArrowPosition:NSPopUpArrowAtBottom];
330 // The options button is handled differently than the rest as it floats
332 [optionsPopUp_ setBezelStyle:NSTexturedRoundedBezelStyle];
333 [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtBottom];
335 [showOriginalButton_ setTarget:self];
336 [showOriginalButton_ setAction:@selector(showOriginal:)];
337 [translateMessageButton_ setTarget:self];
338 [translateMessageButton_ setAction:@selector(messageButtonPressed:)];
341 setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_REVERT)];
343 // Add and configure controls that are visible in all modes.
344 [optionsPopUp_ setAutoresizingMask:NSViewMinXMargin];
345 // Add "options" popup z-ordered below all other controls so when we
346 // resize the toolbar it doesn't hide them.
347 [infoBarView_ addSubview:optionsPopUp_
348 positioned:NSWindowBelow
350 [GTMUILocalizerAndLayoutTweaker sizeToFitView:optionsPopUp_];
351 MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
352 VerticallyCenterView(optionsPopUp_);
354 [infoBarView_ setPostsFrameChangedNotifications:YES];
355 [[NSNotificationCenter defaultCenter]
357 selector:@selector(didChangeFrame:)
358 name:NSViewFrameDidChangeNotification
359 object:infoBarView_];
360 // Show and place GUI elements.
364 - (void)infobarWillHide {
365 [[fromLanguagePopUp_ menu] cancelTracking];
366 [[toLanguagePopUp_ menu] cancelTracking];
367 [[optionsPopUp_ menu] cancelTracking];
368 [super infobarWillHide];
371 - (void)infobarWillClose {
372 [self disablePopUpMenu:[fromLanguagePopUp_ menu]];
373 [self disablePopUpMenu:[toLanguagePopUp_ menu]];
374 [self disablePopUpMenu:[optionsPopUp_ menu]];
375 [[NSNotificationCenter defaultCenter] removeObserver:self];
376 [super infobarWillClose];
379 - (void)adjustOptionsButtonSizeAndVisibilityForView:(NSView*)lastView {
380 [optionsPopUp_ setHidden:NO];
381 [self rebuildOptionsMenu:NO];
382 [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtBottom];
383 [optionsPopUp_ sizeToFit];
385 MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
386 if (!VerifyControlOrderAndSpacing(lastView, optionsPopUp_)) {
387 [self rebuildOptionsMenu:YES];
388 NSRect oldFrame = [optionsPopUp_ frame];
389 oldFrame.size.width = NSHeight(oldFrame);
390 [optionsPopUp_ setFrame:oldFrame];
391 [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtCenter];
392 MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
393 if (!VerifyControlOrderAndSpacing(lastView, optionsPopUp_)) {
394 [optionsPopUp_ setHidden:YES];
399 // Called when "Translate" button is clicked.
400 - (void)ok:(id)sender {
403 TranslateInfoBarDelegate* delegate = [self delegate];
404 translate::TranslateStep state = delegate->translate_step();
405 DCHECK(state == translate::TRANSLATE_STEP_BEFORE_TRANSLATE ||
406 state == translate::TRANSLATE_STEP_TRANSLATE_ERROR);
407 delegate->Translate();
410 // Called when someone clicks on the "Nope" button.
411 - (void)cancel:(id)sender {
414 TranslateInfoBarDelegate* delegate = [self delegate];
415 DCHECK_EQ(translate::TRANSLATE_STEP_BEFORE_TRANSLATE,
416 delegate->translate_step());
417 delegate->TranslationDeclined();
421 - (void)messageButtonPressed:(id)sender {
424 [self delegate]->MessageInfoBarButtonPressed();
427 - (IBAction)showOriginal:(id)sender {
430 [self delegate]->RevertTranslation();
433 // Called when any of the language drop down menus are changed.
434 - (void)languageMenuChanged:(id)item {
437 if ([item respondsToSelector:@selector(tag)]) {
438 int cmd = [item tag];
439 if (cmd >= IDC_TRANSLATE_TARGET_LANGUAGE_BASE) {
440 cmd -= IDC_TRANSLATE_TARGET_LANGUAGE_BASE;
441 [self targetLanguageModified:cmd];
443 } else if (cmd >= IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE) {
444 cmd -= IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE;
445 [self sourceLanguageModified:cmd];
449 NOTREACHED() << "Language menu was changed with a bad language ID";
452 // Called when the options menu is changed.
453 - (void)optionsMenuChanged:(id)item {
456 if ([item respondsToSelector:@selector(tag)]) {
457 int cmd = [item tag];
458 // Danger Will Robinson! : This call can release the infobar (e.g. invoking
459 // "About Translate" can open a new tab).
460 // Do not access member variables after this line!
461 optionsMenuModel_->ExecuteCommand(cmd, 0);
468 [showOriginalButton_ setTarget:nil];
469 [translateMessageButton_ setTarget:nil];
473 #pragma mark NSMenuDelegate
475 // Invoked by virtue of us being set as the delegate for the options menu.
476 - (void)menuNeedsUpdate:(NSMenu *)menu {
477 [self adjustOptionsButtonSizeAndVisibilityForView:
478 [[self visibleControls] lastObject]];
483 @implementation TranslateInfoBarControllerBase (TestingAPI)
485 - (NSArray*)allControls {
486 return [NSArray arrayWithObjects:label1_.get(),fromLanguagePopUp_.get(),
487 label2_.get(), toLanguagePopUp_.get(), label3_.get(), okButton_,
488 cancelButton_, showOriginalButton_.get(), translateMessageButton_.get(),
492 - (NSMenu*)optionsMenu {
493 return [optionsPopUp_ menu];
496 - (NSButton*)translateMessageButton {
497 return translateMessageButton_.get();
500 - (bool)verifyLayout {
501 // All the controls available to translate infobars, except the options popup.
502 // The options popup is shown/hidden instead of actually removed. This gets
503 // checked in the subclasses.
504 NSArray* allControls = [self allControls];
505 NSArray* visibleControls = [self visibleControls];
507 // Step 1: Make sure control visibility is what we expect.
508 for (NSUInteger i = 0; i < [allControls count]; ++i) {
509 id control = [allControls objectAtIndex:i];
510 bool hasSuperView = [control superview];
511 bool expectedVisibility = [visibleControls containsObject:control];
513 if (expectedVisibility != hasSuperView) {
514 NSString *title = @"";
515 if ([control isKindOfClass:[NSPopUpButton class]]) {
516 title = [[[control menu] itemAtIndex:0] title];
520 "State: " << [self description] <<
521 " Control @" << i << (hasSuperView ? " has" : " doesn't have") <<
522 " a superview" << [[control description] UTF8String] <<
523 " Title=" << [title UTF8String];
528 // Step 2: Check that controls are ordered correctly with no overlap.
529 id previousControl = nil;
530 for (NSUInteger i = 0; i < [visibleControls count]; ++i) {
531 id control = [visibleControls objectAtIndex:i];
532 // The options pop up doesn't lay out like the rest of the controls as
533 // it floats to the right. It has some known issues shown in
534 // http://crbug.com/47941.
535 if (control == optionsPopUp_.get())
537 if (previousControl &&
538 !VerifyControlOrderAndSpacing(previousControl, control)) {
539 NSString *title = @"";
540 if ([control isKindOfClass:[NSPopUpButton class]]) {
541 title = [[[control menu] itemAtIndex:0] title];
544 "State: " << [self description] <<
545 " Control @" << i << " not ordered correctly: " <<
546 [[control description] UTF8String] <<[title UTF8String];
549 previousControl = control;
555 @end // TranslateInfoBarControllerBase (TestingAPI)