• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 <Cocoa/Cocoa.h>
6
7#include "base/logging.h"  // for NOTREACHED()
8#include "base/mac/mac_util.h"
9#include "base/sys_string_conversions.h"
10#include "chrome/browser/tab_contents/confirm_infobar_delegate.h"
11#include "chrome/browser/tab_contents/link_infobar_delegate.h"
12#import "chrome/browser/ui/cocoa/animatable_view.h"
13#include "chrome/browser/ui/cocoa/event_utils.h"
14#include "chrome/browser/ui/cocoa/infobars/infobar.h"
15#import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
16#import "chrome/browser/ui/cocoa/infobars/infobar_controller.h"
17#include "skia/ext/skia_utils_mac.h"
18#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
19#include "webkit/glue/window_open_disposition.h"
20
21namespace {
22// Durations set to match the default SlideAnimation duration.
23const float kAnimateOpenDuration = 0.12;
24const float kAnimateCloseDuration = 0.12;
25}
26
27// This simple subclass of |NSTextView| just doesn't show the (text) cursor
28// (|NSTextView| displays the cursor with full keyboard accessibility enabled).
29@interface InfoBarTextView : NSTextView
30- (void)fixupCursor;
31@end
32
33@implementation InfoBarTextView
34
35// Never draw the insertion point (otherwise, it shows up without any user
36// action if full keyboard accessibility is enabled).
37- (BOOL)shouldDrawInsertionPoint {
38  return NO;
39}
40
41- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
42                              granularity:(NSSelectionGranularity)granularity {
43  // Do not allow selections.
44  return NSMakeRange(0, 0);
45}
46
47// Convince NSTextView to not show an I-Beam cursor when the cursor is over the
48// text view but not over actual text.
49//
50// http://www.mail-archive.com/cocoa-dev@lists.apple.com/msg10791.html
51// "NSTextView sets the cursor over itself dynamically, based on considerations
52// including the text under the cursor. It does so in -mouseEntered:,
53// -mouseMoved:, and -cursorUpdate:, so those would be points to consider
54// overriding."
55- (void)mouseMoved:(NSEvent*)e {
56  [super mouseMoved:e];
57  [self fixupCursor];
58}
59
60- (void)mouseEntered:(NSEvent*)e {
61  [super mouseEntered:e];
62  [self fixupCursor];
63}
64
65- (void)cursorUpdate:(NSEvent*)e {
66  [super cursorUpdate:e];
67  [self fixupCursor];
68}
69
70- (void)fixupCursor {
71  if ([[NSCursor currentCursor] isEqual:[NSCursor IBeamCursor]])
72    [[NSCursor arrowCursor] set];
73}
74
75@end
76
77@interface InfoBarController (PrivateMethods)
78// Sets |label_| based on |labelPlaceholder_|, sets |labelPlaceholder_| to nil.
79- (void)initializeLabel;
80
81// Asks the container controller to remove the infobar for this delegate.  This
82// call will trigger a notification that starts the infobar animating closed.
83- (void)removeInfoBar;
84
85// Performs final cleanup after an animation is finished or stopped, including
86// notifying the InfoBarDelegate that the infobar was closed and removing the
87// infobar from its container, if necessary.
88- (void)cleanUpAfterAnimation:(BOOL)finished;
89
90// Sets the info bar message to the specified |message|, with a hypertext
91// style link. |link| will be inserted into message at |linkOffset|.
92- (void)setLabelToMessage:(NSString*)message
93                 withLink:(NSString*)link
94                 atOffset:(NSUInteger)linkOffset;
95@end
96
97@implementation InfoBarController
98
99@synthesize containerController = containerController_;
100@synthesize delegate = delegate_;
101
102- (id)initWithDelegate:(InfoBarDelegate*)delegate {
103  DCHECK(delegate);
104  if ((self = [super initWithNibName:@"InfoBar"
105                              bundle:base::mac::MainAppBundle()])) {
106    delegate_ = delegate;
107  }
108  return self;
109}
110
111// All infobars have an icon, so we set up the icon in the base class
112// awakeFromNib.
113- (void)awakeFromNib {
114  DCHECK(delegate_);
115  if (delegate_->GetIcon()) {
116    [image_ setImage:gfx::SkBitmapToNSImage(*(delegate_->GetIcon()))];
117  } else {
118    // No icon, remove it from the view and grow the textfield to include the
119    // space.
120    NSRect imageFrame = [image_ frame];
121    NSRect labelFrame = [labelPlaceholder_ frame];
122    labelFrame.size.width += NSMinX(imageFrame) - NSMinX(labelFrame);
123    labelFrame.origin.x = imageFrame.origin.x;
124    [image_ removeFromSuperview];
125    [labelPlaceholder_ setFrame:labelFrame];
126  }
127  [self initializeLabel];
128
129  [self addAdditionalControls];
130}
131
132// Called when someone clicks on the embedded link.
133- (BOOL) textView:(NSTextView*)textView
134    clickedOnLink:(id)link
135          atIndex:(NSUInteger)charIndex {
136  if ([self respondsToSelector:@selector(linkClicked)])
137    [self performSelector:@selector(linkClicked)];
138  return YES;
139}
140
141// Called when someone clicks on the ok button.
142- (void)ok:(id)sender {
143  // Subclasses must override this method if they do not hide the ok button.
144  NOTREACHED();
145}
146
147// Called when someone clicks on the cancel button.
148- (void)cancel:(id)sender {
149  // Subclasses must override this method if they do not hide the cancel button.
150  NOTREACHED();
151}
152
153// Called when someone clicks on the close button.
154- (void)dismiss:(id)sender {
155  if (delegate_)
156    delegate_->InfoBarDismissed();
157
158  [self removeInfoBar];
159}
160
161- (AnimatableView*)animatableView {
162  return static_cast<AnimatableView*>([self view]);
163}
164
165- (void)open {
166  // Simply reset the frame size to its opened size, forcing a relayout.
167  CGFloat finalHeight = [[self view] frame].size.height;
168  [[self animatableView] setHeight:finalHeight];
169}
170
171- (void)animateOpen {
172  // Force the frame size to be 0 and then start an animation.
173  NSRect frame = [[self view] frame];
174  CGFloat finalHeight = frame.size.height;
175  frame.size.height = 0;
176  [[self view] setFrame:frame];
177  [[self animatableView] animateToNewHeight:finalHeight
178                                   duration:kAnimateOpenDuration];
179}
180
181- (void)close {
182  // Stop any running animations.
183  [[self animatableView] stopAnimation];
184  infoBarClosing_ = YES;
185  [self cleanUpAfterAnimation:YES];
186}
187
188- (void)animateClosed {
189  // Notify the container of our intentions.
190  [containerController_ willRemoveController:self];
191
192  // Start animating closed.  We will receive a notification when the animation
193  // is done, at which point we can remove our view from the hierarchy and
194  // notify the delegate that the infobar was closed.
195  [[self animatableView] animateToNewHeight:0 duration:kAnimateCloseDuration];
196
197  // The above call may trigger an animationDidStop: notification for any
198  // currently-running animations, so do not set |infoBarClosing_| until after
199  // starting the animation.
200  infoBarClosing_ = YES;
201}
202
203- (void)addAdditionalControls {
204  // Default implementation does nothing.
205}
206
207- (void)infobarWillClose {
208  // Default implementation does nothing.
209}
210
211- (void)setLabelToMessage:(NSString*)message {
212  NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
213  NSFont* font = [NSFont labelFontOfSize:
214      [NSFont systemFontSizeForControlSize:NSRegularControlSize]];
215  [attributes setObject:font
216                 forKey:NSFontAttributeName];
217  [attributes setObject:[NSCursor arrowCursor]
218                 forKey:NSCursorAttributeName];
219  scoped_nsobject<NSAttributedString> attributedString(
220      [[NSAttributedString alloc] initWithString:message
221                                      attributes:attributes]);
222  [[label_.get() textStorage] setAttributedString:attributedString];
223}
224
225- (void)removeButtons {
226  // Extend the label all the way across.
227  NSRect labelFrame = [label_.get() frame];
228  labelFrame.size.width = NSMaxX([cancelButton_ frame]) - NSMinX(labelFrame);
229  [okButton_ removeFromSuperview];
230  [cancelButton_ removeFromSuperview];
231  [label_.get() setFrame:labelFrame];
232}
233
234@end
235
236@implementation InfoBarController (PrivateMethods)
237
238- (void)initializeLabel {
239  // Replace the label placeholder NSTextField with the real label NSTextView.
240  // The former doesn't show links in a nice way, but the latter can't be added
241  // in IB without a containing scroll view, so create the NSTextView
242  // programmatically.
243  label_.reset([[InfoBarTextView alloc]
244      initWithFrame:[labelPlaceholder_ frame]]);
245  [label_.get() setAutoresizingMask:[labelPlaceholder_ autoresizingMask]];
246  [[labelPlaceholder_ superview]
247      replaceSubview:labelPlaceholder_ with:label_.get()];
248  labelPlaceholder_ = nil;  // Now released.
249  [label_.get() setDelegate:self];
250  [label_.get() setEditable:NO];
251  [label_.get() setDrawsBackground:NO];
252  [label_.get() setHorizontallyResizable:NO];
253  [label_.get() setVerticallyResizable:NO];
254}
255
256- (void)removeInfoBar {
257  // TODO(rohitrao): This method can be called even if the infobar has already
258  // been removed and |delegate_| is NULL.  Is there a way to rewrite the code
259  // so that inner event loops don't cause us to try and remove the infobar
260  // twice?  http://crbug.com/54253
261  [containerController_ removeDelegate:delegate_];
262}
263
264- (void)cleanUpAfterAnimation:(BOOL)finished {
265  // Don't need to do any cleanup if the bar was animating open.
266  if (!infoBarClosing_)
267    return;
268
269  // Notify the delegate that the infobar was closed.  The delegate may delete
270  // itself as a result of InfoBarClosed(), so we null out its pointer.
271  if (delegate_) {
272    delegate_->InfoBarClosed();
273    delegate_ = NULL;
274  }
275
276  // If the animation ran to completion, then we need to remove ourselves from
277  // the container.  If the animation was interrupted, then the container will
278  // take care of removing us.
279  // TODO(rohitrao): UGH!  This works for now, but should be cleaner.
280  if (finished)
281    [containerController_ removeController:self];
282}
283
284- (void)animationDidStop:(NSAnimation*)animation {
285  [self cleanUpAfterAnimation:NO];
286}
287
288- (void)animationDidEnd:(NSAnimation*)animation {
289  [self cleanUpAfterAnimation:YES];
290}
291
292// TODO(joth): This method factors out some common functionality between the
293// various derived infobar classes, however the class hierarchy itself could
294// use refactoring to reduce this duplication. http://crbug.com/38924
295- (void)setLabelToMessage:(NSString*)message
296                 withLink:(NSString*)link
297                 atOffset:(NSUInteger)linkOffset {
298  if (linkOffset == std::wstring::npos) {
299    // linkOffset == std::wstring::npos means the link should be right-aligned,
300    // which is not supported on Mac (http://crbug.com/47728).
301    NOTIMPLEMENTED();
302    linkOffset = [message length];
303  }
304  // Create an attributes dictionary for the entire message.  We have
305  // to expicitly set the font the control's font.  We also override
306  // the cursor to give us the normal cursor rather than the text
307  // insertion cursor.
308  NSMutableDictionary* linkAttributes = [NSMutableDictionary dictionary];
309  [linkAttributes setObject:[NSCursor arrowCursor]
310                     forKey:NSCursorAttributeName];
311  NSFont* font = [NSFont labelFontOfSize:
312      [NSFont systemFontSizeForControlSize:NSRegularControlSize]];
313  [linkAttributes setObject:font
314                     forKey:NSFontAttributeName];
315
316  // Create the attributed string for the main message text.
317  scoped_nsobject<NSMutableAttributedString> infoText(
318      [[NSMutableAttributedString alloc] initWithString:message]);
319  [infoText.get() addAttributes:linkAttributes
320                    range:NSMakeRange(0, [infoText.get() length])];
321  // Add additional attributes to style the link text appropriately as
322  // well as linkify it.
323  [linkAttributes setObject:[NSColor blueColor]
324                     forKey:NSForegroundColorAttributeName];
325  [linkAttributes setObject:[NSNumber numberWithBool:YES]
326                     forKey:NSUnderlineStyleAttributeName];
327  [linkAttributes setObject:[NSCursor pointingHandCursor]
328                     forKey:NSCursorAttributeName];
329  [linkAttributes setObject:[NSNumber numberWithInt:NSSingleUnderlineStyle]
330                     forKey:NSUnderlineStyleAttributeName];
331  [linkAttributes setObject:[NSString string]  // dummy value
332                     forKey:NSLinkAttributeName];
333
334  // Insert the link text into the string at the appropriate offset.
335  scoped_nsobject<NSAttributedString> attributedString(
336      [[NSAttributedString alloc] initWithString:link
337                                      attributes:linkAttributes]);
338  [infoText.get() insertAttributedString:attributedString.get()
339                                 atIndex:linkOffset];
340  // Update the label view with the new text.
341  [[label_.get() textStorage] setAttributedString:infoText];
342}
343
344@end
345
346
347/////////////////////////////////////////////////////////////////////////
348// LinkInfoBarController implementation
349
350@implementation LinkInfoBarController
351
352// Link infobars have a text message, of which part is linkified.  We
353// use an NSAttributedString to display styled text, and we set a
354// NSLink attribute on the hyperlink portion of the message.  Infobars
355// use a custom NSTextField subclass, which allows us to override
356// textView:clickedOnLink:atIndex: and intercept clicks.
357//
358- (void)addAdditionalControls {
359  // No buttons.
360  [self removeButtons];
361
362  LinkInfoBarDelegate* delegate = delegate_->AsLinkInfoBarDelegate();
363  DCHECK(delegate);
364  size_t offset = std::wstring::npos;
365  string16 message = delegate->GetMessageTextWithOffset(&offset);
366  [self setLabelToMessage:base::SysUTF16ToNSString(message)
367                 withLink:base::SysUTF16ToNSString(delegate->GetLinkText())
368                 atOffset:offset];
369}
370
371// Called when someone clicks on the link in the infobar.  This method
372// is called by the InfobarTextField on its delegate (the
373// LinkInfoBarController).
374- (void)linkClicked {
375  WindowOpenDisposition disposition =
376      event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
377  if (delegate_ && delegate_->AsLinkInfoBarDelegate()->LinkClicked(disposition))
378    [self removeInfoBar];
379}
380
381@end
382
383
384/////////////////////////////////////////////////////////////////////////
385// ConfirmInfoBarController implementation
386
387@implementation ConfirmInfoBarController
388
389// Called when someone clicks on the "OK" button.
390- (IBAction)ok:(id)sender {
391  if (delegate_ && delegate_->AsConfirmInfoBarDelegate()->Accept())
392    [self removeInfoBar];
393}
394
395// Called when someone clicks on the "Cancel" button.
396- (IBAction)cancel:(id)sender {
397  if (delegate_ && delegate_->AsConfirmInfoBarDelegate()->Cancel())
398    [self removeInfoBar];
399}
400
401// Confirm infobars can have OK and/or cancel buttons, depending on
402// the return value of GetButtons().  We create each button if
403// required and position them to the left of the close button.
404- (void)addAdditionalControls {
405  ConfirmInfoBarDelegate* delegate = delegate_->AsConfirmInfoBarDelegate();
406  DCHECK(delegate);
407  int visibleButtons = delegate->GetButtons();
408
409  NSRect okButtonFrame = [okButton_ frame];
410  NSRect cancelButtonFrame = [cancelButton_ frame];
411
412  DCHECK(NSMaxX(okButtonFrame) < NSMinX(cancelButtonFrame))
413      << "Cancel button expected to be on the right of the Ok button in nib";
414
415  CGFloat rightEdge = NSMaxX(cancelButtonFrame);
416  CGFloat spaceBetweenButtons =
417      NSMinX(cancelButtonFrame) - NSMaxX(okButtonFrame);
418  CGFloat spaceBeforeButtons =
419      NSMinX(okButtonFrame) - NSMaxX([label_.get() frame]);
420
421  // Update and position the Cancel button if needed.  Otherwise, hide it.
422  if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) {
423    [cancelButton_ setTitle:base::SysUTF16ToNSString(
424          delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_CANCEL))];
425    [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
426    cancelButtonFrame = [cancelButton_ frame];
427
428    // Position the cancel button to the left of the Close button.
429    cancelButtonFrame.origin.x = rightEdge - cancelButtonFrame.size.width;
430    [cancelButton_ setFrame:cancelButtonFrame];
431
432    // Update the rightEdge
433    rightEdge = NSMinX(cancelButtonFrame);
434  } else {
435    [cancelButton_ removeFromSuperview];
436  }
437
438  // Update and position the OK button if needed.  Otherwise, hide it.
439  if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK) {
440    [okButton_ setTitle:base::SysUTF16ToNSString(
441          delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_OK))];
442    [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_];
443    okButtonFrame = [okButton_ frame];
444
445    // If we had a Cancel button, leave space between the buttons.
446    if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) {
447      rightEdge -= spaceBetweenButtons;
448    }
449
450    // Position the OK button on our current right edge.
451    okButtonFrame.origin.x = rightEdge - okButtonFrame.size.width;
452    [okButton_ setFrame:okButtonFrame];
453
454
455    // Update the rightEdge
456    rightEdge = NSMinX(okButtonFrame);
457  } else {
458    [okButton_ removeFromSuperview];
459  }
460
461  // If we had either button, leave space before the edge of the textfield.
462  if ((visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) ||
463      (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK)) {
464    rightEdge -= spaceBeforeButtons;
465  }
466
467  NSRect frame = [label_.get() frame];
468  DCHECK(rightEdge > NSMinX(frame))
469      << "Need to make the xib larger to handle buttons with text this long";
470  frame.size.width = rightEdge - NSMinX(frame);
471  [label_.get() setFrame:frame];
472
473  // Set the text and link.
474  NSString* message = base::SysUTF16ToNSString(delegate->GetMessageText());
475  string16 link = delegate->GetLinkText();
476  if (link.empty()) {
477    // Simple case: no link, so just set the message directly.
478    [self setLabelToMessage:message];
479  } else {
480    // Inserting the link unintentionally causes the text to have a slightly
481    // different result to the simple case above: text is truncated on word
482    // boundaries (if needed) rather than elided with ellipses.
483
484    // Add spacing between the label and the link.
485    message = [message stringByAppendingString:@"   "];
486    [self setLabelToMessage:message
487                   withLink:base::SysUTF16ToNSString(link)
488                   atOffset:[message length]];
489  }
490}
491
492// Called when someone clicks on the link in the infobar.  This method
493// is called by the InfobarTextField on its delegate (the
494// LinkInfoBarController).
495- (void)linkClicked {
496  WindowOpenDisposition disposition =
497      event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
498  if (delegate_ &&
499      delegate_->AsConfirmInfoBarDelegate()->LinkClicked(disposition))
500    [self removeInfoBar];
501}
502
503@end
504
505
506//////////////////////////////////////////////////////////////////////////
507// CreateInfoBar() implementations
508
509InfoBar* LinkInfoBarDelegate::CreateInfoBar() {
510  LinkInfoBarController* controller =
511      [[LinkInfoBarController alloc] initWithDelegate:this];
512  return new InfoBar(controller);
513}
514
515InfoBar* ConfirmInfoBarDelegate::CreateInfoBar() {
516  ConfirmInfoBarController* controller =
517      [[ConfirmInfoBarController alloc] initWithDelegate:this];
518  return new InfoBar(controller);
519}
520