OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2016 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/security_state_bubble_decoration.h " | |
6 | |
7 #include <cmath> | |
8 | |
9 #import "base/mac/mac_util.h" | |
10 #include "base/strings/sys_string_conversions.h" | |
11 #import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" | |
12 #import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h" | |
13 #import "chrome/browser/ui/cocoa/themed_window.h" | |
14 #include "grit/theme_resources.h" | |
15 #include "skia/ext/skia_utils_mac.h" | |
16 #import "ui/base/cocoa/nsview_additions.h" | |
17 #include "ui/base/material_design/material_design_controller.h" | |
18 #include "ui/gfx/animation/tween.h" | |
19 #include "ui/gfx/color_palette.h" | |
20 #include "ui/gfx/font_list.h" | |
21 #include "ui/gfx/image/image_skia_util_mac.h" | |
22 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" | |
23 #include "ui/gfx/text_elider.h" | |
24 | |
25 // TODO(spqchan): Decorations that don't fit in the available space are | |
26 // omitted. See crbug.com/638427. | |
27 | |
28 namespace { | |
29 | |
30 // This is used to increase the right margin of this decoration. | |
31 const CGFloat kRightSideMargin = 1.0; | |
32 | |
33 // Padding between the icon and label. | |
34 CGFloat kIconLabelPadding = 4.0; | |
35 | |
36 // Inset for the background. | |
37 const CGFloat kBackgroundYInset = 4.0; | |
38 | |
39 // The offset of the text's baseline on a retina screen. | |
40 const CGFloat kRetinaBaselineOffset = 0.5; | |
41 | |
42 // The info-bubble point should look like it points to the bottom of the lock | |
43 // icon. Determined with Pixie.app. | |
44 const CGFloat kPageInfoBubblePointYOffset = 6.0; | |
45 | |
46 // Minimum acceptable width for the ev bubble. | |
47 const CGFloat kMinElidedBubbleWidth = 150.0; | |
48 | |
49 // Maximum amount of available space to make the bubble, subject to | |
50 // |kMinElidedBubbleWidth|. | |
51 const float kMaxBubbleFraction = 0.5; | |
52 | |
53 // The base text color used for non-MD. The color tuples are stolen from | |
54 // location_bar_view_gtk.cc. | |
55 const SkColor kBaseTextColor = SkColorSetRGB(0x07, 0x95, 0x00); | |
56 | |
57 // Duration of animation in ms. | |
58 const NSTimeInterval kInAnimationDuration = 0.330; | |
59 const NSTimeInterval kOutAnimationDuration = 0.250; | |
60 | |
61 // Interval of the animation timer, 60Hz. | |
62 const NSTimeInterval kAnimationInterval = 1.0 / 60.0; | |
63 | |
64 // Transformation values at the beginning of the animation. | |
65 const CGFloat kStartScale = 0.25; | |
66 const CGFloat kStartx_offset = -15.0; | |
67 | |
68 // Different states of the animation. In |kAnimationIn|, the verbose state | |
69 // animates in. In |kAnimationOut|, the verbose state animates out. | |
70 enum AnimationState { | |
71 kAnimationIn, | |
72 kAnimationOut, | |
73 }; | |
74 | |
75 } // namespace | |
76 | |
77 @interface SecurityStateAnimation : NSObject { | |
78 @private | |
79 SecurityStateBubbleDecoration* owner_; // Weak, owns this. | |
80 | |
81 // Counter, [0..1], with aninmation progress. | |
82 double progress_; | |
83 | |
84 // Animation timer. Owns this, owned by the run loop. | |
85 NSTimer* timer_; | |
86 | |
87 // The timestamp of the last time timerFired: was called. | |
88 NSTimeInterval frameTimestamp_; | |
89 } | |
90 | |
91 @property(readonly, nonatomic) AnimationState state; | |
92 | |
93 // Designated initializer. |owner| must not be nil. Animation timer will start | |
94 // as soon as the object is created. | |
95 - (instancetype)initWithOwner:(SecurityStateBubbleDecoration*)owner | |
96 state:(AnimationState)state; | |
97 | |
98 // Call when |owner| is going away or the animation needs to be stopped. | |
99 // Ensures that any dangling references are cleared. Can be called multiple | |
100 // times. | |
101 - (void)stopAnimation; | |
102 | |
103 // Returns the current progress of the animation with the curve applied to it. | |
104 // A value in the range of [0, 1]. | |
105 - (double)animationProgress; | |
106 | |
107 // Returns true if the animation is running. | |
108 - (BOOL)isRunning; | |
109 | |
110 @end | |
111 | |
112 @implementation SecurityStateAnimation | |
113 | |
114 @synthesize state = state_; | |
115 | |
116 - (instancetype)initWithOwner:(SecurityStateBubbleDecoration*)owner | |
117 state:(AnimationState)state { | |
118 DCHECK(ui::MaterialDesignController::IsModeMaterial()); | |
119 | |
120 if ((self = [super init])) { | |
121 owner_ = owner; | |
122 frameTimestamp_ = CACurrentMediaTime(); | |
123 timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationInterval | |
124 target:self | |
125 selector:@selector(timerFired:) | |
126 userInfo:nil | |
127 repeats:YES]; | |
128 // Add the timer to |NSRunLoopCommonModes| because it should run in | |
129 // |NSEventTrackingRunLoopMode| as well as |NSDefaultRunLoopMode|. This | |
130 // allows the timer to run when the user resizes the window via mouse drag. | |
131 [[NSRunLoop mainRunLoop] addTimer:timer_ forMode:NSRunLoopCommonModes]; | |
132 | |
133 state_ = state; | |
134 } | |
135 | |
136 return self; | |
137 } | |
138 | |
139 - (void)dealloc { | |
140 DCHECK(!timer_); | |
141 [super dealloc]; | |
142 } | |
143 | |
144 // Clear weak references and stop the timer. | |
145 - (void)stopAnimation { | |
146 [timer_ invalidate]; | |
147 timer_ = nil; | |
148 } | |
149 | |
150 - (BOOL)isRunning { | |
151 return timer_ != nil; | |
152 } | |
153 | |
154 - (void)timerFired:(NSTimer*)timer { | |
155 CGFloat currentTime = CACurrentMediaTime(); | |
156 CGFloat elapsedTime = currentTime - frameTimestamp_; | |
157 frameTimestamp_ = currentTime; | |
158 | |
159 // Increment animation progress, normalized to [0..1]. | |
160 CGFloat duration = | |
161 state_ == kAnimationIn ? kInAnimationDuration : kOutAnimationDuration; | |
162 progress_ += elapsedTime / duration; | |
163 progress_ = std::min(progress_, 1.0); | |
164 | |
165 // Stop timer if it has reached the end of its life. | |
166 if (progress_ >= 1.0) | |
167 [self stopAnimation]; | |
168 | |
169 owner_->OnAnimationProgressed(); | |
170 } | |
171 | |
172 - (double)animationProgress { | |
173 if (state_ == kAnimationIn) | |
174 return gfx::Tween::CalculateValue(gfx::Tween::FAST_OUT_SLOW_IN, progress_); | |
175 | |
176 return 1 - gfx::Tween::CalculateValue(gfx::Tween::FAST_OUT_SLOW_IN_EXPO, | |
177 progress_); | |
178 } | |
179 | |
180 @end | |
181 | |
182 ////////////////////////////////////////////////////////////////// | |
183 // SecurityStateBubbleDecoration, public: | |
184 | |
185 SecurityStateBubbleDecoration::SecurityStateBubbleDecoration( | |
186 LocationIconDecoration* location_icon, | |
187 LocationBarViewMac* owner) | |
188 : location_icon_(location_icon), | |
189 label_color_(gfx::kGoogleGreen700), | |
190 image_fade_(true), | |
191 owner_(owner) { | |
192 if (ui::MaterialDesignController::IsModeMaterial()) { | |
193 // On Retina the text label is 1px above the Omnibox textfield's text | |
194 // baseline. If the Omnibox textfield also drew the label the baselines | |
195 // would align. | |
196 SetRetinaBaselineOffset(kRetinaBaselineOffset); | |
197 | |
198 base::scoped_nsobject<NSMutableParagraphStyle> style( | |
199 [[NSMutableParagraphStyle alloc] init]); | |
200 [style setLineBreakMode:NSLineBreakByClipping]; | |
201 [attributes_ setObject:style forKey:NSParagraphStyleAttributeName]; | |
202 } else { | |
203 SetTextColor(skia::SkColorToSRGBNSColor(kBaseTextColor)); | |
204 } | |
205 } | |
206 | |
207 SecurityStateBubbleDecoration::~SecurityStateBubbleDecoration() { | |
208 // Just in case the timer is still holding onto the animation object, force | |
209 // cleanup so it can't get back to |this|. | |
210 [animation_ stopAnimation]; | |
211 animation_.reset(); | |
212 } | |
213 | |
214 void SecurityStateBubbleDecoration::SetFullLabel(NSString* label) { | |
215 full_label_.reset([[NSString alloc] initWithString:label]); | |
Robert Sesek
2016/08/18 19:33:59
[label copy] is what I had in mind, but this is fi
spqchan
2016/08/18 21:08:30
Done.
| |
216 SetLabel(full_label_); | |
217 } | |
218 | |
219 void SecurityStateBubbleDecoration::SetLabelColor(SkColor color) { | |
220 label_color_ = color; | |
221 } | |
222 | |
223 void SecurityStateBubbleDecoration::OnAnimationProgressed() { | |
224 owner_->Layout(); | |
225 } | |
226 | |
227 void SecurityStateBubbleDecoration::AnimateIn(bool image_fade) { | |
228 image_fade_ = image_fade; | |
229 animation_.reset( | |
230 [[SecurityStateAnimation alloc] initWithOwner:this state:kAnimationIn]); | |
231 } | |
232 | |
233 void SecurityStateBubbleDecoration::AnimateOut() { | |
234 if (!HasAnimatedIn()) | |
235 return; | |
236 | |
237 animation_.reset( | |
238 [[SecurityStateAnimation alloc] initWithOwner:this state:kAnimationOut]); | |
239 } | |
240 | |
241 bool SecurityStateBubbleDecoration::HasAnimatedIn() const { | |
242 return animation_.get() && [animation_ state] == kAnimationIn && | |
243 ![animation_ isRunning]; | |
244 } | |
245 | |
246 bool SecurityStateBubbleDecoration::HasAnimatedOut() const { | |
247 if (!animation_.get()) | |
248 return true; | |
249 | |
250 return animation_.get() && [animation_ state] == kAnimationOut && | |
251 ![animation_ isRunning]; | |
252 } | |
253 | |
254 bool SecurityStateBubbleDecoration::AnimatingOut() const { | |
255 return animation_.get() && [animation_ state] == kAnimationOut && | |
256 [animation_ isRunning]; | |
257 } | |
258 | |
259 void SecurityStateBubbleDecoration::ResetAndHide() { | |
260 SetVisible(false); | |
261 if (animation_.get() && [animation_ state] == kAnimationIn) | |
262 animation_.reset(); | |
263 } | |
264 | |
265 ////////////////////////////////////////////////////////////////// | |
266 // SecurityStateBubbleDecoration::LocationBarDecoration: | |
267 | |
268 CGFloat SecurityStateBubbleDecoration::GetWidthForSpace(CGFloat width) { | |
269 if (!ui::MaterialDesignController::IsModeMaterial()) | |
270 return GetWidthForText(width); | |
271 | |
272 CGFloat location_icon_width = location_icon_->GetWidthForSpace(width); | |
273 CGFloat text_width = GetWidthForText(width) - location_icon_width; | |
274 return (text_width * GetAnimationProgress()) + location_icon_width; | |
275 } | |
276 | |
277 void SecurityStateBubbleDecoration::DrawInFrame(NSRect frame, | |
278 NSView* control_view) { | |
279 const NSRect decoration_frame = NSInsetRect(frame, 0.0, kBackgroundYInset); | |
280 CGFloat text_offset = NSMinX(decoration_frame); | |
281 if (image_) { | |
282 // The image should fade in if we're animating in. | |
283 CGFloat image_alpha = | |
284 image_fade_ && animation_.get() && [animation_ state] == kAnimationIn | |
285 ? GetAnimationProgress() | |
286 : 1.0; | |
287 | |
288 // Center the image vertically. | |
289 const NSSize image_size = [image_ size]; | |
290 NSRect image_rect = decoration_frame; | |
291 image_rect.origin.y += | |
292 std::floor((NSHeight(decoration_frame) - image_size.height) / 2.0); | |
293 image_rect.size = image_size; | |
294 [image_ drawInRect:image_rect | |
295 fromRect:NSZeroRect // Entire image | |
296 operation:NSCompositeSourceOver | |
297 fraction:image_alpha | |
298 respectFlipped:YES | |
299 hints:nil]; | |
300 text_offset = NSMaxX(image_rect) + kIconLabelPadding; | |
301 } | |
302 | |
303 // Set the text color and draw the text. | |
304 if (label_) { | |
305 bool in_dark_mode = [[control_view window] inIncognitoModeWithSystemTheme]; | |
306 NSColor* text_color = | |
307 in_dark_mode ? skia::SkColorToSRGBNSColor(kMaterialDarkModeTextColor) | |
308 : GetBackgroundBorderColor(); | |
309 SetTextColor(text_color); | |
310 | |
311 // Transform the coordinate system to adjust the baseline on Retina. | |
312 // This is the only way to get fractional adjustments. | |
313 // gfx::ScopedNSGraphicsContextSaveGState save_graphics_state; | |
314 CGFloat line_width = [control_view cr_lineWidth]; | |
315 if (line_width < 1) { | |
316 NSAffineTransform* transform = [NSAffineTransform transform]; | |
317 [transform translateXBy:0 yBy:kRetinaBaselineOffset]; | |
318 [transform concat]; | |
319 } | |
320 | |
321 base::scoped_nsobject<NSAttributedString> text([[NSAttributedString alloc] | |
322 initWithString:label_ | |
323 attributes:attributes_]); | |
324 | |
325 // Calculate the text frame based on the text height and offsets. | |
326 NSRect text_rect = frame; | |
327 CGFloat textHeight = [text size].height; | |
328 | |
329 text_rect.origin.x = text_offset; | |
330 text_rect.origin.y = std::round(NSMidY(text_rect) - textHeight / 2.0) - 1; | |
331 text_rect.size.width = NSMaxX(decoration_frame) - NSMinX(text_rect); | |
332 text_rect.size.height = textHeight; | |
333 | |
334 NSAffineTransform* transform = [NSAffineTransform transform]; | |
335 CGFloat progress = GetAnimationProgress(); | |
336 | |
337 // Apply transformations so that the text animation: | |
338 // - Scales from 0.75 to 1. | |
339 // - Translates the X position to its origin after it got scaled, and | |
340 // before moving in a position from from -15 to 0 | |
341 // - Translates the Y position so that the text is centered vertically. | |
342 double scale = gfx::Tween::DoubleValueBetween(progress, kStartScale, 1.0); | |
343 | |
344 double x_origin_offset = NSMinX(text_rect) * (1 - scale); | |
345 double y_origin_offset = NSMinY(text_rect) * (1 - scale); | |
346 double x_offset = | |
347 gfx::Tween::DoubleValueBetween(progress, kStartx_offset, 0); | |
348 double y_offset = NSHeight(text_rect) * (1 - scale) / 2.0; | |
349 | |
350 [transform translateXBy:x_offset + x_origin_offset | |
351 yBy:y_offset + y_origin_offset]; | |
352 [transform scaleBy:scale]; | |
353 [transform concat]; | |
354 | |
355 // Draw the label. | |
356 [text drawInRect:text_rect]; | |
357 | |
358 // Draw the divider. | |
359 NSBezierPath* line = [NSBezierPath bezierPath]; | |
360 [line setLineWidth:line_width]; | |
361 [line moveToPoint:NSMakePoint(NSMaxX(decoration_frame) - DividerPadding(), | |
362 NSMinY(decoration_frame))]; | |
363 [line lineToPoint:NSMakePoint(NSMaxX(decoration_frame) - DividerPadding(), | |
364 NSMaxY(decoration_frame))]; | |
365 | |
366 NSColor* divider_color = GetDividerColor(in_dark_mode); | |
367 CGFloat divider_alpha = | |
368 [divider_color alphaComponent] * GetAnimationProgress(); | |
369 divider_color = [divider_color colorWithAlphaComponent:divider_alpha]; | |
370 [divider_color set]; | |
371 [line stroke]; | |
372 } | |
373 } | |
374 | |
375 void SecurityStateBubbleDecoration::DrawWithBackgroundInFrame( | |
376 NSRect background_frame, | |
377 NSRect frame, | |
378 NSView* control_view) { | |
379 if (!ui::MaterialDesignController::IsModeMaterial()) { | |
380 BubbleDecoration::DrawWithBackgroundInFrame(background_frame, frame, | |
381 control_view); | |
382 return; | |
383 } | |
384 | |
385 NSRect rect = NSInsetRect(background_frame, 0, 3); | |
386 rect.size.width -= kRightSideMargin; | |
387 | |
388 CGFloat line_width = [control_view cr_lineWidth]; | |
389 bool in_dark_mode = [[control_view window] inIncognitoModeWithSystemTheme]; | |
390 // Only adjust the path rect by 1/2 the line width if it's going to be | |
391 // stroked (so that the stroke lines fall along pixel lines). | |
392 if (!in_dark_mode) { | |
393 rect = NSInsetRect(rect, line_width / 2., line_width / 2.); | |
394 } | |
395 | |
396 DrawInFrame(frame, control_view); | |
397 } | |
398 | |
399 // Pass mouse operations through to location icon. | |
400 bool SecurityStateBubbleDecoration::IsDraggable() { | |
401 return location_icon_->IsDraggable(); | |
402 } | |
403 | |
404 NSPasteboard* SecurityStateBubbleDecoration::GetDragPasteboard() { | |
405 return location_icon_->GetDragPasteboard(); | |
406 } | |
407 | |
408 NSImage* SecurityStateBubbleDecoration::GetDragImage() { | |
409 return location_icon_->GetDragImage(); | |
410 } | |
411 | |
412 NSRect SecurityStateBubbleDecoration::GetDragImageFrame(NSRect frame) { | |
413 return GetImageRectInFrame(frame); | |
414 } | |
415 | |
416 bool SecurityStateBubbleDecoration::OnMousePressed(NSRect frame, | |
417 NSPoint location) { | |
418 return location_icon_->OnMousePressed(frame, location); | |
419 } | |
420 | |
421 bool SecurityStateBubbleDecoration::AcceptsMousePress() { | |
422 return true; | |
423 } | |
424 | |
425 NSPoint SecurityStateBubbleDecoration::GetBubblePointInFrame(NSRect frame) { | |
426 NSRect image_rect = GetImageRectInFrame(frame); | |
427 return NSMakePoint(NSMidX(image_rect), | |
428 NSMaxY(image_rect) - kPageInfoBubblePointYOffset); | |
429 } | |
430 | |
431 ////////////////////////////////////////////////////////////////// | |
432 // SecurityStateBubbleDecoration::BubbleDecoration: | |
433 | |
434 NSColor* SecurityStateBubbleDecoration::GetBackgroundBorderColor() { | |
435 return skia::SkColorToSRGBNSColor( | |
436 SkColorSetA(label_color_, 255.0 * GetAnimationProgress())); | |
437 } | |
438 | |
439 ui::NinePartImageIds SecurityStateBubbleDecoration::GetBubbleImageIds() { | |
440 return IMAGE_GRID(IDR_OMNIBOX_EV_BUBBLE); | |
441 } | |
442 | |
443 NSColor* SecurityStateBubbleDecoration::GetDarkModeTextColor() { | |
444 return [NSColor whiteColor]; | |
445 } | |
446 | |
447 ////////////////////////////////////////////////////////////////// | |
448 // SecurityStateBubbleDecoration, private: | |
449 | |
450 CGFloat SecurityStateBubbleDecoration::GetAnimationProgress() const { | |
451 if (!ui::MaterialDesignController::IsModeMaterial()) | |
452 return 1.0; | |
453 | |
454 return animation_.get() ? [animation_ animationProgress] : 0.0; | |
455 } | |
456 | |
457 CGFloat SecurityStateBubbleDecoration::GetWidthForText(CGFloat width) { | |
458 // Limit with to not take up too much of the available width, but | |
459 // also don't let it shrink too much. | |
460 width = std::max(width * kMaxBubbleFraction, kMinElidedBubbleWidth); | |
461 | |
462 // Use the full label if it fits. | |
463 NSImage* image = GetImage(); | |
464 const CGFloat all_width = GetWidthForImageAndLabel(image, full_label_); | |
465 if (all_width <= width) { | |
466 SetLabel(full_label_); | |
467 return all_width; | |
468 } | |
469 | |
470 // Width left for laying out the label. | |
471 const CGFloat width_left = width - GetWidthForImageAndLabel(image, @""); | |
472 | |
473 // Middle-elide the label to fit |width_left|. This leaves the | |
474 // prefix and the trailing country code in place. | |
475 NSString* elided_label = base::SysUTF16ToNSString(gfx::ElideText( | |
476 base::SysNSStringToUTF16(full_label_), | |
477 gfx::FontList(gfx::Font(GetFont())), width_left, gfx::ELIDE_MIDDLE)); | |
478 | |
479 // Use the elided label. | |
480 SetLabel(elided_label); | |
481 return GetWidthForImageAndLabel(image, elided_label); | |
482 } | |
OLD | NEW |