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