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