page_info_bubble_controller.mm revision dc0f95d653279beabeb9817299e2902918ba123e
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/browser_list.h"
11#include "chrome/browser/google/google_util.h"
12#include "chrome/browser/profiles/profile.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/certificate_viewer.h"
20#include "content/browser/cert_store.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  // Replace the window's content.
279  [[[self window] contentView] setSubviews:
280      [NSArray arrayWithObject:contentView]];
281
282  NSRect windowFrame = NSMakeRect(0, 0, kWindowWidth, offset);
283  windowFrame.size = [[[self window] contentView] convertSize:windowFrame.size
284                                                       toView:nil];
285  // Adjust the origin by the difference in height.
286  windowFrame.origin = [[self window] frame].origin;
287  windowFrame.origin.y -= NSHeight(windowFrame) -
288      NSHeight([[self window] frame]);
289
290  // Resize the window. Only animate if the window is visible, otherwise it
291  // could be "growing" while it's opening, looking awkward.
292  [[self window] setFrame:windowFrame
293                  display:YES
294                  animate:[[self window] isVisible]];
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 setDrawsBackground:NO];
324  [textField setBezeled:NO];
325}
326
327// Adds the title text field at the given x,y position, and returns the y
328// position for the next element.
329- (CGFloat)addHeadlineViewForInfo:(const PageInfoModel::SectionInfo&)info
330                       toSubviews:(NSMutableArray*)subviews
331                          atPoint:(NSPoint)point {
332  NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSpacing);
333  scoped_nsobject<NSTextField> textField(
334      [[NSTextField alloc] initWithFrame:frame]);
335  [self configureTextFieldAsLabel:textField.get()];
336  [textField setStringValue:base::SysUTF16ToNSString(info.headline)];
337  NSFont* font = [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]];
338  [textField setFont:font];
339  frame.size.height +=
340      [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
341          textField];
342  [textField setFrame:frame];
343  [subviews addObject:textField.get()];
344  return NSHeight(frame);
345}
346
347// Adds the description text field at the given x,y position, and returns the y
348// position for the next element.
349- (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info
350                          toSubviews:(NSMutableArray*)subviews
351                             atPoint:(NSPoint)point {
352  NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSize);
353  scoped_nsobject<NSTextField> textField(
354      [[NSTextField alloc] initWithFrame:frame]);
355  [self configureTextFieldAsLabel:textField.get()];
356  [textField setStringValue:base::SysUTF16ToNSString(info.description)];
357  [textField setFont:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]]];
358
359  // If the text is oversized, resize the text field.
360  frame.size.height +=
361      [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
362          textField];
363  [subviews addObject:textField.get()];
364  return NSHeight(frame);
365}
366
367// Adds the certificate button at a pre-determined x position and the given y.
368// Returns the y position for the next element.
369- (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews
370                                 atOffset:(CGFloat)offset {
371  // The certificate button should only be added if there is SSL information.
372  DCHECK(certID_);
373
374  // Create the certificate button. The frame will be fixed up by GTM, so
375  // use arbitrary values.
376  NSRect frame = NSMakeRect(kTextXPosition, offset, 100, 14);
377  NSButton* certButton = [self certificateButtonWithFrame:frame];
378  [subviews addObject:certButton];
379  [GTMUILocalizerAndLayoutTweaker sizeToFitView:certButton];
380
381  // By default, assume that we don't have certificate information to show.
382  scoped_refptr<net::X509Certificate> cert;
383  CertStore::GetInstance()->RetrieveCert(certID_, &cert);
384
385  // Don't bother showing certificates if there isn't one. Gears runs
386  // with no OS root certificate.
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