OLD | NEW |
(Empty) | |
| 1 // Copyright 2013 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/autofill/autofill_overlay_controller.h" |
| 6 |
| 7 #include "base/logging.h" |
| 8 #include "base/mac/foundation_util.h" |
| 9 #include "base/strings/sys_string_conversions.h" |
| 10 #include "base/timer/timer.h" |
| 11 #include "chrome/browser/ui/autofill/autofill_dialog_types.h" |
| 12 #include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h" |
| 13 #include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h" |
| 14 #include "skia/ext/skia_utils_mac.h" |
| 15 #include "ui/base/animation/animation_delegate.h" |
| 16 #include "ui/base/animation/multi_animation.h" |
| 17 |
| 18 namespace { |
| 19 |
| 20 // Spacing between lines of text in the overlay view. |
| 21 const CGFloat kOverlayTextInterlineSpacing = 10; |
| 22 |
| 23 // Spacing below image and above text messages in overlay view. |
| 24 const CGFloat kOverlayImageBottomMargin = 50; |
| 25 |
| 26 // TODO(groby): Unify colors with Views. |
| 27 // Slight shading for mouse hover and legal document background. |
| 28 SkColor kShadingColor = 0xfff2f2f2; |
| 29 |
| 30 // A border color for the legal document view. |
| 31 SkColor kSubtleBorderColor = 0xffdfdfdf; |
| 32 |
| 33 // Shorten a few long types. |
| 34 typedef ui::MultiAnimation::Part Part; |
| 35 typedef ui::MultiAnimation::Parts Parts; |
| 36 |
| 37 } // namespace |
| 38 |
| 39 // Bridges Objective C and C++ delegate interfaces. |
| 40 class AnimationDelegateBridge : public ui::AnimationDelegate { |
| 41 public: |
| 42 AnimationDelegateBridge(id<AnimationDelegate>); |
| 43 |
| 44 protected: |
| 45 // AnimationDelegate implementation. |
| 46 virtual void AnimationProgressed(const ui::Animation* animation) OVERRIDE; |
| 47 virtual void AnimationEnded(const ui::Animation* animation) OVERRIDE; |
| 48 |
| 49 private: |
| 50 id<AnimationDelegate> delegate_; // Not owned. Owns DelegateBridge. |
| 51 }; |
| 52 |
| 53 AnimationDelegateBridge::AnimationDelegateBridge( |
| 54 id<AnimationDelegate> delegate) : delegate_(delegate) {} |
| 55 |
| 56 void AnimationDelegateBridge::AnimationProgressed( |
| 57 const ui::Animation* animation) { |
| 58 [delegate_ animationProgressed:animation]; |
| 59 } |
| 60 |
| 61 void AnimationDelegateBridge::AnimationEnded( |
| 62 const ui::Animation* animation) { |
| 63 [delegate_ animationEnded:animation]; |
| 64 } |
| 65 |
| 66 class OverlayTimerBridge { |
| 67 public: |
| 68 OverlayTimerBridge(AutofillOverlayController* controller); |
| 69 void SetExpiry(const base::TimeDelta& delta); |
| 70 |
| 71 private: |
| 72 void UpdateOverlayState(); |
| 73 |
| 74 base::Timer refresh_timer_; // Controls when it's time to refresh overlay. |
| 75 |
| 76 // not owned, |overlay_view_controller_| owns |this|. |
| 77 AutofillOverlayController* overlay_view_controller_; |
| 78 }; |
| 79 |
| 80 OverlayTimerBridge::OverlayTimerBridge(AutofillOverlayController* controller) |
| 81 : refresh_timer_(false, false), |
| 82 overlay_view_controller_(controller) { |
| 83 } |
| 84 |
| 85 void OverlayTimerBridge::SetExpiry(const base::TimeDelta& expiry) { |
| 86 if (expiry != base::TimeDelta()) { |
| 87 refresh_timer_.Start(FROM_HERE, |
| 88 expiry, |
| 89 base::Bind(&OverlayTimerBridge::UpdateOverlayState, |
| 90 base::Unretained(this))); |
| 91 } else { |
| 92 refresh_timer_.Stop(); |
| 93 } |
| 94 } |
| 95 |
| 96 void OverlayTimerBridge::UpdateOverlayState() { |
| 97 [overlay_view_controller_ updateState]; |
| 98 } |
| 99 |
| 100 // An NSView encapsulating the message stack and its custom drawn elements. |
| 101 @interface AutofillMessageStackView : NSView<AutofillLayout> |
| 102 - (CGFloat)heightForWidth:(CGFloat)width; |
| 103 - (void)setMessages: |
| 104 (const std::vector<autofill::DialogOverlayString>&)messages; |
| 105 @end |
| 106 |
| 107 |
| 108 @implementation AutofillMessageStackView |
| 109 |
| 110 - (void)drawRect:(NSRect)dirtyRect { |
| 111 NSColor* shadingColor = gfx::SkColorToCalibratedNSColor(kShadingColor); |
| 112 NSColor* borderColor = gfx::SkColorToCalibratedNSColor(kSubtleBorderColor); |
| 113 |
| 114 CGFloat arrowHalfWidth = kArrowWidth / 2.0; |
| 115 NSRect bounds = [self bounds]; |
| 116 CGFloat y = NSMaxY(bounds) - kArrowHeight; |
| 117 |
| 118 NSBezierPath* arrow = [NSBezierPath bezierPath]; |
| 119 // Note that we purposely draw slightly outside of |bounds| so that the |
| 120 // stroke is hidden on the sides. |
| 121 [arrow moveToPoint:NSMakePoint(NSMinX(bounds) - 1.0, y)]; |
| 122 [arrow relativeLineToPoint:NSMakePoint(NSMidX(bounds) - arrowHalfWidth, 0)]; |
| 123 [arrow relativeLineToPoint:NSMakePoint(arrowHalfWidth, kArrowHeight)]; |
| 124 [arrow relativeLineToPoint:NSMakePoint(arrowHalfWidth, -kArrowHeight)]; |
| 125 [arrow lineToPoint:NSMakePoint(NSMaxX(bounds) + 1.0, y)]; |
| 126 [arrow lineToPoint:NSMakePoint(NSMaxX(bounds) + 1.0, NSMinY(bounds) - 1.0)]; |
| 127 [arrow lineToPoint:NSMakePoint(NSMinX(bounds) - 1.0, NSMinY(bounds) - 1.0)]; |
| 128 [arrow closePath]; |
| 129 |
| 130 [shadingColor setFill]; |
| 131 [arrow fill]; |
| 132 [borderColor setStroke]; |
| 133 [arrow stroke]; |
| 134 |
| 135 [super drawRect:dirtyRect]; |
| 136 } |
| 137 |
| 138 - (CGFloat)heightForWidth:(CGFloat)width { |
| 139 CGFloat height = kOverlayTextInterlineSpacing; |
| 140 for (NSTextView* label in [self subviews]) { |
| 141 height += NSHeight([label frame]); |
| 142 height += kOverlayTextInterlineSpacing; |
| 143 } |
| 144 return height + kArrowHeight; |
| 145 } |
| 146 |
| 147 - (void)setMessages: |
| 148 (const std::vector<autofill::DialogOverlayString>&) messages { |
| 149 // We probably want to look at other multi-line messages somewhere. |
| 150 base::scoped_nsobject<NSMutableArray> labels( |
| 151 [[NSMutableArray alloc] initWithCapacity:messages.size()]); |
| 152 for (size_t i = 0; i < messages.size(); ++i) { |
| 153 base::scoped_nsobject<NSTextField> label( |
| 154 [[NSTextField alloc] initWithFrame:NSZeroRect]); |
| 155 |
| 156 NSFont* labelFont = messages[i].font.GetNativeFont(); |
| 157 [label setEditable:NO]; |
| 158 [label setBordered:NO]; |
| 159 [label setDrawsBackground:NO]; |
| 160 [label setFont:labelFont]; |
| 161 [label setStringValue:base::SysUTF16ToNSString(messages[i].text)]; |
| 162 [label setTextColor:gfx::SkColorToDeviceNSColor(messages[i].text_color)]; |
| 163 DCHECK(messages[i].alignment == gfx::ALIGN_CENTER); |
| 164 [label setAlignment:NSCenterTextAlignment]; |
| 165 [label sizeToFit]; |
| 166 |
| 167 [labels addObject:label]; |
| 168 } |
| 169 [self setSubviews:labels]; |
| 170 [self setHidden:([labels count] == 0)]; |
| 171 } |
| 172 |
| 173 - (void)performLayout { |
| 174 CGFloat y = |
| 175 NSMaxY([self bounds]) - kArrowHeight - kOverlayTextInterlineSpacing; |
| 176 for (NSTextView* label in [self subviews]) { |
| 177 CGFloat labelHeight = NSHeight([label frame]); |
| 178 [label setFrame:NSMakeRect(0, y - labelHeight, |
| 179 NSWidth([self bounds]), labelHeight)]; |
| 180 y = NSMinY([label frame]) - kOverlayTextInterlineSpacing; |
| 181 } |
| 182 DCHECK_GT(0.0, y); |
| 183 } |
| 184 |
| 185 - (NSSize)preferredSize { |
| 186 NOTREACHED(); |
| 187 return NSZeroSize; |
| 188 } |
| 189 |
| 190 @end |
| 191 |
| 192 |
| 193 @implementation AutofillOverlayController |
| 194 |
| 195 - (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate { |
| 196 if (self = [super initWithNibName:nil bundle:nil]) { |
| 197 delegate_ = delegate; |
| 198 refreshTimer_.reset(new OverlayTimerBridge(self)); |
| 199 |
| 200 base::scoped_nsobject<NSBox> view( |
| 201 [[NSBox alloc] initWithFrame:NSZeroRect]); |
| 202 [view setBoxType:NSBoxCustom]; |
| 203 [view setBorderType:NSNoBorder]; |
| 204 [view setContentViewMargins:NSZeroSize]; |
| 205 [view setTitlePosition:NSNoTitle]; |
| 206 |
| 207 childView_.reset([[NSView alloc] initWithFrame:NSZeroRect]); |
| 208 messageStackView_.reset( |
| 209 [[AutofillMessageStackView alloc] initWithFrame:NSZeroRect]); |
| 210 imageView_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]); |
| 211 [imageView_ setImageAlignment:NSImageAlignCenter]; |
| 212 |
| 213 [childView_ setSubviews:@[messageStackView_, imageView_]]; |
| 214 [view addSubview:childView_]; |
| 215 [self setView:view]; |
| 216 } |
| 217 return self; |
| 218 } |
| 219 |
| 220 - (void)updateState { |
| 221 const autofill::DialogOverlayState& state = delegate_->GetDialogOverlay(); |
| 222 |
| 223 // Don't update anything if we're still fading out the old state. |
| 224 if (fadeOutAnimation_) |
| 225 return; |
| 226 |
| 227 if (state.image.IsEmpty()) { |
| 228 [[self view] setHidden:YES]; |
| 229 return; |
| 230 } |
| 231 |
| 232 NSBox* view = base::mac::ObjCCastStrict<NSBox>([self view]); |
| 233 [view setFillColor:[[view window] backgroundColor]]; |
| 234 [view setAlphaValue:1]; |
| 235 [childView_ setAlphaValue:1]; |
| 236 [imageView_ setImage:state.image.ToNSImage()]; |
| 237 [messageStackView_ setMessages:state.strings]; |
| 238 [childView_ setHidden:NO]; |
| 239 [view setHidden:NO]; |
| 240 |
| 241 id delegate = [[[self view] window] windowController]; |
| 242 if ([delegate respondsToSelector:@selector(requestRelayout)]) |
| 243 [delegate performSelector:@selector(requestRelayout)]; |
| 244 |
| 245 refreshTimer_->SetExpiry(state.expiry); |
| 246 } |
| 247 |
| 248 - (void)beginFadeOut { |
| 249 // Remove first responders, since focus rings show on top of overlay view. |
| 250 // TODO(groby): Figure out to do that less hacky. Ideally, the focus ring |
| 251 // should be part of the controls fading in, not appear at the end. |
| 252 [[self view] setNextResponder:[[[self view] window] firstResponder]]; |
| 253 [[[self view] window] makeFirstResponder:[self view]]; |
| 254 |
| 255 Parts parts; |
| 256 // For this part of the animation, simply show the splash image. |
| 257 parts.push_back(Part(autofill::kSplashDisplayDurationMs, ui::Tween::ZERO)); |
| 258 // For this part of the animation, fade out the splash image. |
| 259 parts.push_back( |
| 260 Part(autofill::kSplashFadeOutDurationMs, ui::Tween::EASE_IN)); |
| 261 // For this part of the animation, fade out |this| (fade in the dialog). |
| 262 parts.push_back( |
| 263 Part(autofill::kSplashFadeInDialogDurationMs, ui::Tween::EASE_OUT)); |
| 264 fadeOutAnimation_.reset( |
| 265 new ui::MultiAnimation(parts, |
| 266 ui::MultiAnimation::GetDefaultTimerInterval())); |
| 267 animationDelegate_.reset(new AnimationDelegateBridge(self)); |
| 268 fadeOutAnimation_->set_delegate(animationDelegate_.get()); |
| 269 fadeOutAnimation_->set_continuous(false); |
| 270 fadeOutAnimation_->Start(); |
| 271 } |
| 272 |
| 273 - (int)heightForWidth:(int) width { |
| 274 // 0 means "no preference". Image-only overlays fit the container. |
| 275 if ([messageStackView_ isHidden]) |
| 276 return 0; |
| 277 |
| 278 // Overlays with text messages express a size preference. |
| 279 return kOverlayImageBottomMargin + |
| 280 [messageStackView_ heightForWidth:width] + |
| 281 NSHeight([imageView_ frame]); |
| 282 |
| 283 return 0; |
| 284 } |
| 285 |
| 286 - (NSSize)preferredSize { |
| 287 NOTREACHED(); // Only implemented as part of AutofillLayout protocol. |
| 288 return NSZeroSize; |
| 289 } |
| 290 |
| 291 - (void)performLayout { |
| 292 NSRect bounds = [[self view] bounds]; |
| 293 [childView_ setFrame:bounds]; |
| 294 if ([messageStackView_ isHidden]) { |
| 295 [imageView_ setFrame:bounds]; |
| 296 return; |
| 297 } |
| 298 |
| 299 int messageHeight = [messageStackView_ heightForWidth:NSWidth(bounds)]; |
| 300 [messageStackView_ setFrame: |
| 301 NSMakeRect(0, 0, NSWidth(bounds), messageHeight)]; |
| 302 [messageStackView_ performLayout]; |
| 303 |
| 304 NSSize imageSize = [[imageView_ image] size]; |
| 305 [imageView_ setFrame:NSMakeRect( |
| 306 0, NSMaxY([messageStackView_ frame]) + kOverlayImageBottomMargin, |
| 307 NSWidth(bounds), imageSize.height)]; |
| 308 } |
| 309 |
| 310 - (void)animationProgressed:(const ui::Animation*) animation { |
| 311 DCHECK_EQ(animation, fadeOutAnimation_.get()); |
| 312 |
| 313 // Fade out children in stage 1. |
| 314 if (fadeOutAnimation_->current_part_index() == 1) { |
| 315 [childView_ setAlphaValue:(1 - fadeOutAnimation_->GetCurrentValue())]; |
| 316 } |
| 317 |
| 318 // Fade out background in stage 2(i.e. fade in what's behind |this|). |
| 319 if (fadeOutAnimation_->current_part_index() == 2) { |
| 320 [[self view] setAlphaValue: (1 - fadeOutAnimation_->GetCurrentValue())]; |
| 321 [childView_ setHidden:YES]; |
| 322 } |
| 323 |
| 324 // If any fading was done, refresh display. |
| 325 if (fadeOutAnimation_->current_part_index() != 0) { |
| 326 [[self view] setNeedsDisplay:YES]; |
| 327 } |
| 328 } |
| 329 |
| 330 - (void)animationEnded:(const ui::Animation*)animation { |
| 331 DCHECK_EQ(animation, fadeOutAnimation_.get()); |
| 332 [[self view] setHidden:YES]; |
| 333 fadeOutAnimation_.reset(); |
| 334 } |
| 335 |
| 336 @end |
OLD | NEW |