1// Copyright (c) 2011 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. 4 5#import "chrome/browser/ui/cocoa/page_info_bubble_controller.h" 6 7#include "base/message_loop.h" 8#include "base/sys_string_conversions.h" 9#include "base/task.h" 10#include "chrome/browser/google/google_util.h" 11#include "chrome/browser/profiles/profile.h" 12#include "chrome/browser/ui/browser_list.h" 13#import "chrome/browser/ui/cocoa/browser_window_controller.h" 14#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h" 15#import "chrome/browser/ui/cocoa/info_bubble_view.h" 16#import "chrome/browser/ui/cocoa/info_bubble_window.h" 17#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" 18#include "chrome/common/url_constants.h" 19#include "content/browser/cert_store.h" 20#include "content/browser/certificate_viewer.h" 21#include "grit/generated_resources.h" 22#include "grit/locale_settings.h" 23#include "net/base/cert_status_flags.h" 24#include "net/base/x509_certificate.h" 25#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 26#include "ui/base/l10n/l10n_util.h" 27#include "ui/base/l10n/l10n_util_mac.h" 28#include "ui/gfx/image.h" 29 30@interface PageInfoBubbleController (Private) 31- (PageInfoModel*)model; 32- (NSButton*)certificateButtonWithFrame:(NSRect)frame; 33- (void)configureTextFieldAsLabel:(NSTextField*)textField; 34- (CGFloat)addHeadlineViewForInfo:(const PageInfoModel::SectionInfo&)info 35 toSubviews:(NSMutableArray*)subviews 36 atPoint:(NSPoint)point; 37- (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info 38 toSubviews:(NSMutableArray*)subviews 39 atPoint:(NSPoint)point; 40- (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews 41 atOffset:(CGFloat)offset; 42- (void)addImageViewForInfo:(const PageInfoModel::SectionInfo&)info 43 toSubviews:(NSMutableArray*)subviews 44 atOffset:(CGFloat)offset; 45- (CGFloat)addHelpButtonToSubviews:(NSMutableArray*)subviews 46 atOffset:(CGFloat)offset; 47- (CGFloat)addSeparatorToSubviews:(NSMutableArray*)subviews 48 atOffset:(CGFloat)offset; 49- (NSPoint)anchorPointForWindowWithHeight:(CGFloat)bubbleHeight 50 parentWindow:(NSWindow*)parent; 51@end 52 53// This simple NSView subclass is used as the single subview of the page info 54// bubble's window's contentView. Drawing is flipped so that layout of the 55// sections is easier. Apple recommends flipping the coordinate origin when 56// doing a lot of text layout because it's more natural. 57@interface PageInfoContentView : NSView 58@end 59@implementation PageInfoContentView 60- (BOOL)isFlipped { 61 return YES; 62} 63@end 64 65namespace { 66 67// The width of the window, in view coordinates. The height will be determined 68// by the content. 69const CGFloat kWindowWidth = 380; 70 71// Spacing in between sections. 72const CGFloat kVerticalSpacing = 10; 73 74// Padding along on the X-axis between the window frame and content. 75const CGFloat kFramePadding = 10; 76 77// Spacing between the optional headline and description text views. 78const CGFloat kHeadlineSpacing = 2; 79 80// Spacing between the image and the text. 81const CGFloat kImageSpacing = 10; 82 83// Square size of the image. 84const CGFloat kImageSize = 30; 85 86// The X position of the text fields. Variants for with and without an image. 87const CGFloat kTextXPositionNoImage = kFramePadding; 88const CGFloat kTextXPosition = kTextXPositionNoImage + kImageSize + 89 kImageSpacing; 90 91// Width of the text fields. 92const CGFloat kTextWidth = kWindowWidth - (kImageSize + kImageSpacing + 93 kFramePadding * 2); 94 95// Bridge that listens for change notifications from the model. 96class PageInfoModelBubbleBridge : public PageInfoModel::PageInfoModelObserver { 97 public: 98 PageInfoModelBubbleBridge() 99 : controller_(nil), 100 ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)) { 101 } 102 103 // PageInfoModelObserver implementation. 104 virtual void ModelChanged() { 105 // Check to see if a layout has already been scheduled. 106 if (!task_factory_.empty()) 107 return; 108 109 // Delay performing layout by a second so that all the animations from 110 // InfoBubbleWindow and origin updates from BaseBubbleController finish, so 111 // that we don't all race trying to change the frame's origin. 112 // 113 // Using ScopedRunnableMethodFactory is superior here to |-performSelector:| 114 // because it will not retain its target; if the child outlives its parent, 115 // zombies get left behind (http://crbug.com/59619). This will also cancel 116 // the scheduled Tasks if the controller (and thus this bridge) get 117 // destroyed before the message can be delivered. 118 MessageLoop::current()->PostDelayedTask(FROM_HERE, 119 task_factory_.NewRunnableMethod( 120 &PageInfoModelBubbleBridge::PerformLayout), 121 1000 /* milliseconds */); 122 } 123 124 // Sets the controller. 125 void set_controller(PageInfoBubbleController* controller) { 126 controller_ = controller; 127 } 128 129 private: 130 void PerformLayout() { 131 [controller_ performLayout]; 132 } 133 134 PageInfoBubbleController* controller_; // weak 135 136 // Factory that vends RunnableMethod tasks for scheduling layout. 137 ScopedRunnableMethodFactory<PageInfoModelBubbleBridge> task_factory_; 138 139 DISALLOW_COPY_AND_ASSIGN(PageInfoModelBubbleBridge); 140}; 141 142} // namespace 143 144namespace browser { 145 146void ShowPageInfoBubble(gfx::NativeWindow parent, 147 Profile* profile, 148 const GURL& url, 149 const NavigationEntry::SSLStatus& ssl, 150 bool show_history) { 151 PageInfoModelBubbleBridge* bridge = new PageInfoModelBubbleBridge(); 152 PageInfoModel* model = 153 new PageInfoModel(profile, url, ssl, show_history, bridge); 154 PageInfoBubbleController* controller = 155 [[PageInfoBubbleController alloc] initWithPageInfoModel:model 156 modelObserver:bridge 157 parentWindow:parent]; 158 bridge->set_controller(controller); 159 [controller setCertID:ssl.cert_id()]; 160 [controller showWindow:nil]; 161} 162 163} // namespace browser 164 165@implementation PageInfoBubbleController 166 167@synthesize certID = certID_; 168 169- (id)initWithPageInfoModel:(PageInfoModel*)model 170 modelObserver:(PageInfoModel::PageInfoModelObserver*)bridge 171 parentWindow:(NSWindow*)parentWindow { 172 DCHECK(parentWindow); 173 174 // Use an arbitrary height because it will be changed by the bridge. 175 NSRect contentRect = NSMakeRect(0, 0, kWindowWidth, 0); 176 // Create an empty window into which content is placed. 177 scoped_nsobject<InfoBubbleWindow> window( 178 [[InfoBubbleWindow alloc] initWithContentRect:contentRect 179 styleMask:NSBorderlessWindowMask 180 backing:NSBackingStoreBuffered 181 defer:NO]); 182 183 if ((self = [super initWithWindow:window.get() 184 parentWindow:parentWindow 185 anchoredAt:NSZeroPoint])) { 186 model_.reset(model); 187 bridge_.reset(bridge); 188 [[self bubble] setArrowLocation:info_bubble::kTopLeft]; 189 [self performLayout]; 190 } 191 return self; 192} 193 194- (PageInfoModel*)model { 195 return model_.get(); 196} 197 198- (IBAction)showCertWindow:(id)sender { 199 DCHECK(certID_ != 0); 200 ShowCertificateViewerByID([self parentWindow], certID_); 201} 202 203- (IBAction)showHelpPage:(id)sender { 204 GURL url = google_util::AppendGoogleLocaleParam( 205 GURL(chrome::kPageInfoHelpCenterURL)); 206 Browser* browser = BrowserList::GetLastActive(); 207 browser->OpenURL(url, GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK); 208} 209 210// This will create the subviews for the page info window. The general layout 211// is 2 or 3 boxed and titled sections, each of which has a status image to 212// provide visual feedback and a description that explains it. The description 213// text is usually only 1 or 2 lines, but can be much longer. At the bottom of 214// the window is a button to view the SSL certificate, which is disabled if 215// not using HTTPS. 216- (void)performLayout { 217 // |offset| is the Y position that should be drawn at next. 218 CGFloat offset = kFramePadding + info_bubble::kBubbleArrowHeight; 219 220 // Keep the new subviews in an array that gets replaced at the end. 221 NSMutableArray* subviews = [NSMutableArray array]; 222 223 // The subviews will be attached to the PageInfoContentView, which has a 224 // flipped origin. This allows the code to build top-to-bottom. 225 const int sectionCount = model_->GetSectionCount(); 226 for (int i = 0; i < sectionCount; ++i) { 227 PageInfoModel::SectionInfo info = model_->GetSectionInfo(i); 228 229 // Only certain sections have images. This affects the X position. 230 BOOL hasImage = model_->GetIconImage(info.icon_id) != nil; 231 CGFloat xPosition = (hasImage ? kTextXPosition : kTextXPositionNoImage); 232 233 // Insert the image subview for sections that are appropriate. 234 CGFloat imageBaseline = offset + kImageSize; 235 if (hasImage) { 236 [self addImageViewForInfo:info toSubviews:subviews atOffset:offset]; 237 } 238 239 // Add the title. 240 if (!info.headline.empty()) { 241 offset += [self addHeadlineViewForInfo:info 242 toSubviews:subviews 243 atPoint:NSMakePoint(xPosition, offset)]; 244 offset += kHeadlineSpacing; 245 } 246 247 // Create the description of the state. 248 offset += [self addDescriptionViewForInfo:info 249 toSubviews:subviews 250 atPoint:NSMakePoint(xPosition, offset)]; 251 252 if (info.type == PageInfoModel::SECTION_INFO_IDENTITY && certID_) { 253 offset += kVerticalSpacing; 254 offset += [self addCertificateButtonToSubviews:subviews atOffset:offset]; 255 } 256 257 // If at this point the description and optional headline and button are 258 // not as tall as the image, adjust the offset by the difference. 259 CGFloat imageBaselineDelta = imageBaseline - offset; 260 if (imageBaselineDelta > 0) 261 offset += imageBaselineDelta; 262 263 // Add the separators. 264 offset += kVerticalSpacing; 265 offset += [self addSeparatorToSubviews:subviews atOffset:offset]; 266 } 267 268 // The last item at the bottom of the window is the help center link. 269 offset += [self addHelpButtonToSubviews:subviews atOffset:offset]; 270 offset += kVerticalSpacing; 271 272 // Create the dummy view that uses flipped coordinates. 273 NSRect contentFrame = NSMakeRect(0, 0, kWindowWidth, offset); 274 scoped_nsobject<PageInfoContentView> contentView( 275 [[PageInfoContentView alloc] initWithFrame:contentFrame]); 276 [contentView setSubviews:subviews]; 277 278 NSRect windowFrame = NSMakeRect(0, 0, kWindowWidth, offset); 279 windowFrame.size = [[[self window] contentView] convertSize:windowFrame.size 280 toView:nil]; 281 // Adjust the origin by the difference in height. 282 windowFrame.origin = [[self window] frame].origin; 283 windowFrame.origin.y -= NSHeight(windowFrame) - 284 NSHeight([[self window] frame]); 285 286 // Resize the window. Only animate if the window is visible, otherwise it 287 // could be "growing" while it's opening, looking awkward. 288 [[self window] setFrame:windowFrame 289 display:YES 290 animate:[[self window] isVisible]]; 291 292 // Replace the window's content. 293 [[[self window] contentView] setSubviews: 294 [NSArray arrayWithObject:contentView]]; 295 296 NSPoint anchorPoint = 297 [self anchorPointForWindowWithHeight:NSHeight(windowFrame) 298 parentWindow:[self parentWindow]]; 299 [self setAnchorPoint:anchorPoint]; 300} 301 302// Creates the button with a given |frame| that, when clicked, will show the 303// SSL certificate information. 304- (NSButton*)certificateButtonWithFrame:(NSRect)frame { 305 NSButton* certButton = [[[NSButton alloc] initWithFrame:frame] autorelease]; 306 [certButton setTitle: 307 l10n_util::GetNSStringWithFixup(IDS_PAGEINFO_CERT_INFO_BUTTON)]; 308 [certButton setButtonType:NSMomentaryPushInButton]; 309 [certButton setBezelStyle:NSRoundRectBezelStyle]; 310 [certButton setTarget:self]; 311 [certButton setAction:@selector(showCertWindow:)]; 312 [[certButton cell] setControlSize:NSSmallControlSize]; 313 NSFont* font = [NSFont systemFontOfSize: 314 [NSFont systemFontSizeForControlSize:NSSmallControlSize]]; 315 [[certButton cell] setFont:font]; 316 return certButton; 317} 318 319// Sets proprties on the given |field| to act as the title or description labels 320// in the bubble. 321- (void)configureTextFieldAsLabel:(NSTextField*)textField { 322 [textField setEditable:NO]; 323 [textField setSelectable:YES]; 324 [textField setDrawsBackground:NO]; 325 [textField setBezeled:NO]; 326} 327 328// Adds the title text field at the given x,y position, and returns the y 329// position for the next element. 330- (CGFloat)addHeadlineViewForInfo:(const PageInfoModel::SectionInfo&)info 331 toSubviews:(NSMutableArray*)subviews 332 atPoint:(NSPoint)point { 333 NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSpacing); 334 scoped_nsobject<NSTextField> textField( 335 [[NSTextField alloc] initWithFrame:frame]); 336 [self configureTextFieldAsLabel:textField.get()]; 337 [textField setStringValue:base::SysUTF16ToNSString(info.headline)]; 338 NSFont* font = [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]]; 339 [textField setFont:font]; 340 frame.size.height += 341 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: 342 textField]; 343 [textField setFrame:frame]; 344 [subviews addObject:textField.get()]; 345 return NSHeight(frame); 346} 347 348// Adds the description text field at the given x,y position, and returns the y 349// position for the next element. 350- (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info 351 toSubviews:(NSMutableArray*)subviews 352 atPoint:(NSPoint)point { 353 NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSize); 354 scoped_nsobject<NSTextField> textField( 355 [[NSTextField alloc] initWithFrame:frame]); 356 [self configureTextFieldAsLabel:textField.get()]; 357 [textField setStringValue:base::SysUTF16ToNSString(info.description)]; 358 [textField setFont:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]]]; 359 360 // If the text is oversized, resize the text field. 361 frame.size.height += 362 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField: 363 textField]; 364 [subviews addObject:textField.get()]; 365 return NSHeight(frame); 366} 367 368// Adds the certificate button at a pre-determined x position and the given y. 369// Returns the y position for the next element. 370- (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews 371 atOffset:(CGFloat)offset { 372 // The certificate button should only be added if there is SSL information. 373 DCHECK(certID_); 374 375 // Create the certificate button. The frame will be fixed up by GTM, so 376 // use arbitrary values. 377 NSRect frame = NSMakeRect(kTextXPosition, offset, 100, 14); 378 NSButton* certButton = [self certificateButtonWithFrame:frame]; 379 [subviews addObject:certButton]; 380 [GTMUILocalizerAndLayoutTweaker sizeToFitView:certButton]; 381 382 // By default, assume that we don't have certificate information to show. 383 scoped_refptr<net::X509Certificate> cert; 384 CertStore::GetInstance()->RetrieveCert(certID_, &cert); 385 386 // Don't bother showing certificates if there isn't one. 387 if (!cert.get() || !cert->os_cert_handle()) { 388 // This should only ever happen in unit tests. 389 [certButton setEnabled:NO]; 390 } 391 392 return NSHeight([certButton frame]); 393} 394 395// Adds the state image at a pre-determined x position and the given y. This 396// does not affect the next Y position because the image is placed next to 397// a text field that is larger and accounts for the image's size. 398- (void)addImageViewForInfo:(const PageInfoModel::SectionInfo&)info 399 toSubviews:(NSMutableArray*)subviews 400 atOffset:(CGFloat)offset { 401 NSRect frame = 402 NSMakeRect(kFramePadding, offset, kImageSize, kImageSize); 403 scoped_nsobject<NSImageView> imageView( 404 [[NSImageView alloc] initWithFrame:frame]); 405 [imageView setImageFrameStyle:NSImageFrameNone]; 406 [imageView setImage:*model_->GetIconImage(info.icon_id)]; 407 [subviews addObject:imageView.get()]; 408} 409 410// Adds the help center button that explains the icons. Returns the y position 411// delta for the next offset. 412- (CGFloat)addHelpButtonToSubviews:(NSMutableArray*)subviews 413 atOffset:(CGFloat)offset { 414 NSRect frame = NSMakeRect(kFramePadding, offset, 100, 10); 415 scoped_nsobject<NSButton> button([[NSButton alloc] initWithFrame:frame]); 416 NSString* string = 417 l10n_util::GetNSStringWithFixup(IDS_PAGE_INFO_HELP_CENTER_LINK); 418 scoped_nsobject<HyperlinkButtonCell> cell( 419 [[HyperlinkButtonCell alloc] initTextCell:string]); 420 [cell setControlSize:NSSmallControlSize]; 421 [button setCell:cell.get()]; 422 [button setButtonType:NSMomentaryPushInButton]; 423 [button setBezelStyle:NSRegularSquareBezelStyle]; 424 [button setTarget:self]; 425 [button setAction:@selector(showHelpPage:)]; 426 [subviews addObject:button.get()]; 427 428 // Call size-to-fit to fixup for the localized string. 429 [GTMUILocalizerAndLayoutTweaker sizeToFitView:button.get()]; 430 return NSHeight([button frame]); 431} 432 433// Adds a 1px separator between sections. Returns the y position delta for the 434// next offset. 435- (CGFloat)addSeparatorToSubviews:(NSMutableArray*)subviews 436 atOffset:(CGFloat)offset { 437 const CGFloat kSpacerHeight = 1.0; 438 NSRect frame = NSMakeRect(kFramePadding, offset, 439 kWindowWidth - 2 * kFramePadding, kSpacerHeight); 440 scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]); 441 [spacer setBoxType:NSBoxSeparator]; 442 [spacer setBorderType:NSLineBorder]; 443 [spacer setAlphaValue:0.2]; 444 [subviews addObject:spacer.get()]; 445 return kVerticalSpacing + kSpacerHeight; 446} 447 448// Takes in the bubble's height and the parent window, which should be a 449// BrowserWindow, and gets the proper anchor point for the bubble. The returned 450// point is in screen coordinates. 451- (NSPoint)anchorPointForWindowWithHeight:(CGFloat)bubbleHeight 452 parentWindow:(NSWindow*)parent { 453 BrowserWindowController* controller = [parent windowController]; 454 NSPoint origin = NSZeroPoint; 455 if ([controller isKindOfClass:[BrowserWindowController class]]) { 456 LocationBarViewMac* locationBar = [controller locationBarBridge]; 457 if (locationBar) { 458 NSPoint bubblePoint = locationBar->GetPageInfoBubblePoint(); 459 origin = [parent convertBaseToScreen:bubblePoint]; 460 } 461 } 462 return origin; 463} 464 465@end 466