// Copyright (c) 2010 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/cocoa/page_info_bubble_controller.h" #include "app/l10n_util.h" #include "app/l10n_util_mac.h" #include "base/sys_string_conversions.h" #include "chrome/browser/browser_list.h" #include "chrome/browser/cert_store.h" #include "chrome/browser/certificate_viewer.h" #import "chrome/browser/cocoa/browser_window_controller.h" #import "chrome/browser/cocoa/hyperlink_button_cell.h" #import "chrome/browser/cocoa/info_bubble_view.h" #import "chrome/browser/cocoa/info_bubble_window.h" #import "chrome/browser/cocoa/location_bar/location_bar_view_mac.h" #include "chrome/browser/profile.h" #include "grit/generated_resources.h" #include "grit/locale_settings.h" #include "net/base/cert_status_flags.h" #include "net/base/x509_certificate.h" #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" @interface PageInfoBubbleController (Private) - (PageInfoModel*)model; - (NSButton*)certificateButtonWithFrame:(NSRect)frame; - (void)configureTextFieldAsLabel:(NSTextField*)textField; - (CGFloat)addTitleViewForInfo:(const PageInfoModel::SectionInfo&)info toSubviews:(NSMutableArray*)subviews atPoint:(NSPoint)point; - (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info toSubviews:(NSMutableArray*)subviews atPoint:(NSPoint)point; - (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset; - (void)addImageViewForInfo:(const PageInfoModel::SectionInfo&)info toSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset; - (CGFloat)addHelpButtonToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset; - (CGFloat)addSeparatorToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset; - (NSPoint)anchorPointForWindowWithHeight:(CGFloat)bubbleHeight parentWindow:(NSWindow*)parent; @end namespace { // The width of the window, in view coordinates. The height will be determined // by the content. const NSInteger kWindowWidth = 380; // Spacing in between sections. const NSInteger kVerticalSpacing = 10; // Padding along on the X-axis between the window frame and content. const NSInteger kFramePadding = 20; // Spacing between the title and description text views. const NSInteger kTitleSpacing = 2; // Spacing between the image and the text. const NSInteger kImageSpacing = 10; // Square size of the image. const CGFloat kImageSize = 30; // The X position of the text fields. Variants for with and without an image. const CGFloat kTextXPositionNoImage = kFramePadding; const CGFloat kTextXPosition = kTextXPositionNoImage + kImageSize + kImageSpacing; // Width of the text fields. const CGFloat kTextWidth = kWindowWidth - (kImageSize + kImageSpacing + kFramePadding * 2); // Bridge that listens for change notifications from the model. class PageInfoModelBubbleBridge : public PageInfoModel::PageInfoModelObserver { public: PageInfoModelBubbleBridge() : controller_(nil) {} // PageInfoModelObserver implementation. virtual void ModelChanged() { // Delay performing layout by a second so that all the animations from // InfoBubbleWindow and origin updates from BaseBubbleController finish, so // that we don't all race trying to change the frame's origin. [controller_ performSelector:@selector(performLayout) withObject:nil afterDelay:1.0]; } // Sets the controller. void set_controller(PageInfoBubbleController* controller) { controller_ = controller; } private: PageInfoBubbleController* controller_; // weak }; } // namespace namespace browser { void ShowPageInfoBubble(gfx::NativeWindow parent, Profile* profile, const GURL& url, const NavigationEntry::SSLStatus& ssl, bool show_history) { PageInfoModelBubbleBridge* bridge = new PageInfoModelBubbleBridge(); PageInfoModel* model = new PageInfoModel(profile, url, ssl, show_history, bridge); PageInfoBubbleController* controller = [[PageInfoBubbleController alloc] initWithPageInfoModel:model modelObserver:bridge parentWindow:parent]; bridge->set_controller(controller); [controller setCertID:ssl.cert_id()]; [controller showWindow:nil]; } } // namespace browser @implementation PageInfoBubbleController @synthesize certID = certID_; - (id)initWithPageInfoModel:(PageInfoModel*)model modelObserver:(PageInfoModel::PageInfoModelObserver*)bridge parentWindow:(NSWindow*)parentWindow { // Use an arbitrary height because it will be changed by the bridge. NSRect contentRect = NSMakeRect(0, 0, kWindowWidth, 0); // Create an empty window into which content is placed. scoped_nsobject window( [[InfoBubbleWindow alloc] initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]); if ((self = [super initWithWindow:window.get() parentWindow:parentWindow anchoredAt:NSZeroPoint])) { model_.reset(model); bridge_.reset(bridge); [[self bubble] setArrowLocation:info_bubble::kTopLeft]; [self performLayout]; } return self; } - (PageInfoModel*)model { return model_.get(); } - (IBAction)showCertWindow:(id)sender { DCHECK(certID_ != 0); ShowCertificateViewerByID([self parentWindow], certID_); } - (IBAction)showHelpPage:(id)sender { GURL url = GURL(l10n_util::GetStringUTF16(IDS_PAGE_INFO_HELP_CENTER)); Browser* browser = BrowserList::GetLastActive(); browser->OpenURL(url, GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK); } // This will create the subviews for the page info window. The general layout // is 2 or 3 boxed and titled sections, each of which has a status image to // provide visual feedback and a description that explains it. The description // text is usually only 1 or 2 lines, but can be much longer. At the bottom of // the window is a button to view the SSL certificate, which is disabled if // not using HTTPS. - (void)performLayout { // |offset| is the Y position that should be drawn at next. CGFloat offset = kVerticalSpacing; // Keep the new subviews in an array that gets replaced at the end. NSMutableArray* subviews = [NSMutableArray array]; // First item, drawn at the bottom of the window, is the help center link. offset += [self addHelpButtonToSubviews:subviews atOffset:offset]; offset += kVerticalSpacing; offset += [self addSeparatorToSubviews:subviews atOffset:offset]; // Build the window from bottom-up because Cocoa's coordinate origin is the // lower left. for (int i = model_->GetSectionCount() - 1; i >= 0; --i) { PageInfoModel::SectionInfo info = model_->GetSectionInfo(i); // Only certain sections have images. This affects the X position. BOOL hasImage = model_->GetIconImage(info.icon_id) != nil; CGFloat xPosition = (hasImage ? kTextXPosition : kTextXPositionNoImage); if (info.type == PageInfoModel::SECTION_INFO_IDENTITY) { offset += [self addCertificateButtonToSubviews:subviews atOffset:offset]; } // Create the description of the state. offset += [self addDescriptionViewForInfo:info toSubviews:subviews atPoint:NSMakePoint(xPosition, offset)]; // Add the title. offset += kTitleSpacing; offset += [self addTitleViewForInfo:info toSubviews:subviews atPoint:NSMakePoint(xPosition, offset)]; // Insert the image subview for sections that are appropriate. if (hasImage) { [self addImageViewForInfo:info toSubviews:subviews atOffset:offset]; } // Add the separators. if (i != 0) { offset += kVerticalSpacing; offset += [self addSeparatorToSubviews:subviews atOffset:offset]; } } // Replace the window's content. [[[self window] contentView] setSubviews:subviews]; offset += kFramePadding; NSRect windowFrame = NSMakeRect(0, 0, kWindowWidth, 0); windowFrame.size.height += offset; windowFrame.size = [[[self window] contentView] convertSize:windowFrame.size toView:nil]; // Adjust the origin by the difference in height. windowFrame.origin = [[self window] frame].origin; windowFrame.origin.y -= NSHeight(windowFrame) - NSHeight([[self window] frame]); // Resize the window. Only animate if the window is visible, otherwise it // could be "growing" while it's opening, looking awkward. [[self window] setFrame:windowFrame display:YES animate:[[self window] isVisible]]; NSPoint anchorPoint = [self anchorPointForWindowWithHeight:NSHeight(windowFrame) parentWindow:[self parentWindow]]; [self setAnchorPoint:anchorPoint]; } // Creates the button with a given |frame| that, when clicked, will show the // SSL certificate information. - (NSButton*)certificateButtonWithFrame:(NSRect)frame { NSButton* certButton = [[[NSButton alloc] initWithFrame:frame] autorelease]; [certButton setTitle: l10n_util::GetNSStringWithFixup(IDS_PAGEINFO_CERT_INFO_BUTTON)]; [certButton setButtonType:NSMomentaryPushInButton]; [certButton setBezelStyle:NSRoundRectBezelStyle]; [certButton setTarget:self]; [certButton setAction:@selector(showCertWindow:)]; [[certButton cell] setControlSize:NSSmallControlSize]; NSFont* font = [NSFont systemFontOfSize: [NSFont systemFontSizeForControlSize:NSSmallControlSize]]; [[certButton cell] setFont:font]; return certButton; } // Sets proprties on the given |field| to act as the title or description labels // in the bubble. - (void)configureTextFieldAsLabel:(NSTextField*)textField { [textField setEditable:NO]; [textField setDrawsBackground:NO]; [textField setBezeled:NO]; } // Adds the title text field at the given x,y position, and returns the y // position for the next element. - (CGFloat)addTitleViewForInfo:(const PageInfoModel::SectionInfo&)info toSubviews:(NSMutableArray*)subviews atPoint:(NSPoint)point { NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSpacing); scoped_nsobject titleField( [[NSTextField alloc] initWithFrame:frame]); [self configureTextFieldAsLabel:titleField.get()]; [titleField setStringValue:base::SysUTF16ToNSString(info.title)]; NSFont* font = [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]]; [titleField setFont:font]; frame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: titleField]; [titleField setFrame:frame]; [subviews addObject:titleField.get()]; return NSHeight(frame); } // Adds the description text field at the given x,y position, and returns the y // position for the next element. - (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info toSubviews:(NSMutableArray*)subviews atPoint:(NSPoint)point { NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSize); scoped_nsobject textField( [[NSTextField alloc] initWithFrame:frame]); [self configureTextFieldAsLabel:textField.get()]; [textField setStringValue:base::SysUTF16ToNSString(info.description)]; [textField setFont:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]]]; // If the text is oversized, resize the text field. frame.size.height += [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: textField]; [subviews addObject:textField.get()]; return NSHeight(frame); } // Adds the certificate button at a pre-determined x position and the given y. // Returns the y position for the next element. - (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { // Create the certificate button. The frame will be fixed up by GTM, so // use arbitrary values. NSRect frame = NSMakeRect(kTextXPosition, offset, 100, 14); NSButton* certButton = [self certificateButtonWithFrame:frame]; [subviews addObject:certButton]; [GTMUILocalizerAndLayoutTweaker sizeToFitView:certButton]; // By default, assume that we don't have certificate information to show. [certButton setEnabled:NO]; if (certID_) { scoped_refptr cert; CertStore::GetSharedInstance()->RetrieveCert(certID_, &cert); // Don't bother showing certificates if there isn't one. Gears runs // with no OS root certificate. if (cert.get() && cert->os_cert_handle()) { [certButton setEnabled:YES]; } } return NSHeight(frame) + kVerticalSpacing; } // Adds the state image at a pre-determined x position and the given y. This // does not affect the next Y position because the image is placed next to // a text field that is larger and accounts for the image's size. - (void)addImageViewForInfo:(const PageInfoModel::SectionInfo&)info toSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { NSRect frame = NSMakeRect(kFramePadding, offset - kImageSize, kImageSize, kImageSize); scoped_nsobject imageView( [[NSImageView alloc] initWithFrame:frame]); [imageView setImageFrameStyle:NSImageFrameNone]; [imageView setImage:model_->GetIconImage(info.icon_id)]; [subviews addObject:imageView.get()]; } // Adds the help center button that explains the icons. Returns the y position // delta for the next offset. - (CGFloat)addHelpButtonToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { NSRect frame = NSMakeRect(kFramePadding, offset, 100, 10); scoped_nsobject button([[NSButton alloc] initWithFrame:frame]); NSString* string = l10n_util::GetNSStringWithFixup(IDS_PAGE_INFO_HELP_CENTER_LINK); scoped_nsobject cell( [[HyperlinkButtonCell alloc] initTextCell:string]); [cell setControlSize:NSSmallControlSize]; [button setCell:cell.get()]; [button setButtonType:NSMomentaryPushInButton]; [button setBezelStyle:NSRegularSquareBezelStyle]; [button setTarget:self]; [button setAction:@selector(showHelpPage:)]; [subviews addObject:button.get()]; // Call size-to-fit to fixup for the localized string. [GTMUILocalizerAndLayoutTweaker sizeToFitView:button.get()]; return NSHeight([button frame]); } // Adds a 1px separator between sections. Returns the y position delta for the // next offset. - (CGFloat)addSeparatorToSubviews:(NSMutableArray*)subviews atOffset:(CGFloat)offset { scoped_nsobject spacer( [[NSBox alloc] initWithFrame:NSMakeRect(0, offset, kWindowWidth, 1)]); [spacer setBoxType:NSBoxSeparator]; [spacer setBorderType:NSLineBorder]; [spacer setAlphaValue:0.2]; [subviews addObject:spacer.get()]; return kVerticalSpacing; } // Takes in the bubble's height and the parent window, which should be a // BrowserWindow, and gets the proper anchor point for the bubble. The returned // point is in screen coordinates. - (NSPoint)anchorPointForWindowWithHeight:(CGFloat)bubbleHeight parentWindow:(NSWindow*)parent { BrowserWindowController* controller = [parent windowController]; NSPoint origin = NSZeroPoint; if ([controller isKindOfClass:[BrowserWindowController class]]) { LocationBarViewMac* locationBar = [controller locationBarBridge]; if (locationBar) { NSPoint bubblePoint = locationBar->GetPageInfoBubblePoint(); origin = [parent convertBaseToScreen:bubblePoint]; } } return origin; } @end