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 27 namespace { 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. 31 const CGFloat kPopupPointYOffset = 2.0; 32 33 // Duration of animation, 3 seconds. The ContentSettingAnimationState breaks 34 // this up into different states of varying lengths. 35 const NSTimeInterval kAnimationDuration = 3.0; 36 37 // Interval of the animation timer, 60Hz. 38 const 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. 43 const 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. 47 const double kInMotionMultiplier = 1.0 / kInMotionInterval; 48 49 // Padding for the animated text with respect to the image. 50 const CGFloat kTextMarginPadding = 4; 51 const CGFloat kIconMarginPadding = 2; 52 const 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|. 59 enum 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 155 ContentSettingDecoration::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 167 ContentSettingDecoration::~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 173 bool 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 214 CGFloat 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. 220 NSAttributedString* 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 232 NSPoint 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 245 bool ContentSettingDecoration::AcceptsMousePress() { 246 return true; 247 } 248 249 bool 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 289 NSString* ContentSettingDecoration::GetToolTip() { 290 return tooltip_.get(); 291 } 292 293 void 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. 299 CGFloat 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 327 void 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 379 void 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