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 ~TranslateDenialComboboxModel() override {}
49 int GetItemCount() const override { return items_.size(); }
50 base::string16 GetItemAt(int index) override { return items_[index]; }
51 bool IsItemSeparatorAt(int index) override { return false; }
52 int GetDefaultIndex() const override { return 0; }
54 std::vector<base::string16> items_;
56 DISALLOW_COPY_AND_ASSIGN(TranslateDenialComboboxModel);
59 const CGFloat kWindowWidth = 320;
61 // Padding between the window frame and content.
62 const CGFloat kFramePadding = 16;
64 const CGFloat kRelatedControlHorizontalSpacing = -2;
66 const CGFloat kRelatedControlVerticalSpacing = 4;
67 const CGFloat kUnrelatedControlVerticalSpacing = 20;
69 const CGFloat kContentWidth = kWindowWidth - 2 * kFramePadding;
71 @interface TranslateBubbleController()
73 - (void)performLayout;
74 - (NSView*)newBeforeTranslateView;
75 - (NSView*)newTranslatingView;
76 - (NSView*)newAfterTranslateView;
77 - (NSView*)newErrorView;
78 - (NSView*)newAdvancedView;
79 - (void)updateAdvancedView;
80 - (NSTextField*)addText:(NSString*)text
82 - (NSButton*)addLinkButtonWithText:(NSString*)text
85 - (NSButton*)addButton:(NSString*)title
88 - (NSButton*)addCheckbox:(NSString*)title
90 - (NSPopUpButton*)addPopUpButton:(ui::ComboboxModel*)model
93 - (void)handleTranslateButtonPressed;
94 - (void)handleNopeButtonPressed;
95 - (void)handleDoneButtonPressed;
96 - (void)handleCancelButtonPressed;
97 - (void)handleShowOriginalButtonPressed;
98 - (void)handleAdvancedLinkButtonPressed;
99 - (void)handleDenialPopUpButtonNopeSelected;
100 - (void)handleDenialPopUpButtonNeverTranslateLanguageSelected;
101 - (void)handleDenialPopUpButtonNeverTranslateSiteSelected;
102 - (void)handleSourceLanguagePopUpButtonSelectedItemChanged:(id)sender;
103 - (void)handleTargetLanguagePopUpButtonSelectedItemChanged:(id)sender;
107 @implementation TranslateBubbleController
109 - (id)initWithParentWindow:(BrowserWindowController*)controller
110 model:(scoped_ptr<TranslateBubbleModel>)model
111 webContents:(content::WebContents*)webContents {
112 NSWindow* parentWindow = [controller window];
114 // Use an arbitrary size; it will be changed in performLayout.
115 NSRect contentRect = ui::kWindowSizeDeterminedLater;
116 base::scoped_nsobject<InfoBubbleWindow> window(
117 [[InfoBubbleWindow alloc] initWithContentRect:contentRect
118 styleMask:NSBorderlessWindowMask
119 backing:NSBackingStoreBuffered
122 if ((self = [super initWithWindow:window
123 parentWindow:parentWindow
124 anchoredAt:NSZeroPoint])) {
125 webContents_ = webContents;
126 model_ = model.Pass();
127 if (model_->GetViewState() !=
128 TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE) {
129 translateExecuted_ = YES;
133 @(TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE):
134 [self newBeforeTranslateView],
135 @(TranslateBubbleModel::VIEW_STATE_TRANSLATING):
136 [self newTranslatingView],
137 @(TranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE):
138 [self newAfterTranslateView],
139 @(TranslateBubbleModel::VIEW_STATE_ERROR):
141 @(TranslateBubbleModel::VIEW_STATE_ADVANCED):
142 [self newAdvancedView],
145 [self performLayout];
150 @synthesize webContents = webContents_;
152 - (NSView*)currentView {
153 NSNumber* key = @(model_->GetViewState());
154 NSView* view = [views_ objectForKey:key];
159 - (const TranslateBubbleModel*)model {
163 - (void)showWindow:(id)sender {
164 BrowserWindowController* controller = [[self parentWindow] windowController];
165 NSPoint anchorPoint = [[controller toolbarController] translateBubblePoint];
166 anchorPoint = [[self parentWindow] convertBaseToScreen:anchorPoint];
167 [self setAnchorPoint:anchorPoint];
168 [super showWindow:sender];
171 - (void)switchView:(TranslateBubbleModel::ViewState)viewState {
172 if (model_->GetViewState() == viewState)
175 model_->SetViewState(viewState);
176 [self performLayout];
179 - (void)switchToErrorView:(translate::TranslateErrors::Type)errorType {
180 // FIXME: Implement this.
183 - (void)performLayout {
184 NSWindow* window = [self window];
185 [[window contentView] setSubviews:@[ [self currentView] ]];
187 CGFloat height = NSHeight([[self currentView] frame]) +
188 2 * kFramePadding + info_bubble::kBubbleArrowHeight;
190 NSRect windowFrame = [window contentRectForFrameRect:[[self window] frame]];
191 NSRect newWindowFrame = [window frameRectForContentRect:NSMakeRect(
192 NSMinX(windowFrame), NSMaxY(windowFrame) - height, kWindowWidth, height)];
193 [window setFrame:newWindowFrame
195 animate:[[self window] isVisible]];
198 - (NSView*)newBeforeTranslateView {
199 NSRect contentFrame = NSMakeRect(
204 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
207 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_BEFORE_TRANSLATE);
208 NSTextField* textLabel = [self addText:message
210 message = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ADVANCED);
211 NSButton* advancedLinkButton =
212 [self addLinkButtonWithText:message
213 action:@selector(handleAdvancedLinkButtonPressed)
217 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ACCEPT);
218 NSButton* translateButton =
219 [self addButton:title
220 action:@selector(handleTranslateButtonPressed)
222 [translateButton setKeyEquivalent:@"\r"];
224 base::string16 originalLanguageName =
225 model_->GetLanguageNameAt(model_->GetOriginalLanguageIndex());
226 // TODO(hajimehoshi): When TranslateDenialComboboxModel is factored out as a
227 // common model, ui::MenuModel will be used here.
228 translateDenialComboboxModel_.reset(
229 new TranslateDenialComboboxModel(originalLanguageName));
230 NSPopUpButton* denyPopUpButton =
231 [self addPopUpButton:translateDenialComboboxModel_.get()
234 [denyPopUpButton setPullsDown:YES];
235 [[denyPopUpButton itemAtIndex:1] setTarget:self];
236 [[denyPopUpButton itemAtIndex:1]
237 setAction:@selector(handleDenialPopUpButtonNopeSelected)];
238 [[denyPopUpButton itemAtIndex:2] setTarget:self];
239 [[denyPopUpButton itemAtIndex:2]
240 setAction:@selector(handleDenialPopUpButtonNeverTranslateLanguageSelected)];
241 [[denyPopUpButton itemAtIndex:3] setTarget:self];
242 [[denyPopUpButton itemAtIndex:3]
243 setAction:@selector(handleDenialPopUpButtonNeverTranslateSiteSelected)];
245 title = base::SysUTF16ToNSString(
246 l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_DENY));
247 [[denyPopUpButton itemAtIndex:0] setTitle:title];
249 // Adjust width for the first item.
250 base::scoped_nsobject<NSMenu> originalMenu([[denyPopUpButton menu] copy]);
251 [denyPopUpButton removeAllItems];
252 [denyPopUpButton addItemWithTitle:[[originalMenu itemAtIndex:0] title]];
253 [denyPopUpButton sizeToFit];
254 [denyPopUpButton setMenu:originalMenu];
259 [translateButton setFrameOrigin:NSMakePoint(
260 kContentWidth - NSWidth([translateButton frame]), yPos)];
262 NSRect denyPopUpButtonFrame = [denyPopUpButton frame];
263 CGFloat diffY = [[denyPopUpButton cell]
264 titleRectForBounds:[denyPopUpButton bounds]].origin.y;
265 [denyPopUpButton setFrameOrigin:NSMakePoint(
266 NSMinX([translateButton frame]) - denyPopUpButtonFrame.size.width
267 - kRelatedControlHorizontalSpacing,
270 yPos += NSHeight([translateButton frame]) +
271 kUnrelatedControlVerticalSpacing;
273 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
274 [advancedLinkButton setFrameOrigin:NSMakePoint(
275 NSWidth([textLabel frame]), yPos)];
277 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
282 - (NSView*)newTranslatingView {
283 NSRect contentFrame = NSMakeRect(
288 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
291 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_TRANSLATING);
292 NSTextField* textLabel = [self addText:message
295 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_REVERT);
296 NSButton* showOriginalButton =
297 [self addButton:title
298 action:@selector(handleShowOriginalButtonPressed)
300 [showOriginalButton setEnabled:NO];
303 // TODO(hajimehoshi): Use l10n_util::VerticallyReflowGroup.
306 [showOriginalButton setFrameOrigin:NSMakePoint(
307 kContentWidth - NSWidth([showOriginalButton frame]), yPos)];
309 yPos += NSHeight([showOriginalButton frame]) +
310 kUnrelatedControlVerticalSpacing;
312 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
314 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
319 - (NSView*)newAfterTranslateView {
320 NSRect contentFrame = NSMakeRect(
325 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
328 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_TRANSLATED);
329 NSTextField* textLabel = [self addText:message
331 message = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ADVANCED);
332 NSButton* advancedLinkButton =
333 [self addLinkButtonWithText:message
334 action:@selector(handleAdvancedLinkButtonPressed)
337 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_REVERT);
338 NSButton* showOriginalButton =
339 [self addButton:title
340 action:@selector(handleShowOriginalButtonPressed)
346 [showOriginalButton setFrameOrigin:NSMakePoint(
347 kContentWidth - NSWidth([showOriginalButton frame]), yPos)];
349 yPos += NSHeight([showOriginalButton frame]) +
350 kUnrelatedControlVerticalSpacing;
352 [textLabel setFrameOrigin:NSMakePoint(0, yPos)];
353 [advancedLinkButton setFrameOrigin:NSMakePoint(
354 NSMaxX([textLabel frame]), yPos)];
356 [view setFrameSize:NSMakeSize(kContentWidth, NSMaxY([textLabel frame]))];
361 - (NSView*)newErrorView {
362 NSRect contentFrame = NSMakeRect(
367 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
369 // TODO(hajimehoshi): Implement this.
374 - (NSView*)newAdvancedView {
375 NSRect contentFrame = NSMakeRect(
380 NSView* view = [[NSView alloc] initWithFrame:contentFrame];
382 NSString* title = l10n_util::GetNSStringWithFixup(
383 IDS_TRANSLATE_BUBBLE_PAGE_LANGUAGE);
384 NSTextField* sourceLanguageLabel = [self addText:title
386 title = l10n_util::GetNSStringWithFixup(
387 IDS_TRANSLATE_BUBBLE_TRANSLATION_LANGUAGE);
388 NSTextField* targetLanguageLabel = [self addText:title
392 int sourceDefaultIndex = model_->GetOriginalLanguageIndex();
393 int targetDefaultIndex = model_->GetTargetLanguageIndex();
394 sourceLanguageComboboxModel_.reset(
395 new LanguageComboboxModel(sourceDefaultIndex, model_.get()));
396 targetLanguageComboboxModel_.reset(
397 new LanguageComboboxModel(targetDefaultIndex, model_.get()));
398 SEL action = @selector(handleSourceLanguagePopUpButtonSelectedItemChanged:);
399 NSPopUpButton* sourcePopUpButton =
400 [self addPopUpButton:sourceLanguageComboboxModel_.get()
403 action = @selector(handleTargetLanguagePopUpButtonSelectedItemChanged:);
404 NSPopUpButton* targetPopUpButton =
405 [self addPopUpButton:targetLanguageComboboxModel_.get()
409 // 'Always translate' checkbox
410 BOOL isIncognitoWindow = webContents_ ?
411 webContents_->GetBrowserContext()->IsOffTheRecord() : NO;
412 if (!isIncognitoWindow) {
414 l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ALWAYS);
415 alwaysTranslateCheckbox_ = [self addCheckbox:title
420 advancedDoneButton_ =
421 [self addButton:l10n_util::GetNSStringWithFixup(IDS_DONE)
422 action:@selector(handleDoneButtonPressed)
424 advancedCancelButton_ =
425 [self addButton:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
426 action:@selector(handleCancelButtonPressed)
430 CGFloat textLabelWidth = NSWidth([sourceLanguageLabel frame]);
431 if (textLabelWidth < NSWidth([targetLanguageLabel frame]))
432 textLabelWidth = NSWidth([targetLanguageLabel frame]);
436 [advancedDoneButton_ setFrameOrigin:NSMakePoint(0, yPos)];
437 [advancedCancelButton_ setFrameOrigin:NSMakePoint(0, yPos)];
439 yPos += NSHeight([advancedDoneButton_ frame]) +
440 kUnrelatedControlVerticalSpacing;
442 if (alwaysTranslateCheckbox_) {
443 [alwaysTranslateCheckbox_ setFrameOrigin:NSMakePoint(textLabelWidth, yPos)];
445 yPos += NSHeight([alwaysTranslateCheckbox_ frame]) +
446 kRelatedControlVerticalSpacing;
449 CGFloat diffY = [[sourcePopUpButton cell]
450 titleRectForBounds:[sourcePopUpButton bounds]].origin.y;
452 [targetLanguageLabel setFrameOrigin:NSMakePoint(
453 textLabelWidth - NSWidth([targetLanguageLabel frame]), yPos + diffY)];
455 NSRect frame = [targetPopUpButton frame];
456 frame.origin = NSMakePoint(textLabelWidth, yPos);
457 frame.size.width = (kWindowWidth - 2 * kFramePadding) - textLabelWidth;
458 [targetPopUpButton setFrame:frame];
460 yPos += NSHeight([targetPopUpButton frame]) +
461 kRelatedControlVerticalSpacing;
463 [sourceLanguageLabel setFrameOrigin:NSMakePoint(
464 textLabelWidth - NSWidth([sourceLanguageLabel frame]), yPos + diffY)];
466 frame = [sourcePopUpButton frame];
467 frame.origin = NSMakePoint(textLabelWidth, yPos);
468 frame.size.width = NSWidth([targetPopUpButton frame]);
469 [sourcePopUpButton setFrame:frame];
471 [view setFrameSize:NSMakeSize(kContentWidth,
472 NSMaxY([sourcePopUpButton frame]))];
474 [self updateAdvancedView];
479 - (void)updateAdvancedView {
480 NSInteger state = model_->ShouldAlwaysTranslate() ? NSOnState : NSOffState;
481 [alwaysTranslateCheckbox_ setState:state];
484 if (model_->IsPageTranslatedInCurrentLanguages())
485 title = l10n_util::GetNSStringWithFixup(IDS_DONE);
487 title = l10n_util::GetNSStringWithFixup(IDS_TRANSLATE_BUBBLE_ACCEPT);
488 [advancedDoneButton_ setTitle:title];
489 [advancedDoneButton_ sizeToFit];
491 NSRect frame = [advancedDoneButton_ frame];
492 frame.origin.x = (kWindowWidth - 2 * kFramePadding) - NSWidth(frame);
493 [advancedDoneButton_ setFrameOrigin:frame.origin];
495 frame = [advancedCancelButton_ frame];
496 frame.origin.x = NSMinX([advancedDoneButton_ frame]) - NSWidth(frame)
497 - kRelatedControlHorizontalSpacing;
498 [advancedCancelButton_ setFrameOrigin:frame.origin];
501 - (NSTextField*)addText:(NSString*)text
502 toView:(NSView*)view {
503 base::scoped_nsobject<NSTextField> textField(
504 [[NSTextField alloc] initWithFrame:NSZeroRect]);
505 [textField setEditable:NO];
506 [textField setSelectable:YES];
507 [textField setDrawsBackground:NO];
508 [textField setBezeled:NO];
509 [textField setStringValue:text];
510 NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
511 [textField setFont:font];
512 [textField setAutoresizingMask:NSViewWidthSizable];
513 [view addSubview:textField.get()];
515 [textField sizeToFit];
516 return textField.get();
519 - (NSButton*)addLinkButtonWithText:(NSString*)text
521 toView:(NSView*)view {
522 base::scoped_nsobject<NSButton> button(
523 [[HyperlinkButtonCell buttonWithString:text] retain]);
525 [button setButtonType:NSMomentaryPushInButton];
526 [button setBezelStyle:NSRegularSquareBezelStyle];
527 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
529 [button setTarget:self];
530 [button setAction:action];
532 [view addSubview:button.get()];
537 - (NSButton*)addButton:(NSString*)title
539 toView:(NSView*)view {
540 base::scoped_nsobject<NSButton> button(
541 [[NSButton alloc] initWithFrame:NSZeroRect]);
542 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
543 [button setTitle:title];
544 [button setBezelStyle:NSRoundedBezelStyle];
545 [[button cell] setControlSize:NSSmallControlSize];
547 [button setTarget:self];
548 [button setAction:action];
550 [view addSubview:button.get()];
555 - (NSButton*)addCheckbox:(NSString*)title
556 toView:(NSView*)view {
557 base::scoped_nsobject<NSButton> button(
558 [[NSButton alloc] initWithFrame:NSZeroRect]);
559 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
560 [button setTitle:title];
561 [[button cell] setControlSize:NSSmallControlSize];
562 [button setButtonType:NSSwitchButton];
565 [view addSubview:button.get()];
570 - (NSPopUpButton*)addPopUpButton:(ui::ComboboxModel*)model
572 toView:(NSView*)view {
573 base::scoped_nsobject<NSPopUpButton> button(
574 [[BubbleCombobox alloc] initWithFrame:NSZeroRect
577 [button setTarget:self];
578 [button setAction:action];
580 [view addSubview:button.get()];
584 - (void)handleTranslateButtonPressed {
585 translateExecuted_ = YES;
589 - (void)handleNopeButtonPressed {
593 - (void)handleDoneButtonPressed {
594 if (alwaysTranslateCheckbox_) {
595 model_->SetAlwaysTranslate(
596 [alwaysTranslateCheckbox_ state] == NSOnState);
598 if (model_->IsPageTranslatedInCurrentLanguages()) {
599 model_->GoBackFromAdvanced();
600 [self performLayout];
602 translateExecuted_ = true;
604 [self switchView:TranslateBubbleModel::VIEW_STATE_TRANSLATING];
608 - (void)handleCancelButtonPressed {
609 model_->GoBackFromAdvanced();
610 [self performLayout];
613 - (void)handleShowOriginalButtonPressed {
614 model_->RevertTranslation();
618 - (void)handleAdvancedLinkButtonPressed {
619 [self switchView:TranslateBubbleModel::VIEW_STATE_ADVANCED];
622 - (void)handleDenialPopUpButtonNopeSelected {
626 - (void)handleDenialPopUpButtonNeverTranslateLanguageSelected {
627 model_->SetNeverTranslateLanguage(true);
631 - (void)handleDenialPopUpButtonNeverTranslateSiteSelected {
632 model_->SetNeverTranslateSite(true);
636 - (void)handleSourceLanguagePopUpButtonSelectedItemChanged:(id)sender {
637 NSPopUpButton* button = base::mac::ObjCCastStrict<NSPopUpButton>(sender);
638 model_->UpdateOriginalLanguageIndex([button indexOfSelectedItem]);
639 [self updateAdvancedView];
642 - (void)handleTargetLanguagePopUpButtonSelectedItemChanged:(id)sender {
643 NSPopUpButton* button = base::mac::ObjCCastStrict<NSPopUpButton>(sender);
644 model_->UpdateTargetLanguageIndex([button indexOfSelectedItem]);
645 [self updateAdvancedView];