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