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