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.
5 #import "chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h"
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 "content/public/browser/web_contents.h"
22 #include "grit/theme_resources.h"
23 #include "net/base/net_util.h"
24 #include "ui/base/cocoa/appkit_utils.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/resource/resource_bundle.h"
27 #include "ui/gfx/image/image.h"
28 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
30 using content::WebContents;
34 // The bubble point should look like it points to the bottom of the respective
35 // icon. The offset should be 2px.
36 const CGFloat kPageBubblePointYOffset = 2.0;
38 // Duration of animation, 3 seconds. The ContentSettingAnimationState breaks
39 // this up into different states of varying lengths.
40 const NSTimeInterval kAnimationDuration = 3.0;
42 // Interval of the animation timer, 60Hz.
43 const NSTimeInterval kAnimationInterval = 1.0 / 60.0;
45 // The % of time it takes to open or close the animating text, ie at 0.2, the
46 // opening takes 20% of the whole animation and the closing takes 20%. The
47 // remainder of the animation is with the text at full width.
48 const double kInMotionInterval = 0.2;
50 // Used to create a % complete of the "in motion" part of the animation, eg
51 // it should be 1.0 (100%) when the progress is 0.2.
52 const double kInMotionMultiplier = 1.0 / kInMotionInterval;
54 // Padding for the animated text with respect to the image.
55 const CGFloat kTextMarginPadding = 4;
56 const CGFloat kIconMarginPadding = 2;
57 const CGFloat kBorderPadding = 3;
59 // Different states in which the animation can be. In |kOpening|, the text
60 // is getting larger. In |kOpen|, the text should be displayed at full size.
61 // In |kClosing|, the text is again getting smaller. The durations in which
62 // the animation remains in each state are internal to
63 // |ContentSettingAnimationState|.
74 // An ObjC class that handles the multiple states of the text animation and
75 // bridges NSTimer calls back to the ContentSettingDecoration that owns it.
76 // Should be lazily instantiated to only exist when the decoration requires
78 // NOTE: One could make this class more generic, but this class only exists
79 // because CoreAnimation cannot be used (there are no views to work with).
80 @interface ContentSettingAnimationState : NSObject {
82 ContentSettingDecoration* owner_; // Weak, owns this.
83 double progress_; // Counter, [0..1], with aninmation progress.
84 NSTimer* timer_; // Animation timer. Owns this, owned by the run loop.
87 // [0..1], the current progress of the animation. -animationState will return
88 // |kNoAnimation| when progress is <= 0 or >= 1. Useful when state is
89 // |kOpening| or |kClosing| as a multiplier for displaying width. Don't use
90 // to track state transitions, use -animationState instead.
91 @property (readonly, nonatomic) double progress;
93 // Designated initializer. |owner| must not be nil. Animation timer will start
94 // as soon as the object is created.
95 - (id)initWithOwner:(ContentSettingDecoration*)owner;
97 // Returns the current animation state based on how much time has elapsed.
98 - (AnimationState)animationState;
100 // Call when |owner| is going away or the animation needs to be stopped.
101 // Ensures that any dangling references are cleared. Can be called multiple
103 - (void)stopAnimation;
107 @implementation ContentSettingAnimationState
109 @synthesize progress = progress_;
111 - (id)initWithOwner:(ContentSettingDecoration*)owner {
115 timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationInterval
117 selector:@selector(timerFired:)
129 // Clear weak references and stop the timer.
130 - (void)stopAnimation {
136 // Returns the current state based on how much time has elapsed.
137 - (AnimationState)animationState {
138 if (progress_ <= 0.0 || progress_ >= 1.0)
140 if (progress_ <= kInMotionInterval)
142 if (progress_ >= 1.0 - kInMotionInterval)
147 - (void)timerFired:(NSTimer*)timer {
148 // Increment animation progress, normalized to [0..1].
149 progress_ += kAnimationInterval / kAnimationDuration;
150 progress_ = std::min(progress_, 1.0);
151 owner_->AnimationTimerFired();
152 // Stop timer if it has reached the end of its life.
153 if (progress_ >= 1.0)
154 [self stopAnimation];
160 ContentSettingDecoration::ContentSettingDecoration(
161 ContentSettingsType settings_type,
162 LocationBarViewMac* owner,
164 : content_setting_image_model_(
165 ContentSettingImageModel::CreateContentSettingImageModel(
172 ContentSettingDecoration::~ContentSettingDecoration() {
173 // Just in case the timer is still holding onto the animation object, force
174 // cleanup so it can't get back to |this|.
175 [animation_ stopAnimation];
178 bool ContentSettingDecoration::UpdateFromWebContents(
179 WebContents* web_contents) {
180 bool was_visible = IsVisible();
181 int old_icon = content_setting_image_model_->get_icon();
182 content_setting_image_model_->UpdateFromWebContents(web_contents);
183 SetVisible(content_setting_image_model_->is_visible());
184 bool decoration_changed = was_visible != IsVisible() ||
185 old_icon != content_setting_image_model_->get_icon();
187 // TODO(thakis): We should use pdfs for these icons on OSX.
188 // http://crbug.com/35847
189 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
190 SetImage(rb.GetNativeImageNamed(
191 content_setting_image_model_->get_icon()).ToNSImage());
192 SetToolTip(base::SysUTF8ToNSString(
193 content_setting_image_model_->get_tooltip()));
195 // Check if there is an animation and start it if it hasn't yet started.
196 bool has_animated_text =
197 content_setting_image_model_->explanatory_string_id();
199 // Check if the animation has already run.
200 TabSpecificContentSettings* content_settings =
201 TabSpecificContentSettings::FromWebContents(web_contents);
202 ContentSettingsType content_type =
203 content_setting_image_model_->get_content_settings_type();
204 bool ran_animation = content_settings->IsBlockageIndicated(content_type);
206 if (has_animated_text && !ran_animation && !animation_) {
207 // Mark the animation as having been run.
208 content_settings->SetBlockageHasBeenIndicated(content_type);
209 // Start animation, its timer will drive reflow. Note the text is
210 // cached so it is not allowed to change during the animation.
212 [[ContentSettingAnimationState alloc] initWithOwner:this]);
213 animated_text_ = CreateAnimatedText();
214 text_width_ = MeasureTextWidth();
215 } else if (!has_animated_text) {
216 // Decoration no longer has animation, stop it (ok to always do this).
217 [animation_ stopAnimation];
221 // Decoration no longer visible, stop/clear animation.
222 [animation_ stopAnimation];
223 animation_.reset(nil);
225 return decoration_changed;
228 CGFloat ContentSettingDecoration::MeasureTextWidth() {
229 return [animated_text_ size].width;
232 base::scoped_nsobject<NSAttributedString>
233 ContentSettingDecoration::CreateAnimatedText() {
235 l10n_util::GetNSString(
236 content_setting_image_model_->explanatory_string_id());
237 base::scoped_nsobject<NSMutableParagraphStyle> style(
238 [[NSMutableParagraphStyle alloc] init]);
239 // Set line break mode to clip the text, otherwise drawInRect: won't draw a
240 // word if it doesn't fit in the bounding box.
241 [style setLineBreakMode:NSLineBreakByClipping];
242 NSDictionary* attributes = @{ NSFontAttributeName : GetFont(),
243 NSParagraphStyleAttributeName : style };
244 return base::scoped_nsobject<NSAttributedString>(
245 [[NSAttributedString alloc] initWithString:text attributes:attributes]);
248 NSPoint ContentSettingDecoration::GetBubblePointInFrame(NSRect frame) {
249 // Compute the frame as if there is no animation pill in the Omnibox. Place
250 // the bubble where the icon would be without animation, so when the animation
251 // ends, the bubble is pointing in the right place.
252 NSSize image_size = [GetImage() size];
253 frame.origin.x += frame.size.width - image_size.width;
254 frame.size = image_size;
256 const NSRect draw_frame = GetDrawRectInFrame(frame);
257 return NSMakePoint(NSMidX(draw_frame),
258 NSMaxY(draw_frame) + kPageBubblePointYOffset);
261 bool ContentSettingDecoration::AcceptsMousePress() {
265 bool ContentSettingDecoration::OnMousePressed(NSRect frame, NSPoint location) {
266 // Get host. This should be shared on linux/win/osx medium-term.
267 Browser* browser = owner_->browser();
268 WebContents* web_contents = owner_->GetWebContents();
272 // Find point for bubble's arrow in screen coordinates.
273 // TODO(shess): |owner_| is only being used to fetch |field|.
274 // Consider passing in |control_view|. Or refactoring to be
275 // consistent with other decorations (which don't currently bring up
276 // their bubble directly).
277 AutocompleteTextField* field = owner_->GetAutocompleteTextField();
278 NSPoint anchor = GetBubblePointInFrame(frame);
279 anchor = [field convertPoint:anchor toView:nil];
280 anchor = [[field window] convertBaseToScreen:anchor];
283 ContentSettingBubbleModel* model =
284 ContentSettingBubbleModel::CreateContentSettingBubbleModel(
285 browser->content_setting_bubble_model_delegate(),
286 web_contents, profile_,
287 content_setting_image_model_->get_content_settings_type());
288 [ContentSettingBubbleController showForModel:model
289 webContents:web_contents
290 parentWindow:[field window]
295 NSString* ContentSettingDecoration::GetToolTip() {
296 return tooltip_.get();
299 void ContentSettingDecoration::SetToolTip(NSString* tooltip) {
300 tooltip_.reset([tooltip retain]);
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.
305 CGFloat 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.
316 preferred_width += text_width_ * kInMotionMultiplier * progress;
319 preferred_width += text_width_;
322 preferred_width += text_width_ * kInMotionMultiplier * (1 - progress);
330 return preferred_width;
333 void 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);
342 NSImage* icon = GetImage();
343 NSRect icon_rect = background_rect;
345 icon_rect.origin.x += kIconMarginPadding;
346 icon_rect.size.width = [icon size].width;
347 ImageDecoration::DrawInFrame(icon_rect, control_view);
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);
355 // No animation, draw the image as normal.
356 ImageDecoration::DrawInFrame(frame, control_view);
360 void ContentSettingDecoration::AnimationTimerFired() {
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