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