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 "chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h"
6
7#include <algorithm>
8
9#include "base/command_line.h"
10#include "base/sys_string_conversions.h"
11#include "base/utf_string_conversions.h"
12#include "chrome/browser/content_setting_bubble_model.h"
13#include "chrome/browser/content_setting_image_model.h"
14#include "chrome/browser/prefs/pref_service.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/ui/browser_list.h"
17#import "chrome/browser/ui/cocoa/content_settings/content_setting_bubble_cocoa.h"
18#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
19#include "chrome/common/chrome_switches.h"
20#include "chrome/common/pref_names.h"
21#include "content/browser/tab_contents/tab_contents.h"
22#include "net/base/net_util.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/gfx/image.h"
26
27namespace {
28
29// How far to offset up from the bottom of the view to get the top
30// border of the popup 2px below the bottom of the Omnibox.
31const CGFloat kPopupPointYOffset = 2.0;
32
33// Duration of animation, 3 seconds. The ContentSettingAnimationState breaks
34// this up into different states of varying lengths.
35const NSTimeInterval kAnimationDuration = 3.0;
36
37// Interval of the animation timer, 60Hz.
38const NSTimeInterval kAnimationInterval = 1.0 / 60.0;
39
40// The % of time it takes to open or close the animating text, ie at 0.2, the
41// opening takes 20% of the whole animation and the closing takes 20%. The
42// remainder of the animation is with the text at full width.
43const double kInMotionInterval = 0.2;
44
45// Used to create a % complete of the "in motion" part of the animation, eg
46// it should be 1.0 (100%) when the progress is 0.2.
47const double kInMotionMultiplier = 1.0 / kInMotionInterval;
48
49// Padding for the animated text with respect to the image.
50const CGFloat kTextMarginPadding = 4;
51const CGFloat kIconMarginPadding = 2;
52const CGFloat kBorderPadding = 3;
53
54// Different states in which the animation can be. In |kOpening|, the text
55// is getting larger. In |kOpen|, the text should be displayed at full size.
56// In |kClosing|, the text is again getting smaller. The durations in which
57// the animation remains in each state are internal to
58// |ContentSettingAnimationState|.
59enum AnimationState {
60  kNoAnimation,
61  kOpening,
62  kOpen,
63  kClosing
64};
65
66}  // namespace
67
68
69// An ObjC class that handles the multiple states of the text animation and
70// bridges NSTimer calls back to the ContentSettingDecoration that owns it.
71// Should be lazily instantiated to only exist when the decoration requires
72// animation.
73// NOTE: One could make this class more generic, but this class only exists
74// because CoreAnimation cannot be used (there are no views to work with).
75@interface ContentSettingAnimationState : NSObject {
76 @private
77  ContentSettingDecoration* owner_;  // Weak, owns this.
78  double progress_;  // Counter, [0..1], with aninmation progress.
79  NSTimer* timer_;  // Animation timer. Owns this, owned by the run loop.
80}
81
82// [0..1], the current progress of the animation. -animationState will return
83// |kNoAnimation| when progress is <= 0 or >= 1. Useful when state is
84// |kOpening| or |kClosing| as a multiplier for displaying width. Don't use
85// to track state transitions, use -animationState instead.
86@property (readonly, nonatomic) double progress;
87
88// Designated initializer. |owner| must not be nil. Animation timer will start
89// as soon as the object is created.
90- (id)initWithOwner:(ContentSettingDecoration*)owner;
91
92// Returns the current animation state based on how much time has elapsed.
93- (AnimationState)animationState;
94
95// Call when |owner| is going away or the animation needs to be stopped.
96// Ensures that any dangling references are cleared. Can be called multiple
97// times.
98- (void)stopAnimation;
99
100@end
101
102@implementation ContentSettingAnimationState
103
104@synthesize progress = progress_;
105
106- (id)initWithOwner:(ContentSettingDecoration*)owner {
107  self = [super init];
108  if (self) {
109    owner_ = owner;
110    timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationInterval
111                                              target:self
112                                            selector:@selector(timerFired:)
113                                            userInfo:nil
114                                             repeats:YES];
115  }
116  return self;
117}
118
119- (void)dealloc {
120  [self stopAnimation];
121  [super dealloc];
122}
123
124// Clear weak references and stop the timer.
125- (void)stopAnimation {
126  owner_ = nil;
127  [timer_ invalidate];
128  timer_ = nil;
129}
130
131// Returns the current state based on how much time has elapsed.
132- (AnimationState)animationState {
133  if (progress_ <= 0.0 || progress_ >= 1.0)
134    return kNoAnimation;
135  if (progress_ <= kInMotionInterval)
136    return kOpening;
137  if (progress_ >= 1.0 - kInMotionInterval)
138    return kClosing;
139  return kOpen;
140}
141
142- (void)timerFired:(NSTimer*)timer {
143  // Increment animation progress, normalized to [0..1].
144  progress_ += kAnimationInterval / kAnimationDuration;
145  progress_ = std::min(progress_, 1.0);
146  owner_->AnimationTimerFired();
147  // Stop timer if it has reached the end of its life.
148  if (progress_ >= 1.0)
149    [self stopAnimation];
150}
151
152@end
153
154
155ContentSettingDecoration::ContentSettingDecoration(
156    ContentSettingsType settings_type,
157    LocationBarViewMac* owner,
158    Profile* profile)
159    : content_setting_image_model_(
160          ContentSettingImageModel::CreateContentSettingImageModel(
161              settings_type)),
162      owner_(owner),
163      profile_(profile),
164      text_width_(0.0) {
165}
166
167ContentSettingDecoration::~ContentSettingDecoration() {
168  // Just in case the timer is still holding onto the animation object, force
169  // cleanup so it can't get back to |this|.
170  [animation_ stopAnimation];
171}
172
173bool ContentSettingDecoration::UpdateFromTabContents(
174    TabContents* tab_contents) {
175  bool was_visible = IsVisible();
176  int old_icon = content_setting_image_model_->get_icon();
177  content_setting_image_model_->UpdateFromTabContents(tab_contents);
178  SetVisible(content_setting_image_model_->is_visible());
179  bool decoration_changed = was_visible != IsVisible() ||
180      old_icon != content_setting_image_model_->get_icon();
181  if (IsVisible()) {
182    // TODO(thakis): We should use pdfs for these icons on OSX.
183    // http://crbug.com/35847
184    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
185    SetImage(rb.GetNativeImageNamed(content_setting_image_model_->get_icon()));
186    SetToolTip(base::SysUTF8ToNSString(
187        content_setting_image_model_->get_tooltip()));
188    // Check if there is an animation and start it if it hasn't yet started.
189    bool has_animated_text =
190        content_setting_image_model_->explanatory_string_id();
191    // Check if the animation is enabled.
192    bool animation_enabled = !CommandLine::ForCurrentProcess()->HasSwitch(
193        switches::kDisableBlockContentAnimation);
194    if (has_animated_text && animation_enabled && !animation_) {
195      // Start animation, its timer will drive reflow. Note the text is
196      // cached so it is not allowed to change during the animation.
197      animation_.reset(
198          [[ContentSettingAnimationState alloc] initWithOwner:this]);
199      animated_text_.reset(CreateAnimatedText());
200      text_width_ = MeasureTextWidth();
201    } else if (!has_animated_text) {
202      // Decoration no longer has animation, stop it (ok to always do this).
203      [animation_ stopAnimation];
204      animation_.reset(nil);
205    }
206  } else {
207    // Decoration no longer visible, stop/clear animation.
208    [animation_ stopAnimation];
209    animation_.reset(nil);
210  }
211  return decoration_changed;
212}
213
214CGFloat ContentSettingDecoration::MeasureTextWidth() {
215  return [animated_text_ size].width;
216}
217
218// Returns an attributed string with the animated text. Caller is responsible
219// for releasing.
220NSAttributedString* ContentSettingDecoration::CreateAnimatedText() {
221  NSString* text =
222      l10n_util::GetNSString(
223          content_setting_image_model_->explanatory_string_id());
224  NSDictionary* attributes =
225      [NSDictionary dictionaryWithObject:[NSFont labelFontOfSize:14]
226                                  forKey:NSFontAttributeName];
227  NSAttributedString* attr_string =
228      [[NSAttributedString alloc] initWithString:text attributes:attributes];
229  return attr_string;
230}
231
232NSPoint ContentSettingDecoration::GetBubblePointInFrame(NSRect frame) {
233  // Compute the frame as if there is no animation pill in the Omnibox. Place
234  // the bubble where the icon would be without animation, so when the animation
235  // ends, the bubble is pointing in the right place.
236  NSSize image_size = [GetImage() size];
237  frame.origin.x += frame.size.width - image_size.width;
238  frame.size = image_size;
239
240  const NSRect draw_frame = GetDrawRectInFrame(frame);
241  return NSMakePoint(NSMidX(draw_frame),
242                     NSMaxY(draw_frame) - kPopupPointYOffset);
243}
244
245bool ContentSettingDecoration::AcceptsMousePress() {
246  return true;
247}
248
249bool ContentSettingDecoration::OnMousePressed(NSRect frame) {
250  // Get host. This should be shared on linux/win/osx medium-term.
251  TabContents* tabContents =
252      BrowserList::GetLastActive()->GetSelectedTabContents();
253  if (!tabContents)
254    return true;
255
256  // Prerender icon does not include a bubble.
257  ContentSettingsType content_settings_type =
258      content_setting_image_model_->get_content_settings_type();
259  if (content_settings_type == CONTENT_SETTINGS_TYPE_PRERENDER)
260    return true;
261
262  GURL url = tabContents->GetURL();
263  std::wstring displayHost;
264  net::AppendFormattedHost(
265      url,
266      UTF8ToWide(profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)),
267      &displayHost, NULL, NULL);
268
269  // Find point for bubble's arrow in screen coordinates.
270  // TODO(shess): |owner_| is only being used to fetch |field|.
271  // Consider passing in |control_view|.  Or refactoring to be
272  // consistent with other decorations (which don't currently bring up
273  // their bubble directly).
274  AutocompleteTextField* field = owner_->GetAutocompleteTextField();
275  NSPoint anchor = GetBubblePointInFrame(frame);
276  anchor = [field convertPoint:anchor toView:nil];
277  anchor = [[field window] convertBaseToScreen:anchor];
278
279  // Open bubble.
280  ContentSettingBubbleModel* model =
281      ContentSettingBubbleModel::CreateContentSettingBubbleModel(
282          tabContents, profile_, content_settings_type);
283  [ContentSettingBubbleController showForModel:model
284                                   parentWindow:[field window]
285                                     anchoredAt:anchor];
286  return true;
287}
288
289NSString* ContentSettingDecoration::GetToolTip() {
290  return tooltip_.get();
291}
292
293void ContentSettingDecoration::SetToolTip(NSString* tooltip) {
294  tooltip_.reset([tooltip retain]);
295}
296
297// Override to handle the case where there is text to display during the
298// animation. The width is based on the animator's progress.
299CGFloat ContentSettingDecoration::GetWidthForSpace(CGFloat width) {
300  CGFloat preferred_width = ImageDecoration::GetWidthForSpace(width);
301  if (animation_.get()) {
302    AnimationState state = [animation_ animationState];
303    if (state != kNoAnimation) {
304      CGFloat progress = [animation_ progress];
305      // Add the margins, fixed for all animation states.
306      preferred_width += kIconMarginPadding + kTextMarginPadding;
307      // Add the width of the text based on the state of the animation.
308      switch (state) {
309        case kOpening:
310          preferred_width += text_width_ * kInMotionMultiplier * progress;
311          break;
312        case kOpen:
313          preferred_width += text_width_;
314          break;
315        case kClosing:
316          preferred_width += text_width_ * kInMotionMultiplier * (1 - progress);
317          break;
318        default:
319          // Do nothing.
320          break;
321      }
322    }
323  }
324  return preferred_width;
325}
326
327void ContentSettingDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
328  if ([animation_ animationState] != kNoAnimation) {
329    // Draw the background. Cache the gradient.
330    if (!gradient_) {
331      // Colors chosen to match Windows code.
332      NSColor* start_color =
333          [NSColor colorWithCalibratedRed:1.0 green:0.97 blue:0.83 alpha:1.0];
334      NSColor* end_color =
335          [NSColor colorWithCalibratedRed:1.0 green:0.90 blue:0.68 alpha:1.0];
336      NSArray* color_array =
337          [NSArray arrayWithObjects:start_color, end_color, nil];
338      gradient_.reset([[NSGradient alloc] initWithColors:color_array]);
339    }
340
341    NSGraphicsContext* context = [NSGraphicsContext currentContext];
342    [context saveGraphicsState];
343
344    NSRectClip(frame);
345
346    frame = NSInsetRect(frame, 0.0, kBorderPadding);
347    [gradient_ drawInRect:frame angle:90.0];
348    NSColor* border_color =
349        [NSColor colorWithCalibratedRed:0.91 green:0.73 blue:0.4 alpha:1.0];
350    [border_color set];
351    NSFrameRect(frame);
352
353    // Draw the icon.
354    NSImage* icon = GetImage();
355    NSRect icon_rect = frame;
356    if (icon) {
357      icon_rect.origin.x += kIconMarginPadding;
358      icon_rect.size.width = [icon size].width;
359      ImageDecoration::DrawInFrame(icon_rect, control_view);
360    }
361
362    // Draw the text, clipped to fit on the right. While handling clipping,
363    // NSAttributedString's drawInRect: won't draw a word if it doesn't fit
364    // in the bounding box so instead use drawAtPoint: with a manual clip
365    // rect.
366    NSRect remainder = frame;
367    remainder.origin.x = NSMaxX(icon_rect);
368    NSInsetRect(remainder, kTextMarginPadding, kTextMarginPadding);
369    // .get() needed to fix compiler warning (confusion with NSImageRep).
370    [animated_text_.get() drawAtPoint:remainder.origin];
371
372    [context restoreGraphicsState];
373  } else {
374    // No animation, draw the image as normal.
375    ImageDecoration::DrawInFrame(frame, control_view);
376  }
377}
378
379void ContentSettingDecoration::AnimationTimerFired() {
380  owner_->Layout();
381  // Even after the animation completes, the |animator_| object should be kept
382  // alive to prevent the animation from re-appearing if the page opens
383  // additional popups later. The animator will be cleared when the decoration
384  // hides, indicating something has changed with the TabContents (probably
385  // navigation).
386}
387