Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2012 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/confirm_bubble_cocoa.h"
      6 
      7 #include "base/strings/string16.h"
      8 #include "chrome/browser/themes/theme_service.h"
      9 #import "chrome/browser/ui/cocoa/confirm_bubble_controller.h"
     10 #include "chrome/browser/ui/confirm_bubble.h"
     11 #include "chrome/browser/ui/confirm_bubble_model.h"
     12 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
     13 #include "ui/gfx/image/image.h"
     14 #include "ui/gfx/point.h"
     15 
     16 // The width for the message text. We break lines so the specified message fits
     17 // into this width.
     18 const int kMaxMessageWidth = 400;
     19 
     20 // The corner redius of this bubble view.
     21 const int kBubbleCornerRadius = 3;
     22 
     23 // The color for the border of this bubble view.
     24 const float kBubbleWindowEdge = 0.7f;
     25 
     26 // Constants used for layouting controls. These variables are copied from
     27 // "ui/views/layout/layout_constants.h".
     28 // Vertical spacing between a label and some control.
     29 const int kLabelToControlVerticalSpacing = 8;
     30 
     31 // Horizontal spacing between controls that are logically related.
     32 const int kRelatedControlHorizontalSpacing = 8;
     33 
     34 // Vertical spacing between controls that are logically related.
     35 const int kRelatedControlVerticalSpacing = 8;
     36 
     37 // Vertical spacing between the edge of the window and the
     38 // top or bottom of a button.
     39 const int kButtonVEdgeMargin = 6;
     40 
     41 // Horizontal spacing between the edge of the window and the
     42 // left or right of a button.
     43 const int kButtonHEdgeMargin = 7;
     44 
     45 namespace chrome {
     46 
     47 void ShowConfirmBubble(gfx::NativeWindow window,
     48                        gfx::NativeView anchor_view,
     49                        const gfx::Point& origin,
     50                        ConfirmBubbleModel* model) {
     51   // Create a custom NSViewController that manages a bubble view, and add it to
     52   // a child to the specified |anchor_view|. This controller will be
     53   // automatically deleted when it loses first-responder status.
     54   ConfirmBubbleController* controller =
     55       [[ConfirmBubbleController alloc] initWithParent:anchor_view
     56                                                origin:origin.ToCGPoint()
     57                                                 model:model];
     58   [anchor_view addSubview:[controller view]
     59                positioned:NSWindowAbove
     60                relativeTo:nil];
     61   [[anchor_view window] makeFirstResponder:[controller view]];
     62 }
     63 
     64 }  // namespace chrome
     65 
     66 // An interface that is derived from NSTextView and does not accept
     67 // first-responder status, i.e. a NSTextView-derived class that never becomes
     68 // the first responder. When we click a NSTextView object, it becomes the first
     69 // responder. Unfortunately, we delete the ConfirmBubbleCocoa object anytime
     70 // when it loses first-responder status not to prevent disturbing other
     71 // responders.
     72 // To prevent text views in this ConfirmBubbleCocoa object from stealing the
     73 // first-responder status, we use this view in the ConfirmBubbleCocoa object.
     74 @interface ConfirmBubbleTextView : NSTextView
     75 @end
     76 
     77 @implementation ConfirmBubbleTextView
     78 
     79 - (BOOL)acceptsFirstResponder {
     80   return NO;
     81 }
     82 
     83 @end
     84 
     85 // Private Methods
     86 @interface ConfirmBubbleCocoa (Private)
     87 - (void)performLayout;
     88 - (void)closeBubble;
     89 @end
     90 
     91 @implementation ConfirmBubbleCocoa
     92 
     93 - (id)initWithParent:(NSView*)parent
     94           controller:(ConfirmBubbleController*)controller {
     95   // Create a NSView and set its width. We will set its position and height
     96   // after finish layouting controls in performLayout:.
     97   NSRect bounds =
     98       NSMakeRect(0, 0, kMaxMessageWidth + kButtonHEdgeMargin * 2, 0);
     99   if (self = [super initWithFrame:bounds]) {
    100     parent_ = parent;
    101     controller_ = controller;
    102     [self performLayout];
    103   }
    104   return self;
    105 }
    106 
    107 - (void)drawRect:(NSRect)dirtyRect {
    108   // Fill the background rectangle in white and draw its edge.
    109   NSRect bounds = [self bounds];
    110   bounds = NSInsetRect(bounds, 0.5, 0.5);
    111   NSBezierPath* border =
    112       [NSBezierPath gtm_bezierPathWithRoundRect:bounds
    113                             topLeftCornerRadius:kBubbleCornerRadius
    114                            topRightCornerRadius:kBubbleCornerRadius
    115                          bottomLeftCornerRadius:kBubbleCornerRadius
    116                         bottomRightCornerRadius:kBubbleCornerRadius];
    117   [[NSColor colorWithDeviceWhite:1.0f alpha:1.0f] set];
    118   [border fill];
    119   [[NSColor colorWithDeviceWhite:kBubbleWindowEdge alpha:1.0f] set];
    120   [border stroke];
    121 }
    122 
    123 // An NSResponder method.
    124 - (BOOL)resignFirstResponder {
    125   // We do not only accept this request but also close this bubble when we are
    126   // asked to resign the first responder. This bubble should be displayed only
    127   // while it is the first responder.
    128   [self closeBubble];
    129   return YES;
    130 }
    131 
    132 // NSControl action handlers. These handlers are called when we click a cancel
    133 // button, a close icon, and an OK button, respectively.
    134 - (IBAction)cancel:(id)sender {
    135   [controller_ cancel];
    136   [self closeBubble];
    137 }
    138 
    139 - (IBAction)close:(id)sender {
    140   [self closeBubble];
    141 }
    142 
    143 - (IBAction)ok:(id)sender {
    144   [controller_ accept];
    145   [self closeBubble];
    146 }
    147 
    148 // An NSTextViewDelegate method. This function is called when we click a link in
    149 // this bubble.
    150 - (BOOL)textView:(NSTextView*)textView
    151    clickedOnLink:(id)link
    152          atIndex:(NSUInteger)charIndex {
    153   [controller_ linkClicked];
    154   [self closeBubble];
    155   return YES;
    156 }
    157 
    158 // Initializes controls specified by the ConfirmBubbleModel object and layouts
    159 // them into this bubble. This function retrieves text and images from the
    160 // ConfirmBubbleModel object (via the ConfirmBubbleController object) and
    161 // layouts them programmatically. This function layouts controls in the botom-up
    162 // order since NSView uses bottom-up coordinate.
    163 - (void)performLayout {
    164   NSRect frameRect = [self frame];
    165 
    166   // Add the ok button and the cancel button to the first row if we have either
    167   // of them.
    168   CGFloat left = kButtonHEdgeMargin;
    169   CGFloat right = NSWidth(frameRect) - kButtonHEdgeMargin;
    170   CGFloat bottom = kButtonVEdgeMargin;
    171   CGFloat height = 0;
    172   if ([controller_ hasOkButton]) {
    173     okButton_.reset([[NSButton alloc]
    174         initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
    175     [okButton_.get() setBezelStyle:NSRoundedBezelStyle];
    176     [okButton_.get() setTitle:[controller_ okButtonText]];
    177     [okButton_.get() setTarget:self];
    178     [okButton_.get() setAction:@selector(ok:)];
    179     [okButton_.get() sizeToFit];
    180     NSRect okButtonRect = [okButton_.get() frame];
    181     right -= NSWidth(okButtonRect);
    182     okButtonRect.origin.x = right;
    183     [okButton_.get() setFrame:okButtonRect];
    184     [self addSubview:okButton_.get()];
    185     height = std::max(height, NSHeight(okButtonRect));
    186   }
    187   if ([controller_ hasCancelButton]) {
    188     cancelButton_.reset([[NSButton alloc]
    189         initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
    190     [cancelButton_.get() setBezelStyle:NSRoundedBezelStyle];
    191     [cancelButton_.get() setTitle:[controller_ cancelButtonText]];
    192     [cancelButton_.get() setTarget:self];
    193     [cancelButton_.get() setAction:@selector(cancel:)];
    194     [cancelButton_.get() sizeToFit];
    195     NSRect cancelButtonRect = [cancelButton_.get() frame];
    196     right -= NSWidth(cancelButtonRect) + kButtonHEdgeMargin;
    197     cancelButtonRect.origin.x = right;
    198     [cancelButton_.get() setFrame:cancelButtonRect];
    199     [self addSubview:cancelButton_.get()];
    200     height = std::max(height, NSHeight(cancelButtonRect));
    201   }
    202 
    203   // Add the message label (and the link label) to the second row.
    204   left = kButtonHEdgeMargin;
    205   right = NSWidth(frameRect);
    206   bottom += height + kRelatedControlVerticalSpacing;
    207   height = 0;
    208   messageLabel_.reset([[ConfirmBubbleTextView alloc]
    209       initWithFrame:NSMakeRect(left, bottom, kMaxMessageWidth, 0)]);
    210   NSString* messageText = [controller_ messageText];
    211   NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
    212   base::scoped_nsobject<NSMutableAttributedString> attributedMessage(
    213       [[NSMutableAttributedString alloc] initWithString:messageText
    214                                              attributes:attributes]);
    215   NSString* linkText = [controller_ linkText];
    216   if (linkText) {
    217     base::scoped_nsobject<NSAttributedString> whiteSpace(
    218         [[NSAttributedString alloc] initWithString:@" "]);
    219     [attributedMessage.get() appendAttributedString:whiteSpace.get()];
    220     [attributes setObject:[NSString string]
    221                    forKey:NSLinkAttributeName];
    222     base::scoped_nsobject<NSAttributedString> attributedLink(
    223         [[NSAttributedString alloc] initWithString:linkText
    224                                         attributes:attributes]);
    225     [attributedMessage.get() appendAttributedString:attributedLink.get()];
    226   }
    227   [[messageLabel_.get() textStorage] setAttributedString:attributedMessage];
    228   [messageLabel_.get() setHorizontallyResizable:NO];
    229   [messageLabel_.get() setVerticallyResizable:YES];
    230   [messageLabel_.get() setEditable:NO];
    231   [messageLabel_.get() setDrawsBackground:NO];
    232   [messageLabel_.get() setDelegate:self];
    233   [messageLabel_.get() sizeToFit];
    234   height = NSHeight([messageLabel_.get() frame]);
    235   [self addSubview:messageLabel_.get()];
    236 
    237   // Add the icon and the title label to the third row.
    238   left = kButtonHEdgeMargin;
    239   right = NSWidth(frameRect);
    240   bottom += height + kLabelToControlVerticalSpacing;
    241   height = 0;
    242   NSImage* iconImage = [controller_ icon];
    243   if (iconImage) {
    244     icon_.reset([[NSImageView alloc] initWithFrame:NSMakeRect(
    245         left, bottom, [iconImage size].width, [iconImage size].height)]);
    246     [icon_.get() setImage:iconImage];
    247     [self addSubview:icon_.get()];
    248     left += NSWidth([icon_.get() frame]) + kRelatedControlHorizontalSpacing;
    249     height = std::max(height, NSHeight([icon_.get() frame]));
    250   }
    251   titleLabel_.reset([[NSTextView alloc]
    252       initWithFrame:NSMakeRect(left, bottom, right - left, 0)]);
    253   [titleLabel_.get() setString:[controller_ title]];
    254   [titleLabel_.get() setHorizontallyResizable:NO];
    255   [titleLabel_.get() setVerticallyResizable:YES];
    256   [titleLabel_.get() setEditable:NO];
    257   [titleLabel_.get() setSelectable:NO];
    258   [titleLabel_.get() setDrawsBackground:NO];
    259   [titleLabel_.get() sizeToFit];
    260   [self addSubview:titleLabel_.get()];
    261   height = std::max(height, NSHeight([titleLabel_.get() frame]));
    262 
    263   // Adjust the frame rectangle of this bubble so we can show all controls.
    264   NSRect parentRect = [parent_ frame];
    265   frameRect.size.height = bottom + height + kButtonVEdgeMargin;
    266   frameRect.origin.x = (NSWidth(parentRect) - NSWidth(frameRect)) / 2;
    267   frameRect.origin.y = NSHeight(parentRect) - NSHeight(frameRect);
    268   [self setFrame:frameRect];
    269 }
    270 
    271 // Closes this bubble and releases all resources. This function just puts the
    272 // owner ConfirmBubbleController object to the current autorelease pool. (This
    273 // view will be deleted when the owner object is deleted.)
    274 - (void)closeBubble {
    275   [self removeFromSuperview];
    276   [controller_ autorelease];
    277   parent_ = nil;
    278   controller_ = nil;
    279 }
    280 
    281 @end
    282 
    283 @implementation ConfirmBubbleCocoa (ExposedForUnitTesting)
    284 
    285 - (void)clickOk {
    286   [self ok:self];
    287 }
    288 
    289 - (void)clickCancel {
    290   [self cancel:self];
    291 }
    292 
    293 - (void)clickLink {
    294   [self textView:messageLabel_.get() clickedOnLink:nil atIndex:0];
    295 }
    296 
    297 @end
    298