Home | History | Annotate | Download | only in location_bar
      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