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