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/info_bubble_view.h"
12 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
13 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
14 #include "chrome/browser/ui/translate/language_combobox_model.h"
15 #include "chrome/browser/ui/translate/translate_bubble_model_impl.h"
16 #include "components/translate/core/browser/translate_ui_delegate.h"
17 #include "content/public/browser/browser_context.h"
18 #include "grit/generated_resources.h"
19 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
20 #import "ui/base/cocoa/window_size_constants.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/base/models/combobox_model.h"
24 // TODO(hajimehoshi): This class is almost same as that of views. Refactor them.
25 class TranslateDenialComboboxModel : public ui::ComboboxModel {
27 explicit TranslateDenialComboboxModel(
28 const base::string16& original_language_name) {
29 // Dummy menu item, which is shown on the top of a NSPopUpButton. The top
30 // text of the denial pop up menu should be IDS_TRANSLATE_BUBBLE_DENY, while
31 // it is impossible to use it here because NSPopUpButtons' addItemWithTitle
32 // removes a duplicated menu item. Instead, the title will be set later by
33 // NSMenuItem's setTitle.
34 items_.push_back(base::string16());
36 // Menu items in the drop down menu.
37 items_.push_back(l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_DENY));
38 items_.push_back(l10n_util::GetStringFUTF16(
39 IDS_TRANSLATE_BUBBLE_NEVER_TRANSLATE_LANG,
40 original_language_name));
41 items_.push_back(l10n_util::GetStringUTF16(
42 IDS_TRANSLATE_BUBBLE_NEVER_TRANSLATE_SITE));
44 virtual ~TranslateDenialComboboxModel() {}
48 virtual int GetItemCount() const OVERRIDE {
51 virtual base::string16 GetItemAt(int index) OVERRIDE {
54 virtual bool IsItemSeparatorAt(int index) OVERRIDE {
57 virtual int GetDefaultIndex() const OVERRIDE {
61 std::vector<base::string16> items_;
63 DISALLOW_COPY_AND_ASSIGN(TranslateDenialComboboxModel);
66 const CGFloat kWindowWidth = 320;
68 // Padding between the window frame and content.
69 const CGFloat kFramePadding = 16;
71 const CGFloat kRelatedControlHorizontalSpacing = -2;
73 const CGFloat kRelatedControlVerticalSpacing = 4;
74 const CGFloat kUnrelatedControlVerticalSpacing = 20;
76 const CGFloat kContentWidth = kWindowWidth - 2 * kFramePadding;
78 @interface TranslateBubbleController()
80 - (void)performLayout;
81 - (NSView*)newBeforeTranslateView;
82 - (NSView*)newTranslatingView;
83 - (NSView*)newAfterTranslateView;
84 - (NSView*)newErrorView;
85 - (NSView*)newAdvancedView;
86 - (void)updateAdvancedView;
87 - (NSTextField*)addText:(NSString*)text
89 - (NSButton*)addLinkButtonWithText:(NSString*)text
92 - (NSButton*)addButton:(NSString*)title
95 - (NSButton*)addCheckbox:(NSString*)title
97 - (NSPopUpButton*)addPopUpButton:(ui::ComboboxModel*)model
100 - (void)handleTranslateButtonPressed;
101 - (void)handleNopeButtonPressed;
102 - (void)handleDoneButtonPressed;
103 - (void)handleCancelButtonPressed;
104 - (void)handleShowOriginalButtonPressed;
105 - (void)handleAdvancedLinkButtonPressed;
106 - (void)handleDenialPopUpButtonNopeSelected;
107 - (void)handleDenialPopUpButtonNeverTranslateLanguageSelected;
108 - (void)handleDenialPopUpButtonNeverTranslateSiteSelected;
109 - (void)handleSourceLanguagePopUpButtonSelectedItemChanged:(id)sender;
110 - (void)handleTargetLanguagePopUpButtonSelectedItemChanged:(id)sender;
114 @implementation TranslateBubbleController
116 - (id)initWithParentWindow:(BrowserWindowController*)controller
117 model:(scoped_ptr<TranslateBubbleModel>)model
118 webContents:(content::WebContents*)webContents {
119 NSWindow* parentWindow = [controller window];
121 // Use an arbitrary size; it will be changed in performLayout.
122 NSRect contentRect = ui::kWindowSizeDeterminedLater;
123 base::scoped_nsobject<InfoBubbleWindow> window(
124 [[InfoBubbleWindow alloc] initWithContentRect:contentRect
125 styleMask:NSBorderlessWindowMask
126 backing:NSBackingStoreBuffered
129 if ((self = [super initWithWindow:window
130 parentWindow:parentWindow
131 anchoredAt:NSZeroPoint])) {
132 webContents_ = webContents;
133 model_ = model.Pass();
134 if (model_->GetViewState() !=
135 TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE) {
136 translateExecuted_ = YES;
140 @(TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE):
141 [self newBeforeTranslateView],
142 @(TranslateBubbleModel::VIEW_STATE_TRANSLATING):
143 [self newTranslatingView],
144 @(TranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE):
145 [self newAfterTranslateView],
146 @(TranslateBubbleModel::VIEW_STATE_ERROR):
148 @(TranslateBubbleModel::VIEW_STATE_ADVANCED):
149 [self newAdvancedView],
152 [self performLayout];
157 @synthesize webContents = webContents_;
159 - (NSView*)currentView {
160 NSNumber* key = @(model_->GetViewState());
161 NSView* view = [views_ objectForKey:key];
166 - (const TranslateBubbleModel*)model {
170 - (void)showWindow:(id)sender {
171 BrowserWindowController* controller = [[self parentWindow] windowController];
172 NSPoint anchorPoint = [[controller toolbarController] translateBubblePoint];
173 anchorPoint = [[self parentWindow] convertBaseToScreen:anchorPoint];
174 [self setAnchorPoint:anchorPoint];
175 [super showWindow:sender];
178 - (void)switchView:(TranslateBubbleModel::ViewState)viewState {
179 if (model_->GetViewState() == viewState)
182 model_->SetViewState(viewState);
183 [self performLayout];
186 - (void)switchToErrorView:(TranslateErrors::Type)errorType {
187 // FIXME: Implement this.
190 - (void)performLayout {
191 NSWindow* window = [self window];
192 [[window contentView] setSubviews:@[ [self currentView] ]];
194 CGFloat height = NSHeight([[self currentView] frame]) +
195 2 * kFramePadding + info_bubble::kBubbleArrowHeight;
197 NSRect windowFrame = [window contentRectForFrameRect:[[self window] frame]];
198 NSRect newWindowFrame = [window frameRectForContentRect:NSMakeRect(
199 NSMinX(windowFrame), NSMaxY(windowFrame) - height, kWindowWidth, height)];
200 [window setFrame:newWindowFrame
202 animate:[[self window] isVisible]];
205 - (NSView*)newBeforeTranslateView {
206 NSRect contentFrame = NSMakeRect(
211 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
214 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_BEFORE_TRANSLATE);
215 NSTextField* textLabel = [self addText:message
217 message = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ADVANCED);
218 NSButton* advancedLinkButton =
219 [self addLinkButtonWithText:message
220 action:@selector(handleAdvancedLinkButtonPressed)
224 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ACCEPT);
225 NSButton* translateButton =
226 [self addButton:title
227 action:@selector(handleTranslateButtonPressed)
230 base::string16 originalLanguageName =
231 model_->GetLanguageNameAt(model_->GetOriginalLanguageIndex());
232 // TODO(hajimehoshi): When TranslateDenialComboboxModel is factored out as a
233 // common model, ui::MenuModel will be used here.
234 translateDenialComboboxModel_.reset(
235 new TranslateDenialComboboxModel(originalLanguageName));
236 NSPopUpButton* denyPopUpButton =
237 [self addPopUpButton:translateDenialComboboxModel_.get()
240 [denyPopUpButton setPullsDown:YES];
241 [[denyPopUpButton itemAtIndex:1] setTarget:self];
242 [[denyPopUpButton itemAtIndex:1]
243 setAction:@selector(handleDenialPopUpButtonNopeSelected)];
244 [[denyPopUpButton itemAtIndex:2] setTarget:self];
245 [[denyPopUpButton itemAtIndex:2]
246 setAction:@selector(handleDenialPopUpButtonNeverTranslateLanguageSelected)];
247 [[denyPopUpButton itemAtIndex:3] setTarget:self];
248 [[denyPopUpButton itemAtIndex:3]
249 setAction:@selector(handleDenialPopUpButtonNeverTranslateSiteSelected)];
251 title = base::SysUTF16ToNSString(
252 l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_DENY));
253 [[denyPopUpButton itemAtIndex:0] setTitle:title];
255 // Adjust width for the first item.
256 base::scoped_nsobject<NSMenu> originalMenu([[denyPopUpButton menu] copy]);
257 [denyPopUpButton removeAllItems];
258 [denyPopUpButton addItemWithTitle:[[originalMenu itemAtIndex:0] title]];
259 [denyPopUpButton sizeToFit];
260 [denyPopUpButton setMenu:originalMenu];
265 [translateButton setFrameOrigin:NSMakePoint(
266 kContentWidth - NSWidth([translateButton frame]), yPos)];
268 NSRect denyPopUpButtonFrame = [denyPopUpButton frame];
269 CGFloat diffY = [[denyPopUpButton cell]
270 titleRectForBounds:[denyPopUpButton bounds]].origin.y;
271 [denyPopUpButton setFrameOrigin:NSMakePoint(
272 NSMinX([translateButton frame]) - denyPopUpButtonFrame.size.width
273 - kRelatedControlHorizontalSpacing,
276 yPos += NSHeight([translateButton frame]) +
277 kUnrelatedControlVerticalSpacing;
279 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
280 [advancedLinkButton setFrameOrigin:NSMakePoint(
281 NSWidth([textLabel frame]), yPos)];
283 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
288 - (NSView*)newTranslatingView {
289 NSRect contentFrame = NSMakeRect(
294 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
297 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_TRANSLATING);
298 NSTextField* textLabel = [self addText:message
301 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_REVERT);
302 NSButton* showOriginalButton =
303 [self addButton:title
304 action:@selector(handleShowOriginalButtonPressed)
306 [showOriginalButton setEnabled:NO];
309 // TODO(hajimehoshi): Use l10n_util::VerticallyReflowGroup.
312 [showOriginalButton setFrameOrigin:NSMakePoint(
313 kContentWidth - NSWidth([showOriginalButton frame]), yPos)];
315 yPos += NSHeight([showOriginalButton frame]) +
316 kUnrelatedControlVerticalSpacing;
318 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
320 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
325 - (NSView*)newAfterTranslateView {
326 NSRect contentFrame = NSMakeRect(
331 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
334 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_TRANSLATED);
335 NSTextField* textLabel = [self addText:message
337 message = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ADVANCED);
338 NSButton* advancedLinkButton =
339 [self addLinkButtonWithText:message
340 action:@selector(handleAdvancedLinkButtonPressed)
343 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_REVERT);
344 NSButton* showOriginalButton =
345 [self addButton:title
346 action:@selector(handleShowOriginalButtonPressed)
352 [showOriginalButton setFrameOrigin:NSMakePoint(
353 kContentWidth - NSWidth([showOriginalButton frame]), yPos)];
355 yPos += NSHeight([showOriginalButton frame]) +
356 kUnrelatedControlVerticalSpacing;
358 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
359 [advancedLinkButton setFrameOrigin:NSMakePoint(
360 NSMaxX([textLabel frame]), yPos)];
362 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
367 - (NSView*)newErrorView {
368 NSRect contentFrame = NSMakeRect(
373 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
375 // TODO(hajimehoshi): Implement this.
380 - (NSView*)newAdvancedView {
381 NSRect contentFrame = NSMakeRect(
386 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
388 NSString* title = l10n_util::GetNSStringWithFixup(
389 IDS_TRANSLATE_BUBBLE_PAGE_LANGUAGE);
390 NSTextField* sourceLanguageLabel = [self addText:title
392 title = l10n_util::GetNSStringWithFixup(
393 IDS_TRANSLATE_BUBBLE_TRANSLATION_LANGUAGE);
394 NSTextField* targetLanguageLabel = [self addText:title
398 int sourceDefaultIndex = model_->GetOriginalLanguageIndex();
399 int targetDefaultIndex = model_->GetTargetLanguageIndex();
400 sourceLanguageComboboxModel_.reset(
401 new LanguageComboboxModel(sourceDefaultIndex, model_.get()));
402 targetLanguageComboboxModel_.reset(
403 new LanguageComboboxModel(targetDefaultIndex, model_.get()));
404 SEL action = @selector(handleSourceLanguagePopUpButtonSelectedItemChanged:);
405 NSPopUpButton* sourcePopUpButton =
406 [self addPopUpButton:sourceLanguageComboboxModel_.get()
409 action = @selector(handleTargetLanguagePopUpButtonSelectedItemChanged:);
410 NSPopUpButton* targetPopUpButton =
411 [self addPopUpButton:targetLanguageComboboxModel_.get()
415 // 'Always translate' checkbox
416 BOOL isIncognitoWindow = webContents_ ?
417 webContents_->GetBrowserContext()->IsOffTheRecord() : NO;
418 if (!isIncognitoWindow) {
420 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ALWAYS);
421 alwaysTranslateCheckbox_ = [self addCheckbox:title
426 advancedDoneButton_ =
427 [self addButton:l10n_util::GetNSStringWithFixup(IDS_DONE)
428 action:@selector(handleDoneButtonPressed)
430 advancedCancelButton_ =
431 [self addButton:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
432 action:@selector(handleCancelButtonPressed)
436 CGFloat textLabelWidth = NSWidth([sourceLanguageLabel frame]);
437 if (textLabelWidth < NSWidth([targetLanguageLabel frame]))
438 textLabelWidth = NSWidth([targetLanguageLabel frame]);
442 [advancedDoneButton_ setFrameOrigin:NSMakePoint(0, yPos)];
443 [advancedCancelButton_ setFrameOrigin:NSMakePoint(0, yPos)];
445 yPos += NSHeight([advancedDoneButton_ frame]) +
446 kUnrelatedControlVerticalSpacing;
448 if (alwaysTranslateCheckbox_) {
449 [alwaysTranslateCheckbox_ setFrameOrigin:NSMakePoint(textLabelWidth, yPos)];
451 yPos += NSHeight([alwaysTranslateCheckbox_ frame]) +
452 kRelatedControlVerticalSpacing;
455 CGFloat diffY = [[sourcePopUpButton cell]
456 titleRectForBounds:[sourcePopUpButton bounds]].origin.y;
458 [targetLanguageLabel setFrameOrigin:NSMakePoint(
459 textLabelWidth - NSWidth([targetLanguageLabel frame]), yPos + diffY)];
461 NSRect frame = [targetPopUpButton frame];
462 frame.origin = NSMakePoint(textLabelWidth, yPos);
463 frame.size.width = (kWindowWidth - 2 * kFramePadding) - textLabelWidth;
464 [targetPopUpButton setFrame:frame];
466 yPos += NSHeight([targetPopUpButton frame]) +
467 kRelatedControlVerticalSpacing;
469 [sourceLanguageLabel setFrameOrigin:NSMakePoint(
470 textLabelWidth - NSWidth([sourceLanguageLabel frame]), yPos + diffY)];
472 frame = [sourcePopUpButton frame];
473 frame.origin = NSMakePoint(textLabelWidth, yPos);
474 frame.size.width = NSWidth([targetPopUpButton frame]);
475 [sourcePopUpButton setFrame:frame];
477 [view setFrameSize:NSMakeSize(kContentWidth,
478 NSMaxY([sourcePopUpButton frame]))];
480 [self updateAdvancedView];
485 - (void)updateAdvancedView {
486 NSInteger state = model_->ShouldAlwaysTranslate() ? NSOnState : NSOffState;
487 [alwaysTranslateCheckbox_ setState:state];
490 if (model_->IsPageTranslatedInCurrentLanguages())
491 title = l10n_util::GetNSStringWithFixup(IDS_DONE);
493 title = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ACCEPT);
494 [advancedDoneButton_ setTitle:title];
495 [advancedDoneButton_ sizeToFit];
497 NSRect frame = [advancedDoneButton_ frame];
498 frame.origin.x = (kWindowWidth - 2 * kFramePadding) - NSWidth(frame);
499 [advancedDoneButton_ setFrameOrigin:frame.origin];
501 frame = [advancedCancelButton_ frame];
502 frame.origin.x = NSMinX([advancedDoneButton_ frame]) - NSWidth(frame)
503 - kRelatedControlHorizontalSpacing;
504 [advancedCancelButton_ setFrameOrigin:frame.origin];
507 - (NSTextField*)addText:(NSString*)text
508 toView:(NSView*)view {
509 base::scoped_nsobject<NSTextField> textField(
510 [[NSTextField alloc] initWithFrame:NSZeroRect]);
511 [textField setEditable:NO];
512 [textField setSelectable:YES];
513 [textField setDrawsBackground:NO];
514 [textField setBezeled:NO];
515 [textField setStringValue:text];
516 NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
517 [textField setFont:font];
518 [textField setAutoresizingMask:NSViewWidthSizable];
519 [view addSubview:textField.get()];
521 [textField sizeToFit];
522 return textField.get();
525 - (NSButton*)addLinkButtonWithText:(NSString*)text
527 toView:(NSView*)view {
528 base::scoped_nsobject<NSButton> button(
529 [[HyperlinkButtonCell buttonWithString:text] retain]);
531 [button setButtonType:NSMomentaryPushInButton];
532 [button setBezelStyle:NSRegularSquareBezelStyle];
533 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
535 [button setTarget:self];
536 [button setAction:action];
538 [view addSubview:button.get()];
543 - (NSButton*)addButton:(NSString*)title
545 toView:(NSView*)view {
546 base::scoped_nsobject<NSButton> button(
547 [[NSButton alloc] initWithFrame:NSZeroRect]);
548 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
549 [button setTitle:title];
550 [button setBezelStyle:NSRoundedBezelStyle];
551 [[button cell] setControlSize:NSSmallControlSize];
553 [button setTarget:self];
554 [button setAction:action];
556 [view addSubview:button.get()];
561 - (NSButton*)addCheckbox:(NSString*)title
562 toView:(NSView*)view {
563 base::scoped_nsobject<NSButton> button(
564 [[NSButton alloc] initWithFrame:NSZeroRect]);
565 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
566 [button setTitle:title];
567 [[button cell] setControlSize:NSSmallControlSize];
568 [button setButtonType:NSSwitchButton];
571 [view addSubview:button.get()];
576 - (NSPopUpButton*)addPopUpButton:(ui::ComboboxModel*)model
578 toView:(NSView*)view {
579 base::scoped_nsobject<NSPopUpButton> button(
580 [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]);
581 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
582 [button setBordered:YES];
583 [[button cell] setControlSize:NSSmallControlSize];
584 [button setTarget:self];
585 [button setAction:action];
587 for (int i = 0; i < model->GetItemCount(); ++i) {
588 if (model->IsItemSeparatorAt(i))
589 [[button menu] addItem:[NSMenuItem separatorItem]];
591 [button addItemWithTitle:base::SysUTF16ToNSString(model->GetItemAt(i))];
593 [button selectItemAtIndex:model->GetDefaultIndex()];
597 [view addSubview:button.get()];
602 - (void)handleTranslateButtonPressed {
603 translateExecuted_ = YES;
607 - (void)handleNopeButtonPressed {
611 - (void)handleDoneButtonPressed {
612 if (alwaysTranslateCheckbox_) {
613 model_->SetAlwaysTranslate(
614 [alwaysTranslateCheckbox_ state] == NSOnState);
616 if (model_->IsPageTranslatedInCurrentLanguages()) {
617 model_->GoBackFromAdvanced();
618 [self performLayout];
620 translateExecuted_ = true;
622 [self switchView:TranslateBubbleModel::VIEW_STATE_TRANSLATING];
626 - (void)handleCancelButtonPressed {
627 model_->GoBackFromAdvanced();
628 [self performLayout];
631 - (void)handleShowOriginalButtonPressed {
632 model_->RevertTranslation();
636 - (void)handleAdvancedLinkButtonPressed {
637 [self switchView:TranslateBubbleModel::VIEW_STATE_ADVANCED];
640 - (void)handleDenialPopUpButtonNopeSelected {
644 - (void)handleDenialPopUpButtonNeverTranslateLanguageSelected {
645 model_->SetNeverTranslateLanguage(true);
649 - (void)handleDenialPopUpButtonNeverTranslateSiteSelected {
650 model_->SetNeverTranslateSite(true);
654 - (void)handleSourceLanguagePopUpButtonSelectedItemChanged:(id)sender {
655 NSPopUpButton* button = base::mac::ObjCCastStrict<NSPopUpButton>(sender);
656 model_->UpdateOriginalLanguageIndex([button indexOfSelectedItem]);
657 [self updateAdvancedView];
660 - (void)handleTargetLanguagePopUpButtonSelectedItemChanged:(id)sender {
661 NSPopUpButton* button = base::mac::ObjCCastStrict<NSPopUpButton>(sender);
662 model_->UpdateTargetLanguageIndex([button indexOfSelectedItem]);
663 [self updateAdvancedView];