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