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/generated_resources.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<InfoBar> TranslateInfoBarDelegate::CreateInfoBar(
32 scoped_ptr<TranslateInfoBarDelegate> delegate) {
33 scoped_ptr<InfoBarCocoa> infobar(
34 new InfoBarCocoa(delegate.PassAs<InfoBarDelegate>()));
35 base::scoped_nsobject<TranslateInfoBarControllerBase> infobar_controller;
36 switch (infobar->delegate()->AsTranslateInfoBarDelegate()->translate_step()) {
37 case TranslateTabHelper::BEFORE_TRANSLATE:
38 infobar_controller.reset([[BeforeTranslateInfobarController alloc]
39 initWithInfoBar:infobar.get()]);
41 case TranslateTabHelper::AFTER_TRANSLATE:
42 infobar_controller.reset([[AfterTranslateInfobarController alloc]
43 initWithInfoBar:infobar.get()]);
45 case TranslateTabHelper::TRANSLATING:
46 case TranslateTabHelper::TRANSLATE_ERROR:
47 infobar_controller.reset([[TranslateMessageInfobarController alloc]
48 initWithInfoBar:infobar.get()]);
53 infobar->set_controller(infobar_controller);
54 return infobar.PassAs<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() == TranslateTabHelper::AFTER_TRANSLATE)
132 [self delegate]->Translate();
133 int commandId = IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE + newLanguageIdx;
134 int newMenuIdx = [fromLanguagePopUp_ indexOfItemWithTag:commandId];
135 [fromLanguagePopUp_ selectItemAtIndex:newMenuIdx];
138 - (void)targetLanguageModified:(NSInteger)newLanguageIdx {
139 size_t newLanguageIdxSizeT = static_cast<size_t>(newLanguageIdx);
140 DCHECK_NE(TranslateInfoBarDelegate::kNoIndex, newLanguageIdxSizeT);
141 if (newLanguageIdxSizeT == [self delegate]->target_language_index())
143 [self delegate]->UpdateTargetLanguageIndex(newLanguageIdxSizeT);
144 if ([self delegate]->translate_step() == TranslateTabHelper::AFTER_TRANSLATE)
145 [self delegate]->Translate();
146 int commandId = IDC_TRANSLATE_TARGET_LANGUAGE_BASE + newLanguageIdx;
147 int newMenuIdx = [toLanguagePopUp_ indexOfItemWithTag:commandId];
148 [toLanguagePopUp_ selectItemAtIndex:newMenuIdx];
151 - (void)loadLabelText {
152 // Do nothing by default, should be implemented by subclasses.
155 - (void)updateState {
156 [self loadLabelText];
157 [self clearAllControls];
158 [self showVisibleControls:[self visibleControls]];
159 [optionsPopUp_ setHidden:![self shouldShowOptionsPopUp]];
161 [self adjustOptionsButtonSizeAndVisibilityForView:
162 [[self visibleControls] lastObject]];
165 - (void)removeOkCancelButtons {
166 // Removing okButton_ & cancelButton_ from the view may cause them
167 // to be released and since we can still access them from other areas
168 // in the code later, we need them to be nil when this happens.
169 [okButton_ removeFromSuperview];
171 [cancelButton_ removeFromSuperview];
175 - (void)clearAllControls {
176 // Step 1: remove all controls from the infobar so we have a clean slate.
177 NSArray *allControls = [self allControls];
179 for (NSControl* control in allControls) {
180 if ([control superview])
181 [control removeFromSuperview];
185 - (void)showVisibleControls:(NSArray*)visibleControls {
186 NSRect optionsFrame = [optionsPopUp_ frame];
187 for (NSControl* control in visibleControls) {
188 [GTMUILocalizerAndLayoutTweaker sizeToFitView:control];
189 [control setAutoresizingMask:NSViewMaxXMargin];
191 // Need to check if a view is already attached since |label1_| is always
192 // parented and we don't want to add it again.
193 if (![control superview])
194 [infoBarView_ addSubview:control];
196 if ([control isKindOfClass:[NSButton class]])
197 VerticallyCenterView(control);
199 // Make "from" and "to" language popup menus the same size as the options
201 // We don't autosize since some languages names are really long causing
202 // the toolbar to overflow.
203 if ([control isKindOfClass:[NSPopUpButton class]])
204 [control setFrame:optionsFrame];
212 - (NSArray*)visibleControls {
213 return [NSArray array];
216 - (void)rebuildOptionsMenu:(BOOL)hideTitle {
217 if (![self shouldShowOptionsPopUp])
220 // The options model doesn't know how to handle state transitions, so rebuild
221 // it each time through here.
222 optionsMenuModel_.reset(new OptionsMenuModel([self delegate]));
224 [optionsPopUp_ removeAllItems];
226 NSString* optionsLabel = hideTitle ? @"" :
227 l10n_util::GetNSString(IDS_TRANSLATE_INFOBAR_OPTIONS);
228 [optionsPopUp_ addItemWithTitle:optionsLabel];
230 // Populate options menu.
231 NSMenu* optionsMenu = [optionsPopUp_ menu];
232 [optionsMenu setAutoenablesItems:NO];
233 for (int i = 0; i < optionsMenuModel_->GetItemCount(); ++i) {
234 AddMenuItem(optionsMenu,
236 @selector(optionsMenuChanged:),
237 base::SysUTF16ToNSString(optionsMenuModel_->GetLabelAt(i)),
238 optionsMenuModel_->GetCommandIdAt(i),
239 optionsMenuModel_->IsEnabledAt(i),
240 optionsMenuModel_->IsItemCheckedAt(i));
244 - (BOOL)shouldShowOptionsPopUp {
248 - (void)populateLanguageMenus {
249 NSMenu* originalLanguageMenu = [fromLanguagePopUp_ menu];
250 [originalLanguageMenu setAutoenablesItems:NO];
251 NSMenu* targetLanguageMenu = [toLanguagePopUp_ menu];
252 [targetLanguageMenu setAutoenablesItems:NO];
253 for (size_t i = 0; i < [self delegate]->num_languages(); ++i) {
255 base::SysUTF16ToNSString([self delegate]->language_name_at(i));
256 AddMenuItem(originalLanguageMenu,
258 @selector(languageMenuChanged:),
260 IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE + i,
261 i != [self delegate]->target_language_index(),
262 i == [self delegate]->original_language_index());
263 AddMenuItem(targetLanguageMenu,
265 @selector(languageMenuChanged:),
267 IDC_TRANSLATE_TARGET_LANGUAGE_BASE + i,
268 i != [self delegate]->original_language_index(),
269 i == [self delegate]->target_language_index());
271 if ([self delegate]->original_language_index() !=
272 TranslateInfoBarDelegate::kNoIndex) {
274 selectItemAtIndex:([self delegate]->original_language_index())];
277 selectItemAtIndex:([self delegate]->target_language_index())];
280 - (void)addAdditionalControls {
281 using l10n_util::GetNSString;
282 using l10n_util::GetNSStringWithFixup;
284 // Get layout information from the NIB.
285 NSRect okButtonFrame = [okButton_ frame];
286 NSRect cancelButtonFrame = [cancelButton_ frame];
288 DCHECK(NSMaxX(cancelButtonFrame) < NSMinX(okButtonFrame))
289 << "Ok button expected to be on the right of the Cancel button in nib";
291 spaceBetweenControls_ = NSMinX(okButtonFrame) - NSMaxX(cancelButtonFrame);
293 // Instantiate additional controls.
294 [self constructViews];
296 // Set ourselves as the delegate for the options menu so we can populate it
298 [[optionsPopUp_ menu] setDelegate:self];
300 // Replace label_ with label1_ so we get a consistent look between all the
301 // labels we display in the translate view.
302 [[label_ superview] replaceSubview:label_ with:label1_.get()];
303 label_.reset(); // Now released.
305 // Populate contextual menus.
306 [self rebuildOptionsMenu:NO];
307 [self populateLanguageMenus];
309 // Set OK & Cancel text.
310 [okButton_ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_ACCEPT)];
311 [cancelButton_ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_DENY)];
313 // Set up "Show original" and "Try again" buttons.
314 [showOriginalButton_ setFrame:okButtonFrame];
316 // Set each of the buttons and popups to the NSTexturedRoundedBezelStyle
317 // (metal-looking) style.
318 NSArray* allControls = [self allControls];
319 for (NSControl* control in allControls) {
320 if (![control isKindOfClass:[NSButton class]])
322 NSButton* button = (NSButton*)control;
323 [button setBezelStyle:NSTexturedRoundedBezelStyle];
324 if ([button isKindOfClass:[NSPopUpButton class]]) {
325 [[button cell] setArrowPosition:NSPopUpArrowAtBottom];
328 // The options button is handled differently than the rest as it floats
330 [optionsPopUp_ setBezelStyle:NSTexturedRoundedBezelStyle];
331 [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtBottom];
333 [showOriginalButton_ setTarget:self];
334 [showOriginalButton_ setAction:@selector(showOriginal:)];
335 [translateMessageButton_ setTarget:self];
336 [translateMessageButton_ setAction:@selector(messageButtonPressed:)];
339 setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_REVERT)];
341 // Add and configure controls that are visible in all modes.
342 [optionsPopUp_ setAutoresizingMask:NSViewMinXMargin];
343 // Add "options" popup z-ordered below all other controls so when we
344 // resize the toolbar it doesn't hide them.
345 [infoBarView_ addSubview:optionsPopUp_
346 positioned:NSWindowBelow
348 [GTMUILocalizerAndLayoutTweaker sizeToFitView:optionsPopUp_];
349 MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
350 VerticallyCenterView(optionsPopUp_);
352 [infoBarView_ setPostsFrameChangedNotifications:YES];
353 [[NSNotificationCenter defaultCenter]
355 selector:@selector(didChangeFrame:)
356 name:NSViewFrameDidChangeNotification
357 object:infoBarView_];
358 // Show and place GUI elements.
362 - (void)infobarWillHide {
363 [[fromLanguagePopUp_ menu] cancelTracking];
364 [[toLanguagePopUp_ menu] cancelTracking];
365 [[optionsPopUp_ menu] cancelTracking];
366 [super infobarWillHide];
369 - (void)infobarWillClose {
370 [self disablePopUpMenu:[fromLanguagePopUp_ menu]];
371 [self disablePopUpMenu:[toLanguagePopUp_ menu]];
372 [self disablePopUpMenu:[optionsPopUp_ menu]];
373 [[NSNotificationCenter defaultCenter] removeObserver:self];
374 [super infobarWillClose];
377 - (void)adjustOptionsButtonSizeAndVisibilityForView:(NSView*)lastView {
378 [optionsPopUp_ setHidden:NO];
379 [self rebuildOptionsMenu:NO];
380 [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtBottom];
381 [optionsPopUp_ sizeToFit];
383 MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
384 if (!VerifyControlOrderAndSpacing(lastView, optionsPopUp_)) {
385 [self rebuildOptionsMenu:YES];
386 NSRect oldFrame = [optionsPopUp_ frame];
387 oldFrame.size.width = NSHeight(oldFrame);
388 [optionsPopUp_ setFrame:oldFrame];
389 [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtCenter];
390 MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
391 if (!VerifyControlOrderAndSpacing(lastView, optionsPopUp_)) {
392 [optionsPopUp_ setHidden:YES];
397 // Called when "Translate" button is clicked.
398 - (void)ok:(id)sender {
401 TranslateInfoBarDelegate* delegate = [self delegate];
402 TranslateTabHelper::TranslateStep state = delegate->translate_step();
403 DCHECK(state == TranslateTabHelper::BEFORE_TRANSLATE ||
404 state == TranslateTabHelper::TRANSLATE_ERROR);
405 delegate->Translate();
408 // Called when someone clicks on the "Nope" button.
409 - (void)cancel:(id)sender {
412 TranslateInfoBarDelegate* delegate = [self delegate];
413 DCHECK_EQ(TranslateTabHelper::BEFORE_TRANSLATE, delegate->translate_step());
414 delegate->TranslationDeclined();
418 - (void)messageButtonPressed:(id)sender {
421 [self delegate]->MessageInfoBarButtonPressed();
424 - (IBAction)showOriginal:(id)sender {
427 [self delegate]->RevertTranslation();
430 // Called when any of the language drop down menus are changed.
431 - (void)languageMenuChanged:(id)item {
434 if ([item respondsToSelector:@selector(tag)]) {
435 int cmd = [item tag];
436 if (cmd >= IDC_TRANSLATE_TARGET_LANGUAGE_BASE) {
437 cmd -= IDC_TRANSLATE_TARGET_LANGUAGE_BASE;
438 [self targetLanguageModified:cmd];
440 } else if (cmd >= IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE) {
441 cmd -= IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE;
442 [self sourceLanguageModified:cmd];
446 NOTREACHED() << "Language menu was changed with a bad language ID";
449 // Called when the options menu is changed.
450 - (void)optionsMenuChanged:(id)item {
453 if ([item respondsToSelector:@selector(tag)]) {
454 int cmd = [item tag];
455 // Danger Will Robinson! : This call can release the infobar (e.g. invoking
456 // "About Translate" can open a new tab).
457 // Do not access member variables after this line!
458 optionsMenuModel_->ExecuteCommand(cmd, 0);
465 [showOriginalButton_ setTarget:nil];
466 [translateMessageButton_ setTarget:nil];
470 #pragma mark NSMenuDelegate
472 // Invoked by virtue of us being set as the delegate for the options menu.
473 - (void)menuNeedsUpdate:(NSMenu *)menu {
474 [self adjustOptionsButtonSizeAndVisibilityForView:
475 [[self visibleControls] lastObject]];
480 @implementation TranslateInfoBarControllerBase (TestingAPI)
482 - (NSArray*)allControls {
483 return [NSArray arrayWithObjects:label1_.get(),fromLanguagePopUp_.get(),
484 label2_.get(), toLanguagePopUp_.get(), label3_.get(), okButton_,
485 cancelButton_, showOriginalButton_.get(), translateMessageButton_.get(),
489 - (NSMenu*)optionsMenu {
490 return [optionsPopUp_ menu];
493 - (NSButton*)translateMessageButton {
494 return translateMessageButton_.get();
497 - (bool)verifyLayout {
498 // All the controls available to translate infobars, except the options popup.
499 // The options popup is shown/hidden instead of actually removed. This gets
500 // checked in the subclasses.
501 NSArray* allControls = [self allControls];
502 NSArray* visibleControls = [self visibleControls];
504 // Step 1: Make sure control visibility is what we expect.
505 for (NSUInteger i = 0; i < [allControls count]; ++i) {
506 id control = [allControls objectAtIndex:i];
507 bool hasSuperView = [control superview];
508 bool expectedVisibility = [visibleControls containsObject:control];
510 if (expectedVisibility != hasSuperView) {
511 NSString *title = @"";
512 if ([control isKindOfClass:[NSPopUpButton class]]) {
513 title = [[[control menu] itemAtIndex:0] title];
517 "State: " << [self description] <<
518 " Control @" << i << (hasSuperView ? " has" : " doesn't have") <<
519 " a superview" << [[control description] UTF8String] <<
520 " Title=" << [title UTF8String];
525 // Step 2: Check that controls are ordered correctly with no overlap.
526 id previousControl = nil;
527 for (NSUInteger i = 0; i < [visibleControls count]; ++i) {
528 id control = [visibleControls objectAtIndex:i];
529 // The options pop up doesn't lay out like the rest of the controls as
530 // it floats to the right. It has some known issues shown in
531 // http://crbug.com/47941.
532 if (control == optionsPopUp_.get())
534 if (previousControl &&
535 !VerifyControlOrderAndSpacing(previousControl, control)) {
536 NSString *title = @"";
537 if ([control isKindOfClass:[NSPopUpButton class]]) {
538 title = [[[control menu] itemAtIndex:0] title];
541 "State: " << [self description] <<
542 " Control @" << i << " not ordered correctly: " <<
543 [[control description] UTF8String] <<[title UTF8String];
546 previousControl = control;
552 @end // TranslateInfoBarControllerBase (TestingAPI)