Home | History | Annotate | Download | only in cocoa
      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 
     65 namespace {
     66 
     67 // The width of the window, in view coordinates. The height will be determined
     68 // by the content.
     69 const CGFloat kWindowWidth = 380;
     70 
     71 // Spacing in between sections.
     72 const CGFloat kVerticalSpacing = 10;
     73 
     74 // Padding along on the X-axis between the window frame and content.
     75 const CGFloat kFramePadding = 10;
     76 
     77 // Spacing between the optional headline and description text views.
     78 const CGFloat kHeadlineSpacing = 2;
     79 
     80 // Spacing between the image and the text.
     81 const CGFloat kImageSpacing = 10;
     82 
     83 // Square size of the image.
     84 const CGFloat kImageSize = 30;
     85 
     86 // The X position of the text fields. Variants for with and without an image.
     87 const CGFloat kTextXPositionNoImage = kFramePadding;
     88 const CGFloat kTextXPosition = kTextXPositionNoImage + kImageSize +
     89     kImageSpacing;
     90 
     91 // Width of the text fields.
     92 const CGFloat kTextWidth = kWindowWidth - (kImageSize + kImageSpacing +
     93     kFramePadding * 2);
     94 
     95 // Bridge that listens for change notifications from the model.
     96 class 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 
    144 namespace browser {
    145 
    146 void 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