Chromium Code Reviews| Index: chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.mm |
| diff --git a/chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.mm b/chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.mm |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..0ca71741d7672baee51c8188997a49fadcd64d08 |
| --- /dev/null |
| +++ b/chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.mm |
| @@ -0,0 +1,334 @@ |
| +// Copyright 2013 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#import "chrome/browser/ui/cocoa/autofill/autofill_overlay_controller.h" |
| + |
| +#include "base/logging.h" |
| +#include "base/strings/sys_string_conversions.h" |
| +#include "base/timer/timer.h" |
| +#include "chrome/browser/ui/autofill/autofill_dialog_types.h" |
| +#include "chrome/browser/ui/autofill/autofill_dialog_view_delegate.h" |
| +#include "chrome/browser/ui/cocoa/autofill/autofill_dialog_constants.h" |
| +#include "skia/ext/skia_utils_mac.h" |
| +#include "ui/base/animation/animation_delegate.h" |
| +#include "ui/base/animation/multi_animation.h" |
| + |
| +namespace { |
| + |
| +// Spacing between lines of text in the overlay view. |
| +const int kOverlayTextInterlineSpacing = 10; |
|
sail
2013/09/04 20:04:06
CGFloat?, same below
groby-ooo-7-16
2013/09/04 20:41:45
Hm. Technically, they'll be shared with the views
|
| + |
| +// Spacing below image and above text messages in overlay view. |
| +const int kOverlayImageBottomMargin = 50; |
| + |
| +// TODO(groby): Unify colors with Views. |
| +// Slight shading for mouse hover and legal document background. |
| +SkColor kShadingColor = 0xfff2f2f2; |
| + |
| +// A border color for the legal document view. |
| +SkColor kSubtleBorderColor = 0xffdfdfdf; |
| + |
| +// Shorten a few long types. |
| +typedef ui::MultiAnimation::Part Part; |
| +typedef ui::MultiAnimation::Parts Parts; |
| + |
| +} // namespace |
| + |
| +// Bridges Objective C and C++ delegate interfaces. |
| +class AnimationDelegateBridge : public ui::AnimationDelegate { |
| + public: |
| + AnimationDelegateBridge(id<ChromiumAnimationDelegate>); |
|
sail
2013/09/04 21:18:51
argument name?
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + |
| + protected: |
| + // AnimationDelegate implementation. |
| + virtual void AnimationProgressed(const ui::Animation* animation) OVERRIDE; |
| + virtual void AnimationEnded(const ui::Animation* animation) OVERRIDE; |
| + |
| + private: |
| + id<ChromiumAnimationDelegate> delegate_; // Not owned. Owns DelegateBridge. |
| +}; |
|
sail
2013/09/04 21:18:51
DISALLOW_...
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + |
| +AnimationDelegateBridge::AnimationDelegateBridge( |
| + id<ChromiumAnimationDelegate> delegate) : delegate_(delegate) {} |
|
sail
2013/09/04 21:18:51
Instead of using a generic delegate protocol you c
groby-ooo-7-16
2013/09/04 22:19:01
There are more animations coming - this will be ho
sail
2013/09/04 23:08:25
ok
|
| + |
| +void AnimationDelegateBridge::AnimationProgressed( |
| + const ui::Animation* animation) { |
| + [delegate_ animationProgressed:animation]; |
| +} |
| + |
| +void AnimationDelegateBridge::AnimationEnded( |
| + const ui::Animation* animation) { |
| + [delegate_ animationEnded:animation]; |
| +} |
| + |
| +class OverlayTimerBridge { |
| + public: |
| + OverlayTimerBridge(AutofillOverlayController* controller); |
| + void SetExpiry(const base::TimeDelta& delta); |
| + |
| + private: |
| + void UpdateOverlayState(); |
| + |
| + base::Timer refresh_timer_; // Controls when it's time to refresh overlay. |
|
sail
2013/09/04 21:18:51
Use OneShotTimer instead?
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + |
| + // not owned, |overlay_view_controller_| owns |this|. |
| + AutofillOverlayController* overlay_view_controller_; |
| +}; |
|
sail
2013/09/04 21:18:51
DISALLOW_...
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + |
| +OverlayTimerBridge::OverlayTimerBridge(AutofillOverlayController* controller) |
| + : refresh_timer_(false, false), |
| + overlay_view_controller_(controller) { |
| +} |
| + |
| +void OverlayTimerBridge::SetExpiry(const base::TimeDelta& expiry) { |
| + if (expiry != base::TimeDelta()) { |
| + refresh_timer_.Start(FROM_HERE, |
| + expiry, |
| + base::Bind(&OverlayTimerBridge::UpdateOverlayState, |
| + base::Unretained(this))); |
| + } else { |
| + refresh_timer_.Stop(); |
| + } |
| +} |
| + |
| +void OverlayTimerBridge::UpdateOverlayState() { |
| + [overlay_view_controller_ updateState]; |
| +} |
| + |
| +// An NSView encapsulating the message stack and its custom drawn elements. |
| +@interface AutofillMessageStackView : NSView<AutofillLayout> |
| +- (CGFloat)getHeightForWidth:(CGFloat)width; |
| +- (void)setMessages: |
| + (const std::vector<autofill::DialogOverlayString>&)messages; |
| +@end |
| + |
| + |
| +@implementation AutofillMessageStackView |
| + |
| +- (void)drawRect:(NSRect)dirtyRect { |
| + NSColor* shadingColor = gfx::SkColorToCalibratedNSColor(kShadingColor); |
| + NSColor* borderColor = gfx::SkColorToCalibratedNSColor(kSubtleBorderColor); |
| + |
| + CGFloat arrowHalfWidth = kArrowWidth / 2.0; |
|
sail
2013/09/04 20:04:06
I know this was already there but could we put kAr
groby-ooo-7-16
2013/09/04 20:41:45
Will do in follow-up CL, since I'd otherwise have
|
| + NSRect bounds = [self bounds]; |
| + CGFloat y = NSMaxY(bounds) - kArrowHeight; |
| + |
| + NSBezierPath* arrow = [NSBezierPath bezierPath]; |
| + // Note that we purposely draw slightly outside of |bounds| so that the |
| + // stroke is hidden on the sides. |
|
sail
2013/09/04 21:18:51
"sides" -> "sides and bottom" ?
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + [arrow moveToPoint:NSMakePoint(NSMinX(bounds) - 1.0, y)]; |
|
sail
2013/09/04 21:18:51
Instead of adding and subtracting 1, how about doi
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + [arrow relativeLineToPoint:NSMakePoint(NSMidX(bounds) - arrowHalfWidth, 0)]; |
|
sail
2013/09/04 21:18:51
NSMidX(bounds) is absolute. To make it relative yo
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + [arrow relativeLineToPoint:NSMakePoint(arrowHalfWidth, kArrowHeight)]; |
| + [arrow relativeLineToPoint:NSMakePoint(arrowHalfWidth, -kArrowHeight)]; |
| + [arrow lineToPoint:NSMakePoint(NSMaxX(bounds) + 1.0, y)]; |
| + [arrow lineToPoint:NSMakePoint(NSMaxX(bounds) + 1.0, NSMinY(bounds) - 1.0)]; |
| + [arrow lineToPoint:NSMakePoint(NSMinX(bounds) - 1.0, NSMinY(bounds) - 1.0)]; |
| + [arrow closePath]; |
| + |
| + [shadingColor setFill]; |
| + [arrow fill]; |
| + [borderColor setStroke]; |
| + [arrow stroke]; |
| + |
| + [super drawRect:dirtyRect]; |
|
sail
2013/09/04 21:18:51
don't need
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| +} |
| + |
| +- (CGFloat)getHeightForWidth:(CGFloat)width { |
| + CGFloat height = kOverlayTextInterlineSpacing; |
| + for (NSTextView* label in [self subviews]) { |
| + height += NSHeight([label frame]); |
| + height += kOverlayTextInterlineSpacing; |
| + } |
| + return height + kArrowHeight; |
| +} |
| + |
| +- (void)setMessages: |
| + (const std::vector<autofill::DialogOverlayString>&) messages { |
| + // We probably want to look at other multi-line messages somewhere. |
| + base::scoped_nsobject<NSMutableArray> labels( |
| + [[NSMutableArray alloc] initWithCapacity:messages.size()]); |
| + for (size_t i = 0; i < messages.size(); ++i) { |
| + base::scoped_nsobject<NSTextField> label( |
| + [[NSTextField alloc] initWithFrame:NSZeroRect]); |
| + |
| + NSFont* labelFont = messages[i].font.GetNativeFont(); |
| + [label setEditable:NO]; |
| + [label setBordered:NO]; |
| + [label setDrawsBackground:NO]; |
| + [label setFont:labelFont]; |
| + [label setStringValue:base::SysUTF16ToNSString(messages[i].text)]; |
| + [label setTextColor:gfx::SkColorToDeviceNSColor(messages[i].text_color)]; |
| + DCHECK(messages[i].alignment == gfx::ALIGN_CENTER); |
|
sail
2013/09/04 21:18:51
DCHECK_EQ?
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + [label setAlignment:NSCenterTextAlignment]; |
| + [label sizeToFit]; |
| + |
| + [labels addObject:label]; |
| + } |
| + [self setSubviews:labels]; |
| + [self setHidden:([labels count] == 0)]; |
| +} |
| + |
| +- (void)performLayout { |
| + CGFloat y = |
| + NSMaxY([self bounds]) - kArrowHeight - kOverlayTextInterlineSpacing; |
| + for (NSTextView* label in [self subviews]) { |
| + CGFloat labelHeight = NSHeight([label frame]); |
|
sail
2013/09/04 21:18:51
DCHECK([label isKindOfClass:[NSTextField class])?
groby-ooo-7-16
2013/09/04 22:19:01
I suppose. Do we usually do that when iterating ov
sail
2013/09/04 23:08:25
Not usually. I'm just worried about the subviews b
groby-ooo-7-16
2013/09/05 00:44:20
Done.
|
| + [label setFrame:NSMakeRect(0, y - labelHeight, |
| + NSWidth([self bounds]), labelHeight)]; |
| + y = NSMinY([label frame]) - kOverlayTextInterlineSpacing; |
| + } |
| + DCHECK_GT(0.0, y); |
| +} |
| + |
| +- (NSSize)preferredSize { |
| + NOTREACHED(); |
| + return NSZeroSize; |
| +} |
| + |
| +@end |
| + |
| + |
| +@implementation AutofillOverlayController |
| + |
| +- (id)initWithDelegate:(autofill::AutofillDialogViewDelegate*)delegate { |
| + if (self = [super init]) { |
| + delegate_ = delegate; |
| + refreshTimer_.reset(new OverlayTimerBridge(self)); |
| + |
| + view_.reset([[NSBox alloc] initWithFrame:NSZeroRect]); |
| + [view_ setBoxType:NSBoxCustom]; |
| + [view_ setBorderType:NSNoBorder]; |
| + [view_ setContentViewMargins:NSZeroSize]; |
| + [view_ setTitlePosition:NSNoTitle]; |
| + |
| + childView_.reset([[NSView alloc] initWithFrame:NSZeroRect]); |
| + messageStackView_.reset( |
| + [[AutofillMessageStackView alloc] initWithFrame:NSZeroRect]); |
| + imageView_.reset([[NSImageView alloc] initWithFrame:NSZeroRect]); |
| + [imageView_ setImageAlignment:NSImageAlignCenter]; |
| + |
| + [childView_ setSubviews:@[messageStackView_, imageView_]]; |
| + [view_ addSubview:childView_]; |
| + |
| + [self setView:view_]; |
| + } |
| + return self; |
| +} |
| + |
| +- (void)updateState { |
| + const autofill::DialogOverlayState& state = delegate_->GetDialogOverlay(); |
|
sail
2013/09/04 21:18:51
move this below the if statement?
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + |
| + // Don't update anything if we're still fading out the old state. |
| + if (fadeOutAnimation_) |
| + return; |
| + |
| + if (state.image.IsEmpty()) { |
| + [view_ setHidden:YES]; |
| + return; |
| + } |
| + |
| + [view_ setFillColor:[[view_ window] backgroundColor]]; |
| + [view_ setAlphaValue:1]; |
| + [childView_ setAlphaValue:1]; |
| + [imageView_ setImage:state.image.ToNSImage()]; |
| + [messageStackView_ setMessages:state.strings]; |
| + [childView_ setHidden:NO]; |
| + [view_ setHidden:NO]; |
| + |
| + id delegate = [[[self view] window] windowController]; |
|
sail
2013/09/04 21:18:51
You can use the explicit type here since you're do
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + if ([delegate respondsToSelector:@selector(requestRelayout)]) |
|
sail
2013/09/04 21:18:51
I know that this pattern already existed in this c
groby-ooo-7-16
2013/09/04 22:19:01
Hm. I've been planning to actually have an Autofil
sail
2013/09/04 23:08:25
Ok, makes sense.
|
| + [delegate performSelector:@selector(requestRelayout)]; |
| + |
| + refreshTimer_->SetExpiry(state.expiry); |
| +} |
| + |
| +- (void)beginFadeOut { |
| + // Remove first responders, since focus rings show on top of overlay view. |
| + // TODO(groby): Figure out to do that less hacky. Ideally, the focus ring |
| + // should be part of the controls fading in, not appear at the end. |
| + [[self view] setNextResponder:[[[self view] window] firstResponder]]; |
| + [[[self view] window] makeFirstResponder:[self view]]; |
| + |
| + Parts parts; |
| + // For this part of the animation, simply show the splash image. |
| + parts.push_back(Part(autofill::kSplashDisplayDurationMs, ui::Tween::ZERO)); |
| + // For this part of the animation, fade out the splash image. |
| + parts.push_back( |
| + Part(autofill::kSplashFadeOutDurationMs, ui::Tween::EASE_IN)); |
| + // For this part of the animation, fade out |this| (fade in the dialog). |
| + parts.push_back( |
| + Part(autofill::kSplashFadeInDialogDurationMs, ui::Tween::EASE_OUT)); |
| + fadeOutAnimation_.reset( |
| + new ui::MultiAnimation(parts, |
| + ui::MultiAnimation::GetDefaultTimerInterval())); |
| + animationDelegate_.reset(new AnimationDelegateBridge(self)); |
| + fadeOutAnimation_->set_delegate(animationDelegate_.get()); |
| + fadeOutAnimation_->set_continuous(false); |
| + fadeOutAnimation_->Start(); |
| +} |
| + |
| +- (int)getHeightForWidth:(int) width { |
| + // 0 means "no preference". Image-only overlays fit the container. |
| + if ([messageStackView_ isHidden]) |
| + return 0; |
| + |
| + // Overlays with text messages express a size preference. |
| + return kOverlayImageBottomMargin + |
| + [messageStackView_ getHeightForWidth:width] + |
| + NSHeight([imageView_ frame]); |
| + |
| + return 0; |
|
sail
2013/09/04 21:18:51
remove?
groby-ooo-7-16
2013/09/04 22:19:01
Indeed. Surprised clang didn't yell at me. Thanks
|
| +} |
| + |
| +- (NSSize)preferredSize { |
| + NOTREACHED(); // Only implemented as part of AutofillLayout protocol. |
| + return NSZeroSize; |
| +} |
| + |
| +- (void)performLayout { |
| + NSRect bounds = [view_ bounds]; |
| + [childView_ setFrame:bounds]; |
| + if ([messageStackView_ isHidden]) { |
| + [imageView_ setFrame:bounds]; |
| + return; |
| + } |
| + |
| + int messageHeight = [messageStackView_ getHeightForWidth:NSWidth(bounds)]; |
| + [messageStackView_ setFrame: |
| + NSMakeRect(0, 0, NSWidth(bounds), messageHeight)]; |
| + [messageStackView_ performLayout]; |
| + |
| + NSSize imageSize = [[imageView_ image] size]; |
| + [imageView_ setFrame:NSMakeRect( |
| + 0, NSMaxY([messageStackView_ frame]) + kOverlayImageBottomMargin, |
| + NSWidth(bounds), imageSize.height)]; |
| +} |
| + |
| +- (void)animationProgressed:(const ui::Animation*) animation { |
|
sail
2013/09/04 21:18:51
no space before animation
groby-ooo-7-16
2013/09/04 22:19:01
Done.
|
| + DCHECK_EQ(animation, fadeOutAnimation_.get()); |
| + |
| + // Fade out children in stage 1. |
| + if (fadeOutAnimation_->current_part_index() == 1) { |
| + [childView_ setAlphaValue:(1 - fadeOutAnimation_->GetCurrentValue())]; |
| + } |
| + |
| + // Fade out background in stage 2(i.e. fade in what's behind |this|). |
| + if (fadeOutAnimation_->current_part_index() == 2) { |
| + [view_ setAlphaValue: (1 - fadeOutAnimation_->GetCurrentValue())]; |
| + [childView_ setHidden:YES]; |
| + } |
| + |
| + // If any fading was done, refresh display. |
| + if (fadeOutAnimation_->current_part_index() != 0) { |
| + [view_ setNeedsDisplay:YES]; |
| + } |
| +} |
| + |
| +- (void)animationEnded:(const ui::Animation*)animation { |
| + DCHECK_EQ(animation, fadeOutAnimation_.get()); |
| + [[self view] setHidden:YES]; |
| + fadeOutAnimation_.reset(); |
| +} |
| + |
| +@end |