summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/confirm_bubble_cocoa.mm
blob: 80a547525b642f291b3ddc454c2b28f6d488bd36 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// 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 <utility>

#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/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/image/image.h"

// 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;

// Vertical spacing between controls that are logically related.
const int kRelatedControlVerticalSpacing = 8;

// 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 chrome {

void ShowConfirmBubble(gfx::NativeWindow window,
                       gfx::NativeView anchor_view,
                       const gfx::Point& origin,
                       scoped_ptr<ConfirmBubbleModel> model) {
  // Create a custom NSViewController that manages a bubble view, and add it to
  // a child to the specified |anchor_view|. This controller will be
  // automatically deleted when it loses first-responder status.
  ConfirmBubbleController* controller =
      [[ConfirmBubbleController alloc] initWithParent:anchor_view
                                               origin:origin.ToCGPoint()
                                                model:std::move(model)];
  [anchor_view addSubview:[controller view]
               positioned:NSWindowAbove
               relativeTo:nil];
  [[anchor_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:[controller_ linkURL]
                   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;
  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 = 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