1 // Copyright 2014 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/translate/translate_bubble_controller.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/mac/scoped_nsobject.h"
9 #include "base/strings/sys_string_conversions.h"
10 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
11 #import "chrome/browser/ui/cocoa/bubble_combobox.h"
12 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
13 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
14 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
15 #include "chrome/browser/ui/translate/language_combobox_model.h"
16 #include "chrome/browser/ui/translate/translate_bubble_model_impl.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "components/translate/core/browser/translate_ui_delegate.h"
19 #include "content/public/browser/browser_context.h"
20 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
21 #import "ui/base/cocoa/window_size_constants.h"
22 #include "ui/base/l10n/l10n_util.h"
23 #include "ui/base/models/combobox_model.h"
25 // TODO(hajimehoshi): This class is almost same as that of views. Refactor them.
26 class TranslateDenialComboboxModel : public ui::ComboboxModel {
28 explicit TranslateDenialComboboxModel(
29 const base::string16& original_language_name) {
30 // Dummy menu item, which is shown on the top of a NSPopUpButton. The top
31 // text of the denial pop up menu should be IDS_TRANSLATE_BUBBLE_DENY, while
32 // it is impossible to use it here because NSPopUpButtons' addItemWithTitle
33 // removes a duplicated menu item. Instead, the title will be set later by
34 // NSMenuItem's setTitle.
35 items_.push_back(base::string16());
37 // Menu items in the drop down menu.
38 items_.push_back(l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_DENY));
39 items_.push_back(l10n_util::GetStringFUTF16(
40 IDS_TRANSLATE_BUBBLE_NEVER_TRANSLATE_LANG,
41 original_language_name));
42 items_.push_back(l10n_util::GetStringUTF16(
43 IDS_TRANSLATE_BUBBLE_NEVER_TRANSLATE_SITE));
45 virtual ~TranslateDenialComboboxModel() {}
49 virtual int GetItemCount() const OVERRIDE {
52 virtual base::string16 GetItemAt(int index) OVERRIDE {
55 virtual bool IsItemSeparatorAt(int index) OVERRIDE {
58 virtual int GetDefaultIndex() const OVERRIDE {
62 std::vector<base::string16> items_;
64 DISALLOW_COPY_AND_ASSIGN(TranslateDenialComboboxModel);
67 const CGFloat kWindowWidth = 320;
69 // Padding between the window frame and content.
70 const CGFloat kFramePadding = 16;
72 const CGFloat kRelatedControlHorizontalSpacing = -2;
74 const CGFloat kRelatedControlVerticalSpacing = 4;
75 const CGFloat kUnrelatedControlVerticalSpacing = 20;
77 const CGFloat kContentWidth = kWindowWidth - 2 * kFramePadding;
79 @interface TranslateBubbleController()
81 - (void)performLayout;
82 - (NSView*)newBeforeTranslateView;
83 - (NSView*)newTranslatingView;
84 - (NSView*)newAfterTranslateView;
85 - (NSView*)newErrorView;
86 - (NSView*)newAdvancedView;
87 - (void)updateAdvancedView;
88 - (NSTextField*)addText:(NSString*)text
90 - (NSButton*)addLinkButtonWithText:(NSString*)text
93 - (NSButton*)addButton:(NSString*)title
96 - (NSButton*)addCheckbox:(NSString*)title
98 - (NSPopUpButton*)addPopUpButton:(ui::ComboboxModel*)model
100 toView:(NSView*)view;
101 - (void)handleTranslateButtonPressed;
102 - (void)handleNopeButtonPressed;
103 - (void)handleDoneButtonPressed;
104 - (void)handleCancelButtonPressed;
105 - (void)handleShowOriginalButtonPressed;
106 - (void)handleAdvancedLinkButtonPressed;
107 - (void)handleDenialPopUpButtonNopeSelected;
108 - (void)handleDenialPopUpButtonNeverTranslateLanguageSelected;
109 - (void)handleDenialPopUpButtonNeverTranslateSiteSelected;
110 - (void)handleSourceLanguagePopUpButtonSelectedItemChanged:(id)sender;
111 - (void)handleTargetLanguagePopUpButtonSelectedItemChanged:(id)sender;
115 @implementation TranslateBubbleController
117 - (id)initWithParentWindow:(BrowserWindowController*)controller
118 model:(scoped_ptr<TranslateBubbleModel>)model
119 webContents:(content::WebContents*)webContents {
120 NSWindow* parentWindow = [controller window];
122 // Use an arbitrary size; it will be changed in performLayout.
123 NSRect contentRect = ui::kWindowSizeDeterminedLater;
124 base::scoped_nsobject<InfoBubbleWindow> window(
125 [[InfoBubbleWindow alloc] initWithContentRect:contentRect
126 styleMask:NSBorderlessWindowMask
127 backing:NSBackingStoreBuffered
130 if ((self = [super initWithWindow:window
131 parentWindow:parentWindow
132 anchoredAt:NSZeroPoint])) {
133 webContents_ = webContents;
134 model_ = model.Pass();
135 if (model_->GetViewState() !=
136 TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE) {
137 translateExecuted_ = YES;
141 @(TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE):
142 [self newBeforeTranslateView],
143 @(TranslateBubbleModel::VIEW_STATE_TRANSLATING):
144 [self newTranslatingView],
145 @(TranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE):
146 [self newAfterTranslateView],
147 @(TranslateBubbleModel::VIEW_STATE_ERROR):
149 @(TranslateBubbleModel::VIEW_STATE_ADVANCED):
150 [self newAdvancedView],
153 [self performLayout];
158 @synthesize webContents = webContents_;
160 - (NSView*)currentView {
161 NSNumber* key = @(model_->GetViewState());
162 NSView* view = [views_ objectForKey:key];
167 - (const TranslateBubbleModel*)model {
171 - (void)showWindow:(id)sender {
172 BrowserWindowController* controller = [[self parentWindow] windowController];
173 NSPoint anchorPoint = [[controller toolbarController] translateBubblePoint];
174 anchorPoint = [[self parentWindow] convertBaseToScreen:anchorPoint];
175 [self setAnchorPoint:anchorPoint];
176 [super showWindow:sender];
179 - (void)switchView:(TranslateBubbleModel::ViewState)viewState {
180 if (model_->GetViewState() == viewState)
183 model_->SetViewState(viewState);
184 [self performLayout];
187 - (void)switchToErrorView:(translate::TranslateErrors::Type)errorType {
188 // FIXME: Implement this.
191 - (void)performLayout {
192 NSWindow* window = [self window];
193 [[window contentView] setSubviews:@[ [self currentView] ]];
195 CGFloat height = NSHeight([[self currentView] frame]) +
196 2 * kFramePadding + info_bubble::kBubbleArrowHeight;
198 NSRect windowFrame = [window contentRectForFrameRect:[[self window] frame]];
199 NSRect newWindowFrame = [window frameRectForContentRect:NSMakeRect(
200 NSMinX(windowFrame), NSMaxY(windowFrame) - height, kWindowWidth, height)];
201 [window setFrame:newWindowFrame
203 animate:[[self window] isVisible]];
206 - (NSView*)newBeforeTranslateView {
207 NSRect contentFrame = NSMakeRect(
212 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
215 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_BEFORE_TRANSLATE);
216 NSTextField* textLabel = [self addText:message
218 message = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ADVANCED);
219 NSButton* advancedLinkButton =
220 [self addLinkButtonWithText:message
221 action:@selector(handleAdvancedLinkButtonPressed)
225 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ACCEPT);
226 NSButton* translateButton =
227 [self addButton:title
228 action:@selector(handleTranslateButtonPressed)
231 base::string16 originalLanguageName =
232 model_->GetLanguageNameAt(model_->GetOriginalLanguageIndex());
233 // TODO(hajimehoshi): When TranslateDenialComboboxModel is factored out as a
234 // common model, ui::MenuModel will be used here.
235 translateDenialComboboxModel_.reset(
236 new TranslateDenialComboboxModel(originalLanguageName));
237 NSPopUpButton* denyPopUpButton =
238 [self addPopUpButton:translateDenialComboboxModel_.get()
241 [denyPopUpButton setPullsDown:YES];
242 [[denyPopUpButton itemAtIndex:1] setTarget:self];
243 [[denyPopUpButton itemAtIndex:1]
244 setAction:@selector(handleDenialPopUpButtonNopeSelected)];
245 [[denyPopUpButton itemAtIndex:2] setTarget:self];
246 [[denyPopUpButton itemAtIndex:2]
247 setAction:@selector(handleDenialPopUpButtonNeverTranslateLanguageSelected)];
248 [[denyPopUpButton itemAtIndex:3] setTarget:self];
249 [[denyPopUpButton itemAtIndex:3]
250 setAction:@selector(handleDenialPopUpButtonNeverTranslateSiteSelected)];
252 title = base::SysUTF16ToNSString(
253 l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_DENY));
254 [[denyPopUpButton itemAtIndex:0] setTitle:title];
256 // Adjust width for the first item.
257 base::scoped_nsobject<NSMenu> originalMenu([[denyPopUpButton menu] copy]);
258 [denyPopUpButton removeAllItems];
259 [denyPopUpButton addItemWithTitle:[[originalMenu itemAtIndex:0] title]];
260 [denyPopUpButton sizeToFit];
261 [denyPopUpButton setMenu:originalMenu];
266 [translateButton setFrameOrigin:NSMakePoint(
267 kContentWidth - NSWidth([translateButton frame]), yPos)];
269 NSRect denyPopUpButtonFrame = [denyPopUpButton frame];
270 CGFloat diffY = [[denyPopUpButton cell]
271 titleRectForBounds:[denyPopUpButton bounds]].origin.y;
272 [denyPopUpButton setFrameOrigin:NSMakePoint(
273 NSMinX([translateButton frame]) - denyPopUpButtonFrame.size.width
274 - kRelatedControlHorizontalSpacing,
277 yPos += NSHeight([translateButton frame]) +
278 kUnrelatedControlVerticalSpacing;
280 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
281 [advancedLinkButton setFrameOrigin:NSMakePoint(
282 NSWidth([textLabel frame]), yPos)];
284 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
289 - (NSView*)newTranslatingView {
290 NSRect contentFrame = NSMakeRect(
295 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
298 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_TRANSLATING);
299 NSTextField* textLabel = [self addText:message
302 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_REVERT);
303 NSButton* showOriginalButton =
304 [self addButton:title
305 action:@selector(handleShowOriginalButtonPressed)
307 [showOriginalButton setEnabled:NO];
310 // TODO(hajimehoshi): Use l10n_util::VerticallyReflowGroup.
313 [showOriginalButton setFrameOrigin:NSMakePoint(
314 kContentWidth - NSWidth([showOriginalButton frame]), yPos)];
316 yPos += NSHeight([showOriginalButton frame]) +
317 kUnrelatedControlVerticalSpacing;
319 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
321 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
326 - (NSView*)newAfterTranslateView {
327 NSRect contentFrame = NSMakeRect(
332 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
335 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_TRANSLATED);
336 NSTextField* textLabel = [self addText:message
338 message = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ADVANCED);
339 NSButton* advancedLinkButton =
340 [self addLinkButtonWithText:message
341 action:@selector(handleAdvancedLinkButtonPressed)
344 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_REVERT);
345 NSButton* showOriginalButton =
346 [self addButton:title
347 action:@selector(handleShowOriginalButtonPressed)
353 [showOriginalButton setFrameOrigin:NSMakePoint(
354 kContentWidth - NSWidth([showOriginalButton frame]), yPos)];
356 yPos += NSHeight([showOriginalButton frame]) +
357 kUnrelatedControlVerticalSpacing;
359 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
360 [advancedLinkButton setFrameOrigin:NSMakePoint(
361 NSMaxX([textLabel frame]), yPos)];
363 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
368 - (NSView*)newErrorView {
369 NSRect contentFrame = NSMakeRect(
374 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
376 // TODO(hajimehoshi): Implement this.
381 - (NSView*)newAdvancedView {
382 NSRect contentFrame = NSMakeRect(
387 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
389 NSString* title = l10n_util::GetNSStringWithFixup(
390 IDS_TRANSLATE_BUBBLE_PAGE_LANGUAGE);
391 NSTextField* sourceLanguageLabel = [self addText:title
393 title = l10n_util::GetNSStringWithFixup(
394 IDS_TRANSLATE_BUBBLE_TRANSLATION_LANGUAGE);
395 NSTextField* targetLanguageLabel = [self addText:title
399 int sourceDefaultIndex = model_->GetOriginalLanguageIndex();
400 int targetDefaultIndex = model_->GetTargetLanguageIndex();
401 sourceLanguageComboboxModel_.reset(
402 new LanguageComboboxModel(sourceDefaultIndex, model_.get()));
403 targetLanguageComboboxModel_.reset(
404 new LanguageComboboxModel(targetDefaultIndex, model_.get()));
405 SEL action = @selector(handleSourceLanguagePopUpButtonSelectedItemChanged:);
406 NSPopUpButton* sourcePopUpButton =
407 [self addPopUpButton:sourceLanguageComboboxModel_.get()
410 action = @selector(handleTargetLanguagePopUpButtonSelectedItemChanged:);
411 NSPopUpButton* targetPopUpButton =
412 [self addPopUpButton:targetLanguageComboboxModel_.get()
416 // 'Always translate' checkbox
417 BOOL isIncognitoWindow = webContents_ ?
418 webContents_->GetBrowserContext()->IsOffTheRecord() : NO;
419 if (!isIncognitoWindow) {
421 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ALWAYS);
422 alwaysTranslateCheckbox_ = [self addCheckbox:title
427 advancedDoneButton_ =
428 [self addButton:l10n_util::GetNSStringWithFixup(IDS_DONE)
429 action:@selector(handleDoneButtonPressed)
431 advancedCancelButton_ =
432 [self addButton:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
433 action:@selector(handleCancelButtonPressed)
437 CGFloat textLabelWidth = NSWidth([sourceLanguageLabel frame]);
438 if (textLabelWidth < NSWidth([targetLanguageLabel frame]))
439 textLabelWidth = NSWidth([targetLanguageLabel frame]);
443 [advancedDoneButton_ setFrameOrigin:NSMakePoint(0, yPos)];
444 [advancedCancelButton_ setFrameOrigin:NSMakePoint(0, yPos)];
446 yPos += NSHeight([advancedDoneButton_ frame]) +
447 kUnrelatedControlVerticalSpacing;
449 if (alwaysTranslateCheckbox_) {
450 [alwaysTranslateCheckbox_ setFrameOrigin:NSMakePoint(textLabelWidth, yPos)];
452 yPos += NSHeight([alwaysTranslateCheckbox_ frame]) +
453 kRelatedControlVerticalSpacing;
456 CGFloat diffY = [[sourcePopUpButton cell]
457 titleRectForBounds:[sourcePopUpButton bounds]].origin.y;
459 [targetLanguageLabel setFrameOrigin:NSMakePoint(
460 textLabelWidth - NSWidth([targetLanguageLabel frame]), yPos + diffY)];
462 NSRect frame = [targetPopUpButton frame];
463 frame.origin = NSMakePoint(textLabelWidth, yPos);
464 frame.size.width = (kWindowWidth - 2 * kFramePadding) - textLabelWidth;
465 [targetPopUpButton setFrame:frame];
467 yPos += NSHeight([targetPopUpButton frame]) +
468 kRelatedControlVerticalSpacing;
470 [sourceLanguageLabel setFrameOrigin:NSMakePoint(
471 textLabelWidth - NSWidth([sourceLanguageLabel frame]), yPos + diffY)];
473 frame = [sourcePopUpButton frame];
474 frame.origin = NSMakePoint(textLabelWidth, yPos);
475 frame.size.width = NSWidth([targetPopUpButton frame]);
476 [sourcePopUpButton setFrame:frame];
478 [view setFrameSize:NSMakeSize(kContentWidth,
479 NSMaxY([sourcePopUpButton frame]))];
481 [self updateAdvancedView];
486 - (void)updateAdvancedView {
487 NSInteger state = model_->ShouldAlwaysTranslate() ? NSOnState : NSOffState;
488 [alwaysTranslateCheckbox_ setState:state];
491 if (model_->IsPageTranslatedInCurrentLanguages())
492 title = l10n_util::GetNSStringWithFixup(IDS_DONE);
494 title = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ACCEPT);
495 [advancedDoneButton_ setTitle:title];
496 [advancedDoneButton_ sizeToFit];
498 NSRect frame = [advancedDoneButton_ frame];
499 frame.origin.x = (kWindowWidth - 2 * kFramePadding) - NSWidth(frame);
500 [advancedDoneButton_ setFrameOrigin:frame.origin];
502 frame = [advancedCancelButton_ frame];
503 frame.origin.x = NSMinX([advancedDoneButton_ frame]) - NSWidth(frame)
504 - kRelatedControlHorizontalSpacing;
505 [advancedCancelButton_ setFrameOrigin:frame.origin];
508 - (NSTextField*)addText:(NSString*)text
509 toView:(NSView*)view {
510 base::scoped_nsobject<NSTextField> textField(
511 [[NSTextField alloc] initWithFrame:NSZeroRect]);
512 [textField setEditable:NO];
513 [textField setSelectable:YES];
514 [textField setDrawsBackground:NO];
515 [textField setBezeled:NO];
516 [textField setStringValue:text];
517 NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
518 [textField setFont:font];
519 [textField setAutoresizingMask:NSViewWidthSizable];
520 [view addSubview:textField.get()];
522 [textField sizeToFit];
523 return textField.get();
526 - (NSButton*)addLinkButtonWithText:(NSString*)text
528 toView:(NSView*)view {
529 base::scoped_nsobject<NSButton> button(
530 [[HyperlinkButtonCell buttonWithString:text] retain]);
532 [button setButtonType:NSMomentaryPushInButton];
533 [button setBezelStyle:NSRegularSquareBezelStyle];
534 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
536 [button setTarget:self];
537 [button setAction:action];
539 [view addSubview:button.get()];
544 - (NSButton*)addButton:(NSString*)title
546 toView:(NSView*)view {
547 base::scoped_nsobject<NSButton> button(
548 [[NSButton alloc] initWithFrame:NSZeroRect]);
549 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
550 [button setTitle:title];
551 [button setBezelStyle:NSRoundedBezelStyle];
552 [[button cell] setControlSize:NSSmallControlSize];
554 [button setTarget:self];
555 [button setAction:action];
557 [view addSubview:button.get()];
562 - (NSButton*)addCheckbox:(NSString*)title
563 toView:(NSView*)view {
564 base::scoped_nsobject<NSButton> button(
565 [[NSButton alloc] initWithFrame:NSZeroRect]);
566 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
567 [button setTitle:title];
568 [[button cell] setControlSize:NSSmallControlSize];
569 [button setButtonType:NSSwitchButton];
572 [view addSubview:button.get()];
577 - (NSPopUpButton*)addPopUpButton:(ui::ComboboxModel*)model
579 toView:(NSView*)view {
580 base::scoped_nsobject<NSPopUpButton> button(
581 [[BubbleCombobox alloc] initWithFrame:NSZeroRect
584 [button setTarget:self];
585 [button setAction:action];
587 [view addSubview:button.get()];
591 - (void)handleTranslateButtonPressed {
592 translateExecuted_ = YES;
596 - (void)handleNopeButtonPressed {
600 - (void)handleDoneButtonPressed {
601 if (alwaysTranslateCheckbox_) {
602 model_->SetAlwaysTranslate(
603 [alwaysTranslateCheckbox_ state] == NSOnState);
605 if (model_->IsPageTranslatedInCurrentLanguages()) {
606 model_->GoBackFromAdvanced();
607 [self performLayout];
609 translateExecuted_ = true;
611 [self switchView:TranslateBubbleModel::VIEW_STATE_TRANSLATING];
615 - (void)handleCancelButtonPressed {
616 model_->GoBackFromAdvanced();
617 [self performLayout];
620 - (void)handleShowOriginalButtonPressed {
621 model_->RevertTranslation();
625 - (void)handleAdvancedLinkButtonPressed {
626 [self switchView:TranslateBubbleModel::VIEW_STATE_ADVANCED];
629 - (void)handleDenialPopUpButtonNopeSelected {
633 - (void)handleDenialPopUpButtonNeverTranslateLanguageSelected {
634 model_->SetNeverTranslateLanguage(true);
638 - (void)handleDenialPopUpButtonNeverTranslateSiteSelected {
639 model_->SetNeverTranslateSite(true);
643 - (void)handleSourceLanguagePopUpButtonSelectedItemChanged:(id)sender {
644 NSPopUpButton* button = base::mac::ObjCCastStrict<NSPopUpButton>(sender);
645 model_->UpdateOriginalLanguageIndex([button indexOfSelectedItem]);
646 [self updateAdvancedView];
649 - (void)handleTargetLanguagePopUpButtonSelectedItemChanged:(id)sender {
650 NSPopUpButton* button = base::mac::ObjCCastStrict<NSPopUpButton>(sender);
651 model_->UpdateTargetLanguageIndex([button indexOfSelectedItem]);
652 [self updateAdvancedView];