blob: 474bf2132f477fabfa9ca16bfcf319399149754c [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/confirm_bubble_cocoa.h"
#include "base/strings/string16.h"
#include "chrome/browser/themes/theme_service.h"
#import "chrome/browser/ui/cocoa/confirm_bubble_controller.h"
#include "chrome/browser/ui/confirm_bubble.h"
#include "chrome/browser/ui/confirm_bubble_model.h"
#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/point.h"
namespace {
// The width for the message text. We break lines so the specified message fits
// into this width.
const int kMaxMessageWidth = 400;
// The corner redius of this bubble view.
const int kBubbleCornerRadius = 3;
// The color for the border of this bubble view.
const float kBubbleWindowEdge = 0.7f;
// Constants used for layouting controls. These variables are copied from
// "ui/views/layout/layout_constants.h".
// Vertical spacing between a label and some control.
const int kLabelToControlVerticalSpacing = 8;
// Horizontal spacing between controls that are logically related.
const int kRelatedControlHorizontalSpacing = 8;
// Vertical spacing between controls that are logically related.
const int kRelatedControlVerticalSpacing = 8;
// Horizontal spacing between controls that are logically unrelated.
const int kUnrelatedControlHorizontalSpacing = 12;
// Vertical spacing between the edge of the window and the
// top or bottom of a button.
const int kButtonVEdgeMargin = 6;
// Horizontal spacing between the edge of the window and the
// left or right of a button.
const int kButtonHEdgeMargin = 7;
} // namespace
namespace chrome {
void ShowConfirmBubble(gfx::NativeView view,
const gfx::Point& origin,
ConfirmBubbleModel* model) {
// Create a custom NSViewController that manages a bubble view, and add it to
// a child to the specified view. This controller will be automatically
// deleted when it loses first-responder status.
ConfirmBubbleController* controller =
[[ConfirmBubbleController alloc] initWithParent:view
origin:origin.ToCGPoint()
model:model];
[view addSubview:[controller view]
positioned:NSWindowAbove
relativeTo:nil];
[[view window] makeFirstResponder:[controller view]];
}
} // namespace chrome
// An interface that is derived from NSTextView and does not accept
// first-responder status, i.e. a NSTextView-derived class that never becomes
// the first responder. When we click a NSTextView object, it becomes the first
// responder. Unfortunately, we delete the ConfirmBubbleCocoa object anytime
// when it loses first-responder status not to prevent disturbing other
// responders.
// To prevent text views in this ConfirmBubbleCocoa object from stealing the
// first-responder status, we use this view in the ConfirmBubbleCocoa object.
@interface ConfirmBubbleTextView : NSTextView
@end
@implementation ConfirmBubbleTextView
- (BOOL)acceptsFirstResponder {
return NO;
}
@end
// Private Methods
@interface ConfirmBubbleCocoa (Private)
- (void)performLayout;
- (void)closeBubble;
@end
@implementation ConfirmBubbleCocoa
- (id)initWithParent:(NSView*)parent
controller:(ConfirmBubbleController*)controller {
// Create a NSView and set its width. We will set its position and height
// after finish layouting controls in performLayout:.
NSRect bounds =
NSMakeRect(0, 0, kMaxMessageWidth + kButtonHEdgeMargin * 2, 0);
if (self = [super initWithFrame:bounds]) {
parent_ = parent;
controller_ = controller;
[self performLayout];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
// Fill the background rectangle in white and draw its edge.
NSRect bounds = [self bounds];
bounds = NSInsetRect(bounds, 0.5, 0.5);
NSBezierPath* border =
[NSBezierPath gtm_bezierPathWithRoundRect:bounds
topLeftCornerRadius:kBubbleCornerRadius
topRightCornerRadius:kBubbleCornerRadius
bottomLeftCornerRadius:kBubbleCornerRadius
bottomRightCornerRadius:kBubbleCornerRadius];
[[NSColor colorWithDeviceWhite:1.0f alpha:1.0f] set];
[border fill];
[[NSColor colorWithDeviceWhite:kBubbleWindowEdge alpha:1.0f] set];
[border stroke];
}
// An NSResponder method.
- (BOOL)resignFirstResponder {
// We do not only accept this request but also close this bubble when we are
// asked to resign the first responder. This bubble should be displayed only
// while it is the first responder.
[self closeBubble];
return YES;
}
// NSControl action handlers. These handlers are called when we click a cancel
// button, a close icon, and an OK button, respectively.
- (IBAction)cancel:(id)sender {
[controller_ cancel];
[self closeBubble];
}
- (IBAction)close:(id)sender {
[self closeBubble];
}
- (IBAction)ok:(id)sender {
[controller_ accept];
[self closeBubble];
}
// An NSTextViewDelegate method. This function is called when we click a link in
// this bubble.
- (BOOL)textView:(NSTextView*)textView
clickedOnLink:(id)link
atIndex:(NSUInteger)charIndex {
[controller_ linkClicked];
[self closeBubble];
return YES;
}
// Initializes controls specified by the ConfirmBubbleModel object and layouts
// them into this bubble. This function retrieves text and images from the
// ConfirmBubbleModel object (via the ConfirmBubbleController object) and
// layouts them programmatically. This function layouts controls in the botom-up
// order since NSView uses bottom-up coordinate.
- (void)performLayout {
NSRect frameRect = [self frame];
// Add the ok button and the cancel button to the first row if we have either
// of them.
CGFloat left = kButtonHEdgeMargin;
CGFloat right = NSWidth(frameRect) - kButtonHEdgeMargin;
CGFloat bottom = kButtonVEdgeMargin;
CGFloat height = 0;
if ([controller_ hasOkButton]) {
okButton_.reset([[NSButton alloc]
initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
[okButton_.get() setBezelStyle:NSRoundedBezelStyle];
[okButton_.get() setTitle:[controller_ okButtonText]];
[okButton_.get() setTarget:self];
[okButton_.get() setAction:@selector(ok:)];
[okButton_.get() sizeToFit];
NSRect okButtonRect = [okButton_.get() frame];
right -= NSWidth(okButtonRect);
okButtonRect.origin.x = right;
[okButton_.get() setFrame:okButtonRect];
[self addSubview:okButton_.get()];
height = std::max(height, NSHeight(okButtonRect));
}
if ([controller_ hasCancelButton]) {
cancelButton_.reset([[NSButton alloc]
initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
[cancelButton_.get() setBezelStyle:NSRoundedBezelStyle];
[cancelButton_.get() setTitle:[controller_ cancelButtonText]];
[cancelButton_.get() setTarget:self];
[cancelButton_.get() setAction:@selector(cancel:)];
[cancelButton_.get() sizeToFit];
NSRect cancelButtonRect = [cancelButton_.get() frame];
right -= NSWidth(cancelButtonRect) + kButtonHEdgeMargin;
cancelButtonRect.origin.x = right;
[cancelButton_.get() setFrame:cancelButtonRect];
[self addSubview:cancelButton_.get()];
height = std::max(height, NSHeight(cancelButtonRect));
}
// Add the message label (and the link label) to the second row.
left = kButtonHEdgeMargin;
right = NSWidth(frameRect);
bottom += height + kRelatedControlVerticalSpacing;
height = 0;
messageLabel_.reset([[ConfirmBubbleTextView alloc]
initWithFrame:NSMakeRect(left, bottom, kMaxMessageWidth, 0)]);
NSString* messageText = [controller_ messageText];
NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
base::scoped_nsobject<NSMutableAttributedString> attributedMessage(
[[NSMutableAttributedString alloc] initWithString:messageText
attributes:attributes]);
NSString* linkText = [controller_ linkText];
if (linkText) {
base::scoped_nsobject<NSAttributedString> whiteSpace(
[[NSAttributedString alloc] initWithString:@" "]);
[attributedMessage.get() appendAttributedString:whiteSpace.get()];
[attributes setObject:[NSString string]
forKey:NSLinkAttributeName];
base::scoped_nsobject<NSAttributedString> attributedLink(
[[NSAttributedString alloc] initWithString:linkText
attributes:attributes]);
[attributedMessage.get() appendAttributedString:attributedLink.get()];
}
[[messageLabel_.get() textStorage] setAttributedString:attributedMessage];
[messageLabel_.get() setHorizontallyResizable:NO];
[messageLabel_.get() setVerticallyResizable:YES];
[messageLabel_.get() setEditable:NO];
[messageLabel_.get() setDrawsBackground:NO];
[messageLabel_.get() setDelegate:self];
[messageLabel_.get() sizeToFit];
height = NSHeight([messageLabel_.get() frame]);
[self addSubview:messageLabel_.get()];
// Add the icon and the title label to the third row.
left = kButtonHEdgeMargin;
right = NSWidth(frameRect);
bottom += height + kLabelToControlVerticalSpacing;
height = 0;
NSImage* iconImage = [controller_ icon];
if (iconImage) {
icon_.reset([[NSImageView alloc] initWithFrame:NSMakeRect(
left, bottom, [iconImage size].width, [iconImage size].height)]);
[icon_.get() setImage:iconImage];
[self addSubview:icon_.get()];
left += NSWidth([icon_.get() frame]) + kRelatedControlHorizontalSpacing;
height = std::max(height, NSHeight([icon_.get() frame]));
}
titleLabel_.reset([[NSTextView alloc]
initWithFrame:NSMakeRect(left, bottom, right - left, 0)]);
[titleLabel_.get() setString:[controller_ title]];
[titleLabel_.get() setHorizontallyResizable:NO];
[titleLabel_.get() setVerticallyResizable:YES];
[titleLabel_.get() setEditable:NO];
[titleLabel_.get() setSelectable:NO];
[titleLabel_.get() setDrawsBackground:NO];
[titleLabel_.get() sizeToFit];
[self addSubview:titleLabel_.get()];
height = std::max(height, NSHeight([titleLabel_.get() frame]));
// Adjust the frame rectangle of this bubble so we can show all controls.
NSRect parentRect = [parent_ frame];
frameRect.size.height = bottom + height + kButtonVEdgeMargin;
frameRect.origin.x = (NSWidth(parentRect) - NSWidth(frameRect)) / 2;
frameRect.origin.y = NSHeight(parentRect) - NSHeight(frameRect);
[self setFrame:frameRect];
}
// Closes this bubble and releases all resources. This function just puts the
// owner ConfirmBubbleController object to the current autorelease pool. (This
// view will be deleted when the owner object is deleted.)
- (void)closeBubble {
[self removeFromSuperview];
[controller_ autorelease];
parent_ = nil;
controller_ = nil;
}
@end
@implementation ConfirmBubbleCocoa (ExposedForUnitTesting)
- (void)clickOk {
[self ok:self];
}
- (void)clickCancel {
[self cancel:self];
}
- (void)clickLink {
[self textView:messageLabel_.get() clickedOnLink:nil atIndex:0];
}
@end