// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/web_intent_sheet_controller.h" #include "base/memory/scoped_nsobject.h" #include "base/sys_string_conversions.h" #include "base/utf_string_conversions.h" #include "chrome/browser/ui/browser_list.h" #import "chrome/browser/ui/cocoa/event_utils.h" #import "chrome/browser/ui/cocoa/hover_close_button.h" #import "chrome/browser/ui/cocoa/hyperlink_button_cell.h" #import "chrome/browser/ui/cocoa/info_bubble_view.h" #import "chrome/browser/ui/cocoa/info_bubble_window.h" #include "chrome/browser/ui/cocoa/web_intent_picker_cocoa.h" #include "chrome/browser/ui/constrained_window.h" #include "chrome/browser/ui/constrained_window_constants.h" #include "chrome/browser/ui/intents/web_intent_picker_delegate.h" #include "chrome/browser/ui/intents/web_intent_picker_model.h" #import "chrome/browser/ui/cocoa/tabs/throbber_view.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_view.h" #include "grit/generated_resources.h" #include "grit/google_chrome_strings.h" #include "grit/locale_settings.h" #include "grit/theme_resources.h" #include "grit/ui_resources.h" #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/text/text_elider.h" #include "ui/gfx/font.h" #include "ui/gfx/image/image.h" using content::OpenURLParams; using content::Referrer; @interface HyperlinkButtonCell (Private) - (void)customizeButtonCell; @end @interface CustomLinkButtonCell : HyperlinkButtonCell @end namespace { // The width of a service button, in view coordinates. const CGFloat kServiceButtonWidth = 300; // Spacing in between sections. const CGFloat kVerticalSpacing = 18; // Square size of the close button. const CGFloat kCloseButtonSize = 16; // Width of the text fields. const CGFloat kTextWidth = WebIntentPicker::kWindowMinWidth - (WebIntentPicker::kContentAreaBorder * 2.0 + kCloseButtonSize); // Maximum number of intents (suggested and installed) displayed. const int kMaxIntentRows = 4; // Sets properties on the given |field| to act as title or description labels. void ConfigureTextFieldAsLabel(NSTextField* field) { [field setEditable:NO]; [field setSelectable:YES]; [field setDrawsBackground:NO]; [field setBezeled:NO]; } NSButton* CreateHyperlinkButton(NSString* title, const NSRect& frame) { NSButton* button = [[NSButton alloc] initWithFrame:frame]; scoped_nsobject cell( [[CustomLinkButtonCell alloc] initTextCell:title]); [cell setControlSize:NSSmallControlSize]; [button setCell:cell.get()]; [button setButtonType:NSMomentaryPushInButton]; [button setBezelStyle:NSRegularSquareBezelStyle]; return button; } } // namespace // Provide custom link format for intent picker. Removes underline attribute, // since UX direction is "look like WebUI". @implementation CustomLinkButtonCell - (void)customizeButtonCell { [super customizeButtonCell]; [self setTextColor:[NSColor colorWithDeviceRed:0x11/255.0 green:0x55/255.0 blue:0xcc/255.0 alpha:0xff/255.0]]; } - (NSDictionary*)linkAttributes { scoped_nsobject paragraphStyle( [[NSParagraphStyle defaultParagraphStyle] mutableCopy]); [paragraphStyle setAlignment:[self alignment]]; return @{ NSForegroundColorAttributeName: [self textColor], NSFontAttributeName: [self font], NSCursorAttributeName: [NSCursor pointingHandCursor], NSParagraphStyleAttributeName: paragraphStyle.get() }; } @end // This simple NSView subclass is used as the single subview of the page info // bubble's window's contentView. Drawing is flipped so that layout of the // sections is easier. Apple recommends flipping the coordinate origin when // doing a lot of text layout because it's more natural. @interface WebIntentsContentView : NSView @end @implementation WebIntentsContentView - (BOOL)isFlipped { return YES; } - (void)drawRect:(NSRect)rect { [[NSColor colorWithCalibratedWhite:1.0 alpha:1.0] set]; NSRectFill(rect); } @end // NSImageView subclassed to allow fading the alpha value of the image to // indicate an inactive/disabled extension. @interface DimmableImageView : NSImageView { @private CGFloat alpha; } - (void)setEnabled:(BOOL)enabled; // NSView override - (void)drawRect:(NSRect)rect; @end @implementation DimmableImageView - (void)drawRect:(NSRect)rect { NSImage* image = [self image]; NSRect sourceRect, destinationRect; sourceRect.origin = NSZeroPoint; sourceRect.size = [image size]; destinationRect.origin = NSZeroPoint; destinationRect.size = [self frame].size; // If the source image is smaller than the destination, center it. if (destinationRect.size.width > sourceRect.size.width) { destinationRect.origin.x = (destinationRect.size.width - sourceRect.size.width) / 2.0; destinationRect.size.width = sourceRect.size.width; } if (destinationRect.size.height > sourceRect.size.height) { destinationRect.origin.y = (destinationRect.size.height - sourceRect.size.height) / 2.0; destinationRect.size.height = sourceRect.size.height; } [image drawInRect:destinationRect fromRect:sourceRect operation:NSCompositeSourceOver fraction:alpha]; } - (void)setEnabled:(BOOL)enabled { if (enabled) alpha = 1.0; else alpha = 0.5; [self setNeedsDisplay:YES]; } @end @interface WaitingView : NSView { @private scoped_nsobject text_; scoped_nsobject throbber_; } @end @implementation WaitingView - (id)init { NSRect frame = NSMakeRect(WebIntentPicker::kContentAreaBorder, 0, kTextWidth, 140); if (self = [super initWithFrame:frame]) { const CGFloat kTopMargin = 35.0; const CGFloat kBottomMargin = 25.0; const CGFloat kVerticalSpacing = 18.0; ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); frame.origin = NSMakePoint(WebIntentPicker::kContentAreaBorder, kBottomMargin); frame.size.width = kTextWidth; text_.reset([[NSTextField alloc] initWithFrame:frame]); ConfigureTextFieldAsLabel(text_); [text_ setAlignment:NSCenterTextAlignment]; [text_ setFont:[NSFont boldSystemFontOfSize:[NSFont systemFontSize]]]; [text_ setStringValue: l10n_util::GetNSStringWithFixup(IDS_INTENT_PICKER_WAIT_FOR_CWS)]; frame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: text_]; [text_ setFrame:frame]; frame.origin.y += NSHeight(frame) + kVerticalSpacing; // The sprite image consists of all the animation frames put together in one // horizontal/wide image. Each animation frame is square in shape within the // sprite. NSImage* iconImage = rb.GetNativeImageNamed(IDR_SPEECH_INPUT_SPINNER).ToNSImage(); frame.size = [iconImage size]; frame.size.width = NSHeight(frame); frame.origin.x = (WebIntentPicker::kWindowMinWidth - NSWidth(frame))/2.0; throbber_.reset([ThrobberView filmstripThrobberViewWithFrame:frame image:iconImage]); frame.size = NSMakeSize(WebIntentPicker::kWindowMinWidth, NSMaxY(frame) + kTopMargin); frame.origin = NSMakePoint(0, 0); [self setSubviews:@[throbber_, text_]]; [self setFrame:frame]; } return self; } @end // An NSView subclass to display ratings stars. @interface RatingsView : NSView // Mark RatingsView as disabled/enabled. - (void)setEnabled:(BOOL)enabled; @end @implementation RatingsView - (void)setEnabled:(BOOL)enabled { for (DimmableImageView* imageView in [self subviews]) [imageView setEnabled:enabled]; } @end // NSView for the header of the box. @interface HeaderView : NSView { @private // Used to forward button clicks. Weak reference. scoped_nsobject titleField_; scoped_nsobject subtitleField_; scoped_nsobject spacer_; } - (id)init; @end @implementation HeaderView - (id)init { NSRect contentFrame = NSMakeRect(0, 0, WebIntentPicker::kWindowMinWidth, 1); if (self = [super initWithFrame:contentFrame]) { NSRect frame = NSMakeRect(WebIntentPicker::kContentAreaBorder, 0, kTextWidth, 1); ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); titleField_.reset([[NSTextField alloc] initWithFrame:frame]); ConfigureTextFieldAsLabel(titleField_); gfx::Font titleFont = rb.GetFont( ConstrainedWindowConstants::kTitleFontStyle); titleFont = titleFont.DeriveFont(0, gfx::Font::BOLD); [titleField_ setFont:titleFont.GetNativeFont()]; frame = NSMakeRect(WebIntentPicker::kContentAreaBorder, 0, kTextWidth, 1); subtitleField_.reset([[NSTextField alloc] initWithFrame:frame]); ConfigureTextFieldAsLabel(subtitleField_); gfx::Font textFont = rb.GetFont(ConstrainedWindowConstants::kTextFontStyle); [subtitleField_ setFont:textFont.GetNativeFont()]; frame = NSMakeRect(0, 0, WebIntentPicker::kWindowMinWidth, 1.0); spacer_.reset([[NSBox alloc] initWithFrame:frame]); [spacer_ setBoxType:NSBoxSeparator]; [spacer_ setBorderColor:[NSColor blackColor]]; [spacer_ setAlphaValue:0.2]; NSArray* subviews = @[titleField_, subtitleField_, spacer_]; [self setSubviews:subviews]; } return self; } - (void)setTitle:(NSString*)title { NSRect frame = [titleField_ frame]; [titleField_ setStringValue:title]; frame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: titleField_]; [titleField_ setFrame:frame]; } - (void)setSubtitle:(NSString*)subtitle { if (subtitle && [subtitle length]) { NSRect frame = [subtitleField_ frame]; [subtitleField_ setHidden:FALSE]; [subtitleField_ setStringValue:subtitle]; frame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: subtitleField_]; [subtitleField_ setFrame:frame]; } else { [subtitleField_ setHidden:TRUE]; } } - (void)performLayout { CGFloat offset = kVerticalSpacing; NSRect frame = [spacer_ frame]; frame.origin.y = offset; [spacer_ setFrame:frame]; offset += NSHeight(frame); offset += kVerticalSpacing; if (![subtitleField_ isHidden]) { frame = [subtitleField_ frame]; frame.origin.y = offset; [subtitleField_ setFrame:frame]; offset += NSHeight(frame); } frame = [titleField_ frame]; frame.origin.y = offset; [titleField_ setFrame:frame]; offset += NSHeight(frame); // No kContentAreaBorder here, since that is currently handled elsewhere. frame = [self frame]; frame.size.height = offset; [self setFrame:frame]; } @end // NSView for a single row in the intents view. @interface IntentRowView : NSView { @private scoped_nsobject throbber_; scoped_nsobject cwsButton_; scoped_nsobject ratingsWidget_; scoped_nsobject installButton_; scoped_nsobject iconView_; scoped_nsobject label_; } - (id)initWithExtension: (const WebIntentPickerModel::SuggestedExtension*)extension withIndex:(size_t)index forController:(WebIntentPickerSheetController*)controller; - (void)startThrobber; - (void)setEnabled:(BOOL)enabled; - (void)stopThrobber; - (NSInteger)tag; @end @implementation IntentRowView const CGFloat kMaxHeight = 34.0; const CGFloat kTitleX = 20.0; const CGFloat kMinAddButtonHeight = 28.0; const CGFloat kAddButtonX = 245; const CGFloat kAddButtonWidth = 128.0; + (RatingsView*)createStarWidgetWithRating:(CGFloat)rating { const int kStarSpacing = 1; // Spacing between stars in pixels. const CGFloat kStarSize = 16.0; // Size of the star in pixels. NSMutableArray* subviews = [NSMutableArray array]; ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); NSRect imageFrame = NSMakeRect(0, 0, kStarSize, kStarSize); for (int i = 0; i < 5; ++i) { NSImage* nsImage = rb.GetNativeImageNamed( WebIntentPicker::GetNthStarImageIdFromCWSRating(rating, i)).ToNSImage(); scoped_nsobject imageView( [[DimmableImageView alloc] initWithFrame:imageFrame]); [imageView setImage:nsImage]; [imageView setImageFrameStyle:NSImageFrameNone]; [imageView setFrame:imageFrame]; [imageView setEnabled:YES]; [subviews addObject:imageView]; imageFrame.origin.x += kStarSize + kStarSpacing; } NSRect frame = NSMakeRect(0, 0, (kStarSize + kStarSpacing) * 5, kStarSize); RatingsView* widget = [[RatingsView alloc] initWithFrame:frame]; [widget setSubviews:subviews]; return widget; } - (void)setIcon:(NSImage*)icon { NSRect imageFrame = NSZeroRect; iconView_.reset( [[DimmableImageView alloc] initWithFrame:imageFrame]); [iconView_ setImage:icon]; [iconView_ setImageFrameStyle:NSImageFrameNone]; [iconView_ setEnabled:YES]; imageFrame.size = [icon size]; imageFrame.size.height = std::min(NSHeight(imageFrame), kMaxHeight); imageFrame.origin.y += (kMaxHeight - NSHeight(imageFrame)) / 2.0; [iconView_ setFrame:imageFrame]; } - (void)setActionButton:(NSString*)title withSelector:(SEL)selector forController:(WebIntentPickerSheetController*)controller { NSRect frame = NSMakeRect(kAddButtonX, 0, kAddButtonWidth, 0); installButton_.reset([[NSButton alloc] initWithFrame:frame]); [installButton_ setAlignment:NSCenterTextAlignment]; [installButton_ setButtonType:NSMomentaryPushInButton]; [installButton_ setBezelStyle:NSRegularSquareBezelStyle]; [installButton_ setTitle:title]; frame.size.height = std::min(kMinAddButtonHeight, kMaxHeight); frame.origin.y += (kMaxHeight - NSHeight(frame)) / 2.0; [installButton_ setFrame:frame]; [installButton_ setTarget:controller]; [installButton_ setAction:selector]; } - (id)init { // Build the main view. NSRect contentFrame = NSMakeRect( 0, 0, WebIntentPicker::kWindowMinWidth, kMaxHeight); if (self = [super initWithFrame:contentFrame]) { NSMutableArray* subviews = [NSMutableArray array]; if (iconView_) [subviews addObject:iconView_]; if (label_) [subviews addObject:label_]; if (cwsButton_) [subviews addObject:cwsButton_]; if (ratingsWidget_) [subviews addObject:ratingsWidget_]; if (installButton_) [subviews addObject:installButton_]; if (throbber_) [subviews addObject:throbber_]; [self setSubviews:subviews]; } return self; } - (id)initWithExtension: (const WebIntentPickerModel::SuggestedExtension*)extension withIndex:(size_t)index forController:(WebIntentPickerSheetController*)controller { // Add the extension icon. [self setIcon:extension->icon.ToNSImage()]; // Add the extension title. NSRect frame = NSMakeRect(kTitleX, 0, 0, 0); const string16 elidedTitle = ui::ElideText( extension->title, gfx::Font(), WebIntentPicker::kTitleLinkMaxWidth, ui::ELIDE_AT_END); NSString* string = base::SysUTF16ToNSString(elidedTitle); cwsButton_.reset(CreateHyperlinkButton(string, frame)); [cwsButton_ setAlignment:NSCenterTextAlignment]; [cwsButton_ setTarget:controller]; [cwsButton_ setAction:@selector(openExtensionLink:)]; [cwsButton_ setTag:index]; [cwsButton_ sizeToFit]; frame = [cwsButton_ frame]; frame.size.height = std::min([[cwsButton_ cell] cellSize].height, kMaxHeight); frame.origin.y = (kMaxHeight - NSHeight(frame)) / 2.0; [cwsButton_ setFrame:frame]; // Add the star rating CGFloat offsetX = frame.origin.x + NSWidth(frame) + WebIntentPicker::kContentAreaBorder; ratingsWidget_.reset( [IntentRowView createStarWidgetWithRating:extension->average_rating]); frame = [ratingsWidget_ frame]; frame.origin.y += (kMaxHeight - NSHeight(frame)) / 2.0; frame.origin.x = offsetX; [ratingsWidget_ setFrame:frame]; // Add an "add to chromium" button. string = l10n_util::GetNSStringWithFixup( IDS_INTENT_PICKER_INSTALL_EXTENSION); [self setActionButton:string withSelector:@selector(installExtension:) forController:controller]; [installButton_ setTag:index]; // Keep a throbber handy. frame = [installButton_ frame]; frame.origin.x += (NSWidth(frame) - 16) / 2; frame.origin.y += (NSHeight(frame) - 16) /2; frame.size = NSMakeSize(16, 16); throbber_.reset([[NSProgressIndicator alloc] initWithFrame:frame]); [throbber_ setHidden:YES]; [throbber_ setStyle:NSProgressIndicatorSpinningStyle]; return [self init]; } - (id)initWithService: (const WebIntentPickerModel::InstalledService*)service withIndex:(size_t)index forController:(WebIntentPickerSheetController*)controller { // Add the extension icon. [self setIcon:service->favicon.ToNSImage()]; // Add the extension title. NSRect frame = NSMakeRect( kTitleX, 0, WebIntentPicker::kTitleLinkMaxWidth, 10); const string16 elidedTitle = ui::ElideText( service->title, gfx::Font(), WebIntentPicker::kTitleLinkMaxWidth, ui::ELIDE_AT_END); NSString* string = base::SysUTF16ToNSString(elidedTitle); label_.reset([[NSTextField alloc] initWithFrame:frame]); ConfigureTextFieldAsLabel(label_); [label_ setStringValue:string]; frame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: label_]; frame.origin.y = (kMaxHeight - NSHeight(frame)) / 2.0; [label_ setFrame:frame]; string = l10n_util::GetNSStringWithFixup( IDS_INTENT_PICKER_SELECT_INTENT); [self setActionButton:string withSelector:@selector(invokeService:) forController:controller]; [installButton_ setTag:index]; return [self init]; } - (NSInteger)tag { return [installButton_ tag]; } - (void)startThrobber { [installButton_ setHidden:YES]; [throbber_ setHidden:NO]; [throbber_ startAnimation:self]; [iconView_ setEnabled:YES]; [ratingsWidget_ setEnabled:NO]; } - (void)setEnabled:(BOOL) enabled { [installButton_ setEnabled:enabled]; [cwsButton_ setEnabled:enabled]; [iconView_ setEnabled:enabled]; [ratingsWidget_ setEnabled:enabled]; } - (void)stopThrobber { if (throbber_.get()) { [throbber_ setHidden:YES]; [throbber_ stopAnimation:self]; } [installButton_ setHidden:NO]; } - (void)adjustButtonSize:(CGFloat)newWidth { CGFloat increase = std::max(kAddButtonWidth, newWidth) - kAddButtonWidth; NSRect frame = [self frame]; frame.size.width += increase; [self setFrame:frame]; frame = [installButton_ frame]; frame.size.width += increase; [installButton_ setFrame:frame]; } @end @interface IntentView : NSView { @private // Used to forward button clicks. Weak reference. WebIntentPickerSheetController* controller_; } - (id)initWithModel:(WebIntentPickerModel*)model forController:(WebIntentPickerSheetController*)controller; - (void)startThrobberForRow:(NSInteger)index; - (void)stopThrobber; @end @implementation IntentView - (id)initWithModel:(WebIntentPickerModel*)model forController:(WebIntentPickerSheetController*)controller { if (self = [super initWithFrame:NSZeroRect]) { const CGFloat kYMargin = 16.0; int availableSpots = kMaxIntentRows; CGFloat offset = kYMargin; NSMutableArray* subviews = [NSMutableArray array]; NSMutableArray* rows = [NSMutableArray array]; for (size_t i = 0; i < model->GetInstalledServiceCount() && availableSpots; ++i, --availableSpots) { const WebIntentPickerModel::InstalledService& svc = model->GetInstalledServiceAt(i); scoped_nsobject suggestView( [[IntentRowView alloc] initWithService:&svc withIndex:i forController:controller]); [rows addObject:suggestView]; } // Special case for the empty view. size_t count = model->GetSuggestedExtensionCount(); if (count == 0 && availableSpots == kMaxIntentRows) return nil; for (size_t i = count; i > 0 && availableSpots; --i, --availableSpots) { const WebIntentPickerModel::SuggestedExtension& ext = model->GetSuggestedExtensionAt(i - 1); scoped_nsobject suggestView( [[IntentRowView alloc] initWithExtension:&ext withIndex:i-1 forController:controller]); [rows addObject:suggestView]; } // Determine optimal width for buttons given localized texts. scoped_nsobject sizeHelper( [[NSButton alloc] initWithFrame:NSZeroRect]); [sizeHelper setAlignment:NSCenterTextAlignment]; [sizeHelper setButtonType:NSMomentaryPushInButton]; [sizeHelper setBezelStyle:NSRegularSquareBezelStyle]; [sizeHelper setTitle: l10n_util::GetNSStringWithFixup(IDS_INTENT_PICKER_INSTALL_EXTENSION)]; [sizeHelper sizeToFit]; CGFloat buttonWidth = NSWidth([sizeHelper frame]); [sizeHelper setTitle: l10n_util::GetNSStringWithFixup(IDS_INTENT_PICKER_SELECT_INTENT)]; [sizeHelper sizeToFit]; buttonWidth = std::max(buttonWidth, NSWidth([sizeHelper frame])); for (IntentRowView* row in [rows reverseObjectEnumerator]) { [row adjustButtonSize:buttonWidth]; offset += [self addStackedView:row toSubviews:subviews atOffset:offset]; } [self setSubviews:subviews]; NSRect contentFrame = NSMakeRect(WebIntentPicker::kContentAreaBorder, 0, WebIntentPicker::kWindowMinWidth, offset); [self setFrame:contentFrame]; controller_ = controller; } return self; } - (void)startThrobberForRow:(NSInteger)index { for (IntentRowView* row in [self subviews]) { if ([row isMemberOfClass:[IntentRowView class]]) { [row setEnabled:NO]; if ([row tag] == index) { [row startThrobber]; } } } } - (void)stopThrobber { for (IntentRowView* row in [self subviews]) { if ([row isMemberOfClass:[IntentRowView class]]) { [row stopThrobber]; [row setEnabled:YES]; } } } - (IBAction)installExtension:(id)sender { [controller_ installExtension:sender]; } - (CGFloat)addStackedView:(NSView*)view toSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { if (view == nil) return 0.0; NSPoint frameOrigin = [view frame].origin; frameOrigin.y = offset; [view setFrameOrigin:frameOrigin]; [subviews addObject:view]; return NSHeight([view frame]); } @end @implementation WebIntentPickerSheetController - (id)initWithPicker:(WebIntentPickerCocoa*)picker { // Use an arbitrary height because it will reflect the size of the content. NSRect contentRect = NSMakeRect( 0, 0, WebIntentPicker::kWindowMinWidth, kVerticalSpacing); // |window| is retained by the ConstrainedWindowMacDelegateCustomSheet when // the sheet is initialized. scoped_nsobject window( [[NSWindow alloc] initWithContentRect:contentRect styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:YES]); if ((self = [super initWithWindow:window.get()])) { picker_ = picker; if (picker) model_ = picker->model(); inlineDispositionTitleField_.reset([[NSTextField alloc] init]); ConfigureTextFieldAsLabel(inlineDispositionTitleField_); [inlineDispositionTitleField_ setFont: [NSFont boldSystemFontOfSize:[NSFont systemFontSize]]]; flipView_.reset([[WebIntentsContentView alloc] init]); [flipView_ setAutoresizingMask:NSViewMinYMargin]; [[[self window] contentView] setSubviews:@[flipView_]]; [self performLayoutWithModel:model_]; } return self; } // Handle default OSX dialog cancel mechanisms. (Cmd-.) - (void)cancelOperation:(id)sender { if (picker_) picker_->OnCancelled(); [self closeSheet]; } - (void)chooseAnotherService:(id)sender { if (picker_) picker_->OnChooseAnotherService(); } - (void)sheetDidEnd:(NSWindow*)sheet returnCode:(int)returnCode contextInfo:(void*)contextInfo { if (picker_) picker_->OnSheetDidEnd(sheet); } - (void)setInlineDispositionWebContents:(content::WebContents*)webContents { webContents_ = webContents; } - (void)setInlineDispositionFrameSize:(NSSize)inlineContentSize { DCHECK(webContents_); NSView* webContentView = webContents_->GetNativeView(); // Make sure inline content size is never shrunk. inlineContentSize = NSMakeSize( std::max(NSWidth([webContentView frame]), inlineContentSize.width), std::max(NSHeight([webContentView frame]), inlineContentSize.height)); // Compute container size to fit all elements, including padding. NSSize containerSize = inlineContentSize; containerSize.height += [webContentView frame].origin.y + WebIntentPicker::kContentAreaBorder; containerSize.width += 2 * WebIntentPicker::kContentAreaBorder; // Ensure minimum container width. containerSize.width = std::max(CGFloat(WebIntentPicker::kWindowMinWidth), containerSize.width); // Resize web contents. [webContentView setFrameSize:inlineContentSize]; [self setContainerSize:containerSize]; } - (void)setContainerSize:(NSSize)containerSize { // Resize container views NSRect frame = NSMakeRect(0, 0, 0, 0); frame.size = containerSize; [[[self window] contentView] setFrame:frame]; [flipView_ setFrame:frame]; // Resize and reposition dialog window. frame.size = [[[self window] contentView] convertSize:containerSize toView:nil]; frame = [[self window] frameRectForContentRect:frame]; // Readjust window position to keep top in place and center horizontally. NSRect windowFrame = [[self window] frame]; windowFrame.origin.x -= (NSWidth(frame) - NSWidth(windowFrame)) / 2.0; windowFrame.origin.y -= (NSHeight(frame) - NSHeight(windowFrame)); windowFrame.size = frame.size; [[self window] setFrame:windowFrame display:YES animate:NO]; } // Pop up a new tab with the Chrome Web Store. - (IBAction)showChromeWebStore:(id)sender { DCHECK(picker_); picker_->OnSuggestionsLinkClicked( event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent])); } // A picker button has been pressed - invoke corresponding service. - (IBAction)invokeService:(id)sender { DCHECK(picker_); picker_->OnServiceChosen([sender tag]); } - (IBAction)openExtensionLink:(id)sender { DCHECK(model_); DCHECK(picker_); const WebIntentPickerModel::SuggestedExtension& extension = model_->GetSuggestedExtensionAt([sender tag]); picker_->OnExtensionLinkClicked(extension.id, event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent])); } - (IBAction)installExtension:(id)sender { DCHECK(model_); DCHECK(picker_); const WebIntentPickerModel::SuggestedExtension& extension = model_->GetSuggestedExtensionAt([sender tag]); if (picker_) { [intentView_ startThrobberForRow:[sender tag]]; [closeButton_ setEnabled:NO]; picker_->OnExtensionInstallRequested(extension.id); } } - (CGFloat)addStackedView:(NSView*)view toSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { if (view == nil) return 0.0; NSPoint frameOrigin = [view frame].origin; frameOrigin.y = offset; [view setFrameOrigin:frameOrigin]; [subviews addObject:view]; return NSHeight([view frame]); } // Adds a link to the Chrome Web Store, to obtain further intent handlers. // Returns the y position delta for the next offset. - (CGFloat)addCwsButtonToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); NSImage* iconImage = rb.GetNativeImageNamed(IDR_WEBSTORE_ICON_16).ToNSImage(); NSRect imageFrame; imageFrame.origin = NSMakePoint(WebIntentPicker::kContentAreaBorder, offset); imageFrame.size = [iconImage size]; scoped_nsobject iconView( [[NSImageView alloc] initWithFrame:imageFrame]); [iconView setImage:iconImage]; [iconView setImageFrameStyle:NSImageFrameNone]; const CGFloat kCWSIconPadding = 4.0; // Same spacing as for service options. NSRect frame = NSMakeRect(WebIntentPicker::kContentAreaBorder + NSWidth(imageFrame) + kCWSIconPadding, offset, 100, 10); NSString* string = l10n_util::GetNSStringWithFixup( IDS_FIND_MORE_INTENT_HANDLER_MESSAGE); scoped_nsobject button(CreateHyperlinkButton(string, frame)); [button setTarget:self]; [button setAction:@selector(showChromeWebStore:)]; [subviews addObjectsFromArray:@[iconView, button]]; // Call size-to-fit to fixup for the localized string. [GTMUILocalizerAndLayoutTweaker sizeToFitView:button]; return NSHeight([button frame]); } - (void)addCloseButtonToSubviews:(NSMutableArray*)subviews { const CGFloat kButtonPadding = 4.0; // whitespace inside button frame. if (!closeButton_.get()) { NSRect buttonFrame = NSMakeRect( WebIntentPicker::kContentAreaBorder + kTextWidth + kButtonPadding, WebIntentPicker::kContentAreaBorder - kButtonPadding, kCloseButtonSize, kCloseButtonSize); closeButton_.reset( [[HoverCloseButton alloc] initWithFrame:buttonFrame]); // Anchor close button to upper right. // (NSViewMaxYMargin since parent view is flipped.) [closeButton_ setAutoresizingMask:NSViewMaxYMargin|NSViewMinXMargin]; [closeButton_ setTarget:self]; [closeButton_ setAction:@selector(cancelOperation:)]; [[closeButton_ cell] setKeyEquivalent:@"\e"]; } [subviews addObject:closeButton_]; } // Adds a header (icon and explanatory text) to picker bubble. // Returns the y position delta for the next offset. - (CGFloat)addHeaderToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { // Create a new text field if we don't have one yet. // TODO(groby): This should not be necessary since the controller sends this // string. if (!actionTextField_.get()) { NSString* nsString = l10n_util::GetNSStringWithFixup(IDS_CHOOSE_INTENT_HANDLER_MESSAGE); [self setActionString:nsString]; } scoped_nsobject header([[HeaderView alloc] init]); [header setTitle:[actionTextField_ stringValue]]; string16 labelText; if (model_ && model_->GetInstalledServiceCount() == 0) labelText = model_->GetSuggestionsLinkText(); [header setSubtitle:base::SysUTF16ToNSString(labelText)]; [header performLayout]; NSRect frame = [header frame]; frame.origin.y = offset; [header setFrame:frame]; [subviews addObject:header]; return NSHeight(frame); } - (CGFloat)addInlineHtmlToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { if (!webContents_) return 0; // Determine a good size for the inline disposition window. gfx::Size size = picker_->GetMinInlineDispositionSize(); NSView* webContentView = webContents_->GetNativeView(); NSRect contentFrame = NSMakeRect( WebIntentPicker::kContentAreaBorder, offset, std::max(NSWidth([webContentView frame]),CGFloat(size.width())), std::max(NSHeight([webContentView frame]),CGFloat(size.height()))); [webContentView setFrame:contentFrame]; [subviews addObject:webContentView]; return NSHeight(contentFrame); } - (CGFloat)addAnotherServiceLinkToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { DCHECK(model_); DCHECK(model_->IsInlineDisposition()); GURL url = model_->inline_disposition_url(); const WebIntentPickerModel::InstalledService* service = model_->GetInstalledServiceWithURL(url); DCHECK(service); CGFloat originalOffset = offset; // Icon for current service. scoped_nsobject icon; NSRect imageFrame = NSMakeRect(WebIntentPicker::kContentAreaBorder, offset, 0, 0); icon.reset([[NSImageView alloc] initWithFrame:imageFrame]); [icon setImage:service->favicon.ToNSImage()]; [icon setImageFrameStyle:NSImageFrameNone]; [icon setEnabled:YES]; imageFrame.size = [service->favicon.ToNSImage() size]; [icon setFrame:imageFrame]; [subviews addObject:icon]; // Resize control to fit text NSRect textFrame = NSMakeRect(NSMaxX(imageFrame) + 4, offset, WebIntentPicker::kTitleLinkMaxWidth, 1); [inlineDispositionTitleField_ setFrame:textFrame]; [subviews addObject:inlineDispositionTitleField_]; [GTMUILocalizerAndLayoutTweaker sizeToFitView:inlineDispositionTitleField_]; textFrame = [inlineDispositionTitleField_ frame]; // Add link for "choose another service" if other suggestions are available // or if more than one (the current) service is installed. if (model_->show_use_another_service() && (model_->GetInstalledServiceCount() > 1 || model_->GetSuggestedExtensionCount())) { NSRect frame = NSMakeRect( NSMaxX(textFrame) + WebIntentPicker::kContentAreaBorder, offset, 1, 1); NSString* string = l10n_util::GetNSStringWithFixup( IDS_INTENT_PICKER_USE_ALTERNATE_SERVICE); scoped_nsobject button(CreateHyperlinkButton(string, frame)); [[button cell] setControlSize:NSRegularControlSize]; [[button cell] setFont: [NSFont controlContentFontOfSize:[NSFont systemFontSize]]]; [button setTarget:self]; [button setAction:@selector(chooseAnotherService:)]; [subviews addObject:button]; // Call size-to-fit to fixup for the localized string. [GTMUILocalizerAndLayoutTweaker sizeToFitView:button]; // Right-align the "use another service" button. frame = [button frame]; frame.origin.x = WebIntentPicker::kWindowMinWidth - NSWidth(frame) - 2 * WebIntentPicker::kContentAreaBorder - kCloseButtonSize; [button setFrame:frame]; [button setAutoresizingMask:NSViewMinXMargin]; // And finally, make sure the link and the title are horizontally centered. frame = [button frame]; CGFloat height = std::max(NSHeight(textFrame), NSHeight(frame)); frame.origin.y += (height - NSHeight(frame)) / 2.0; frame.size.height = height; textFrame.origin.y += (height - NSHeight(textFrame)) / 2.0; textFrame.size.height = height; [button setFrame:frame]; [inlineDispositionTitleField_ setFrame:textFrame]; } offset += NSHeight(textFrame) + kVerticalSpacing; scoped_nsobject spacer; NSRect frame = NSMakeRect(0, offset, WebIntentPicker::kWindowMinWidth, 1.0); spacer.reset([[NSBox alloc] initWithFrame:frame]); [spacer setBoxType:NSBoxSeparator]; [spacer setAlphaValue:0.2]; [spacer setAutoresizingMask:NSViewWidthSizable]; [subviews addObject: spacer]; return offset + kVerticalSpacing - originalOffset; } - (NSView*)createEmptyView { NSRect titleFrame = NSMakeRect(WebIntentPicker::kContentAreaBorder, WebIntentPicker::kContentAreaBorder, kTextWidth, 1); scoped_nsobject title( [[NSTextField alloc] initWithFrame:titleFrame]); ConfigureTextFieldAsLabel(title); ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); gfx::Font titleFont = rb.GetFont(ConstrainedWindowConstants::kTitleFontStyle); titleFont = titleFont.DeriveFont(0, gfx::Font::BOLD); [title setFont:titleFont.GetNativeFont()]; [title setStringValue: l10n_util::GetNSStringWithFixup(IDS_INTENT_PICKER_NO_SERVICES_TITLE)]; titleFrame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; NSRect bodyFrame = titleFrame; bodyFrame.origin.y += NSHeight(titleFrame) + WebIntentPicker::kContentAreaBorder; scoped_nsobject body( [[NSTextField alloc] initWithFrame:bodyFrame]); ConfigureTextFieldAsLabel(body); [body setStringValue: l10n_util::GetNSStringWithFixup(IDS_INTENT_PICKER_NO_SERVICES)]; bodyFrame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:body]; NSRect viewFrame = NSMakeRect( 0, WebIntentPicker::kContentAreaBorder, std::max(NSWidth(bodyFrame), NSWidth(titleFrame)) + 2 * WebIntentPicker::kContentAreaBorder, NSHeight(titleFrame) + NSHeight(bodyFrame) + kVerticalSpacing); titleFrame.origin.y = NSHeight(viewFrame) - NSHeight(titleFrame); bodyFrame.origin.y = 0; [title setFrame:titleFrame]; [body setFrame:bodyFrame]; NSView* view = [[NSView alloc] initWithFrame:viewFrame]; [view setSubviews:@[title, body]]; return view; } - (void)performLayoutWithModel:(WebIntentPickerModel*)model { model_ = model; // |offset| is the Y position that should be drawn at next. CGFloat offset = WebIntentPicker::kContentAreaBorder; // Keep the new subviews in an array that gets replaced at the end. NSMutableArray* subviews = [NSMutableArray array]; // Indicator that we have neither suggested nor installed services, // and we're not in the wait stage any more either. BOOL isEmpty = model_ && !model_->IsWaitingForSuggestions() && !model_->GetInstalledServiceCount() && !model_->GetSuggestedExtensionCount(); if (model_ && model_->IsWaitingForSuggestions()) { if (!waitingView_.get()) waitingView_.reset([[WaitingView alloc] init]); [subviews addObject:waitingView_]; offset += NSHeight([waitingView_ frame]); } else if (isEmpty) { scoped_nsobject emptyView([self createEmptyView]); [subviews addObject:emptyView]; offset += NSHeight([emptyView frame]); } else if (webContents_) { offset += [self addAnotherServiceLinkToSubviews:subviews atOffset:offset]; offset += [self addInlineHtmlToSubviews:subviews atOffset:offset]; } else { offset += [self addHeaderToSubviews:subviews atOffset:offset]; if (model) { intentView_.reset( [[IntentView alloc] initWithModel:model forController:self]); offset += [self addStackedView:intentView_ toSubviews:subviews atOffset:offset]; } offset += [self addCwsButtonToSubviews:subviews atOffset:offset]; } // Add the bottom padding. offset += WebIntentPicker::kContentAreaBorder; // Resize to fit. [self setContainerSize:NSMakeSize(WebIntentPicker::kWindowMinWidth, offset)]; [self addCloseButtonToSubviews:subviews]; // Replace the window's content. [flipView_ setSubviews:subviews]; } - (void)setActionString:(NSString*)actionString { NSRect textFrame; if (!actionTextField_.get()) { textFrame = NSMakeRect(WebIntentPicker::kContentAreaBorder, 0, kTextWidth, 1); actionTextField_.reset([[NSTextField alloc] initWithFrame:textFrame]); ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); ConfigureTextFieldAsLabel(actionTextField_); gfx::Font titleFont = rb.GetFont( ConstrainedWindowConstants::kTitleFontStyle); titleFont = titleFont.DeriveFont(0, gfx::Font::BOLD); [actionTextField_ setFont:titleFont.GetNativeFont()]; } else { textFrame = [actionTextField_ frame]; } [actionTextField_ setStringValue:actionString]; textFrame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: actionTextField_]; [actionTextField_ setFrame:textFrame]; } - (void)setInlineDispositionTitle:(NSString*)title { NSFont* nsfont = [inlineDispositionTitleField_ font]; gfx::Font font( base::SysNSStringToUTF8([nsfont fontName]), [nsfont pointSize]); NSString* elidedTitle = base::SysUTF16ToNSString(ui::ElideText( base::SysNSStringToUTF16(title), font, WebIntentPicker::kTitleLinkMaxWidth, ui::ELIDE_AT_END)); [inlineDispositionTitleField_ setStringValue:elidedTitle]; } - (void)stopThrobber { [closeButton_ setEnabled:YES]; [intentView_ stopThrobber]; } - (void)closeSheet { [NSApp endSheet:[self window]]; } @end // WebIntentPickerSheetController