blob: ddf00ace198ee221a7d3f76e1661053113d40bcc [file] [log] [blame]
[email protected]35152752012-04-09 04:19:491// Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]7d791652010-12-01 16:34:492// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/cocoa/status_bubble_mac.h"
6
7#include <limits>
8
[email protected]a932d9e2011-09-29 01:14:349#include "base/bind.h"
[email protected]7d791652010-12-01 16:34:4910#include "base/compiler_specific.h"
[email protected]1ba39c002011-08-03 21:05:3611#include "base/mac/mac_util.h"
[email protected]52a025a2014-02-15 03:47:4212#include "base/mac/scoped_block.h"
13#include "base/mac/sdk_forward_declarations.h"
[email protected]3cc55ad2013-07-17 22:17:4114#include "base/message_loop/message_loop.h"
[email protected]a2cba3372013-06-10 21:50:1315#include "base/strings/string_util.h"
[email protected]3268d7b72013-03-28 17:41:4316#include "base/strings/sys_string_conversions.h"
[email protected]5846d582013-06-08 16:02:1217#include "base/strings/utf_string_conversions.h"
[email protected]7d791652010-12-01 16:34:4918#import "chrome/browser/ui/cocoa/bubble_view.h"
[email protected]ebc9b662014-01-30 03:37:3319#include "chrome/browser/ui/elide_url.h"
[email protected]7d791652010-12-01 16:34:4920#include "net/base/net_util.h"
[email protected]a023dca2013-12-18 03:58:3621#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
22#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
23#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
[email protected]9c24e4302012-04-13 22:06:3524#include "ui/base/cocoa/window_size_constants.h"
[email protected]035fa4c2013-11-07 11:27:3325#include "ui/gfx/font_list.h"
[email protected]08397d52011-02-05 01:53:3826#include "ui/gfx/point.h"
[email protected]035fa4c2013-11-07 11:27:3327#include "ui/gfx/text_elider.h"
[email protected]eaa4f142014-01-23 09:35:4928#include "ui/gfx/text_utils.h"
[email protected]7d791652010-12-01 16:34:4929
30namespace {
31
32const int kWindowHeight = 18;
33
34// The width of the bubble in relation to the width of the parent window.
35const CGFloat kWindowWidthPercent = 1.0 / 3.0;
36
37// How close the mouse can get to the infobubble before it starts sliding
38// off-screen.
39const int kMousePadding = 20;
40
41const int kTextPadding = 3;
42
[email protected]7d791652010-12-01 16:34:4943// The status bubble's maximum opacity, when fully faded in.
44const CGFloat kBubbleOpacity = 1.0;
45
46// Delay before showing or hiding the bubble after a SetStatus or SetURL call.
[email protected]9ad0f5bb2014-02-11 01:10:5947const int64 kShowDelayMS = 80;
48const int64 kHideDelayMS = 250;
[email protected]7d791652010-12-01 16:34:4949
50// How long each fade should last.
51const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
52const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
53
54// The minimum representable time interval. This can be used as the value
55// passed to +[NSAnimationContext setDuration:] to stop an in-progress
56// animation as quickly as possible.
57const NSTimeInterval kMinimumTimeInterval =
58 std::numeric_limits<NSTimeInterval>::min();
59
[email protected]9ad0f5bb2014-02-11 01:10:5960// How quickly the status bubble should expand.
61const CGFloat kExpansionDurationSeconds = 0.125;
[email protected]7d791652010-12-01 16:34:4962
63} // namespace
64
65@interface StatusBubbleAnimationDelegate : NSObject {
66 @private
[email protected]52a025a2014-02-15 03:47:4267 base::mac::ScopedBlock<void (^)(void)> completionHandler_;
[email protected]7d791652010-12-01 16:34:4968}
69
[email protected]52a025a2014-02-15 03:47:4270- (id)initWithCompletionHandler:(void (^)(void))completionHandler;
[email protected]7d791652010-12-01 16:34:4971
72// CAAnimation delegate method
73- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
74@end
75
76@implementation StatusBubbleAnimationDelegate
77
[email protected]52a025a2014-02-15 03:47:4278- (id)initWithCompletionHandler:(void (^)(void))completionHandler {
[email protected]7d791652010-12-01 16:34:4979 if ((self = [super init])) {
[email protected]52a025a2014-02-15 03:47:4280 completionHandler_.reset(completionHandler, base::scoped_policy::RETAIN);
[email protected]7d791652010-12-01 16:34:4981 }
82
83 return self;
84}
85
[email protected]52a025a2014-02-15 03:47:4286- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
87 completionHandler_.get()();
[email protected]7d791652010-12-01 16:34:4988}
89
[email protected]52a025a2014-02-15 03:47:4290@end
91
92@interface StatusBubbleWindow : NSWindow {
93 @private
94 void (^completionHandler_)(void);
95}
96
97- (id)animationForKey:(NSString *)key;
98- (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
99 completionHandler:(void (^)(void))completionHandler;
100@end
101
102@implementation StatusBubbleWindow
103
104- (id)animationForKey:(NSString *)key {
105 CAAnimation* animation = [super animationForKey:key];
106 // If completionHandler_ isn't nil, then this is the first of (potentially)
107 // multiple animations in a grouping; give it the completion handler. If
108 // completionHandler_ is nil, then some other animation was tagged with the
109 // completion handler.
110 if (completionHandler_) {
111 DCHECK(![NSAnimationContext respondsToSelector:
112 @selector(runAnimationGroup:completionHandler:)]);
113 StatusBubbleAnimationDelegate* animation_delegate =
114 [[StatusBubbleAnimationDelegate alloc]
115 initWithCompletionHandler:completionHandler_];
116 [animation setDelegate:animation_delegate];
117 completionHandler_ = nil;
118 }
119 return animation;
120}
121
122- (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
123 completionHandler:(void (^)(void))completionHandler {
124 if ([NSAnimationContext respondsToSelector:
125 @selector(runAnimationGroup:completionHandler:)]) {
126 [NSAnimationContext runAnimationGroup:changes
127 completionHandler:completionHandler];
128 } else {
129 // Mac OS 10.6 does not have completion handler callbacks at the Cocoa
130 // level, only at the CoreAnimation level. So intercept calls made to
131 // -animationForKey: and tag one of the animations with a delegate that will
132 // execute the completion handler.
133 completionHandler_ = completionHandler;
134 [NSAnimationContext beginGrouping];
135 changes([NSAnimationContext currentContext]);
136 // At this point, -animationForKey should have been called by CoreAnimation
137 // to set up the animation to run. Verify this.
138 DCHECK(completionHandler_ == nil);
139 [NSAnimationContext endGrouping];
140 }
[email protected]7d791652010-12-01 16:34:49141}
142
143@end
144
145StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
[email protected]d4b2d232013-04-30 21:14:23146 : timer_factory_(this),
147 expand_timer_factory_(this),
[email protected]52a025a2014-02-15 03:47:42148 completion_handler_factory_(this),
[email protected]7d791652010-12-01 16:34:49149 parent_(parent),
150 delegate_(delegate),
151 window_(nil),
152 status_text_(nil),
153 url_text_(nil),
154 state_(kBubbleHidden),
155 immediate_(false),
156 is_expanded_(false) {
[email protected]a0c10182011-03-06 20:58:35157 Create();
158 Attach();
[email protected]7d791652010-12-01 16:34:49159}
160
161StatusBubbleMac::~StatusBubbleMac() {
[email protected]a0c10182011-03-06 20:58:35162 DCHECK(window_);
163
[email protected]7d791652010-12-01 16:34:49164 Hide();
165
[email protected]52a025a2014-02-15 03:47:42166 completion_handler_factory_.InvalidateWeakPtrs();
[email protected]a0c10182011-03-06 20:58:35167 Detach();
168 [window_ release];
169 window_ = nil;
[email protected]7d791652010-12-01 16:34:49170}
171
[email protected]dcd0249872013-12-06 23:58:45172void StatusBubbleMac::SetStatus(const base::string16& status) {
[email protected]7d791652010-12-01 16:34:49173 SetText(status, false);
174}
175
[email protected]65233052011-08-22 19:02:48176void StatusBubbleMac::SetURL(const GURL& url, const std::string& languages) {
[email protected]7d791652010-12-01 16:34:49177 url_ = url;
178 languages_ = languages;
179
[email protected]7d791652010-12-01 16:34:49180 NSRect frame = [window_ frame];
181
182 // Reset frame size when bubble is hidden.
183 if (state_ == kBubbleHidden) {
184 is_expanded_ = false;
185 frame.size.width = NSWidth(CalculateWindowFrame(/*expand=*/false));
186 [window_ setFrame:frame display:NO];
187 }
188
189 int text_width = static_cast<int>(NSWidth(frame) -
190 kBubbleViewTextPositionX -
191 kTextPadding);
192
193 // Scale from view to window coordinates before eliding URL string.
194 NSSize scaled_width = NSMakeSize(text_width, 0);
195 scaled_width = [[parent_ contentView] convertSize:scaled_width fromView:nil];
196 text_width = static_cast<int>(scaled_width.width);
197 NSFont* font = [[window_ contentView] font];
[email protected]035fa4c2013-11-07 11:27:33198 gfx::FontList font_list_chr(
199 gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
[email protected]7d791652010-12-01 16:34:49200
[email protected]dcd0249872013-12-06 23:58:45201 base::string16 original_url_text = net::FormatUrl(url, languages);
202 base::string16 status =
[email protected]ebc9b662014-01-30 03:37:33203 ElideUrl(url, font_list_chr, text_width, languages);
[email protected]7d791652010-12-01 16:34:49204
205 SetText(status, true);
206
207 // In testing, don't use animation. When ExpandBubble is tested, it is
208 // called explicitly.
209 if (immediate_)
210 return;
211 else
212 CancelExpandTimer();
213
214 // If the bubble has been expanded, the user has already hovered over a link
215 // to trigger the expanded state. Don't wait to change the bubble in this
216 // case -- immediately expand or contract to fit the URL.
217 if (is_expanded_ && !url.is_empty()) {
218 ExpandBubble();
219 } else if (original_url_text.length() > status.length()) {
[email protected]3264d162013-05-08 22:02:11220 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
[email protected]a932d9e2011-09-29 01:14:34221 base::Bind(&StatusBubbleMac::ExpandBubble,
222 expand_timer_factory_.GetWeakPtr()),
[email protected]9ad0f5bb2014-02-11 01:10:59223 base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS));
[email protected]7d791652010-12-01 16:34:49224 }
225}
226
[email protected]dcd0249872013-12-06 23:58:45227void StatusBubbleMac::SetText(const base::string16& text, bool is_url) {
[email protected]7d791652010-12-01 16:34:49228 // The status bubble allows the status and URL strings to be set
229 // independently. Whichever was set non-empty most recently will be the
230 // value displayed. When both are empty, the status bubble hides.
231
232 NSString* text_ns = base::SysUTF16ToNSString(text);
233
234 NSString** main;
235 NSString** backup;
236
237 if (is_url) {
238 main = &url_text_;
239 backup = &status_text_;
240 } else {
241 main = &status_text_;
242 backup = &url_text_;
243 }
244
245 // Don't return from this function early. It's important to make sure that
246 // all calls to StartShowing and StartHiding are made, so that all delays
247 // are observed properly. Specifically, if the state is currently
248 // kBubbleShowingTimer, the timer will need to be restarted even if
249 // [text_ns isEqualToString:*main] is true.
250
251 [*main autorelease];
252 *main = [text_ns retain];
253
254 bool show = true;
255 if ([*main length] > 0)
256 [[window_ contentView] setContent:*main];
257 else if ([*backup length] > 0)
258 [[window_ contentView] setContent:*backup];
259 else
260 show = false;
261
[email protected]61987b0c2011-06-03 18:39:27262 if (show) {
263 UpdateSizeAndPosition();
[email protected]7d791652010-12-01 16:34:49264 StartShowing();
[email protected]61987b0c2011-06-03 18:39:27265 } else {
[email protected]7d791652010-12-01 16:34:49266 StartHiding();
[email protected]61987b0c2011-06-03 18:39:27267 }
[email protected]7d791652010-12-01 16:34:49268}
269
270void StatusBubbleMac::Hide() {
271 CancelTimer();
272 CancelExpandTimer();
273 is_expanded_ = false;
274
275 bool fade_out = false;
276 if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
277 SetState(kBubbleHidingFadeOut);
278
279 if (!immediate_) {
280 // An animation is in progress. Cancel it by starting a new animation.
281 // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
282 fade_out = true;
[email protected]52a025a2014-02-15 03:47:42283 AnimateWindowAlpha(0.0, kMinimumTimeInterval);
[email protected]7d791652010-12-01 16:34:49284 }
285 }
286
287 if (!fade_out) {
288 // No animation is in progress, so the opacity can be set directly.
289 [window_ setAlphaValue:0.0];
290 SetState(kBubbleHidden);
291 }
292
293 // Stop any width animation and reset the bubble size.
294 if (!immediate_) {
295 [NSAnimationContext beginGrouping];
296 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
297 [[window_ animator] setFrame:CalculateWindowFrame(/*expand=*/false)
298 display:NO];
299 [NSAnimationContext endGrouping];
300 } else {
301 [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
302 }
303
304 [status_text_ release];
305 status_text_ = nil;
306 [url_text_ release];
307 url_text_ = nil;
308}
309
[email protected]61987b0c2011-06-03 18:39:27310void StatusBubbleMac::SetFrameAvoidingMouse(
311 NSRect window_frame, const gfx::Point& mouse_pos) {
[email protected]7d791652010-12-01 16:34:49312 if (!window_)
313 return;
314
[email protected]61987b0c2011-06-03 18:39:27315 // Bubble's base rect in |parent_| (window base) coordinates.
316 NSRect base_rect;
317 if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
318 base_rect = [delegate_ statusBubbleBaseFrame];
319 } else {
320 base_rect = [[parent_ contentView] bounds];
321 base_rect = [[parent_ contentView] convertRect:base_rect toView:nil];
322 }
[email protected]7d791652010-12-01 16:34:49323
[email protected]61987b0c2011-06-03 18:39:27324 // To start, assume default positioning in the lower left corner.
325 // The window_frame position is in global (screen) coordinates.
326 window_frame.origin = [parent_ convertBaseToScreen:base_rect.origin];
[email protected]7d791652010-12-01 16:34:49327
[email protected]61987b0c2011-06-03 18:39:27328 // Get the cursor position relative to the top right corner of the bubble.
329 gfx::Point relative_pos(mouse_pos.x() - NSMaxX(window_frame),
330 mouse_pos.y() - NSMaxY(window_frame));
[email protected]7d791652010-12-01 16:34:49331
332 // If the mouse is in a position where we think it would move the
[email protected]1ba39c002011-08-03 21:05:36333 // status bubble, figure out where and how the bubble should be moved, and
334 // what sorts of corners it should have.
335 unsigned long corner_flags;
[email protected]61987b0c2011-06-03 18:39:27336 if (relative_pos.y() < kMousePadding &&
337 relative_pos.x() < kMousePadding) {
338 int offset = kMousePadding - relative_pos.y();
[email protected]7d791652010-12-01 16:34:49339
340 // Make the movement non-linear.
341 offset = offset * offset / kMousePadding;
342
343 // When the mouse is entering from the right, we want the offset to be
344 // scaled by how horizontally far away the cursor is from the bubble.
[email protected]61987b0c2011-06-03 18:39:27345 if (relative_pos.x() > 0) {
346 offset *= (kMousePadding - relative_pos.x()) / kMousePadding;
[email protected]7d791652010-12-01 16:34:49347 }
348
[email protected]61987b0c2011-06-03 18:39:27349 bool is_on_screen = true;
[email protected]7d791652010-12-01 16:34:49350 NSScreen* screen = [window_ screen];
351 if (screen &&
352 NSMinY([screen visibleFrame]) > NSMinY(window_frame) - offset) {
[email protected]61987b0c2011-06-03 18:39:27353 is_on_screen = false;
[email protected]7d791652010-12-01 16:34:49354 }
355
356 // If something is shown below tab contents (devtools, download shelf etc.),
357 // adjust the position to sit on top of it.
[email protected]61987b0c2011-06-03 18:39:27358 bool is_any_shelf_visible = NSMinY(base_rect) > 0;
[email protected]7d791652010-12-01 16:34:49359
[email protected]61987b0c2011-06-03 18:39:27360 if (is_on_screen && !is_any_shelf_visible) {
[email protected]7d791652010-12-01 16:34:49361 // Cap the offset and change the visual presentation of the bubble
362 // depending on where it ends up (so that rounded corners square off
363 // and mate to the edges of the tab content).
364 if (offset >= NSHeight(window_frame)) {
365 offset = NSHeight(window_frame);
[email protected]1ba39c002011-08-03 21:05:36366 corner_flags = kRoundedBottomLeftCorner | kRoundedBottomRightCorner;
[email protected]7d791652010-12-01 16:34:49367 } else if (offset > 0) {
[email protected]1ba39c002011-08-03 21:05:36368 corner_flags = kRoundedTopRightCorner |
369 kRoundedBottomLeftCorner |
370 kRoundedBottomRightCorner;
[email protected]7d791652010-12-01 16:34:49371 } else {
[email protected]1ba39c002011-08-03 21:05:36372 corner_flags = kRoundedTopRightCorner;
[email protected]7d791652010-12-01 16:34:49373 }
[email protected]61987b0c2011-06-03 18:39:27374
375 // Place the bubble on the left, but slightly lower.
[email protected]7d791652010-12-01 16:34:49376 window_frame.origin.y -= offset;
377 } else {
378 // Cannot move the bubble down without obscuring other content.
[email protected]61987b0c2011-06-03 18:39:27379 // Move it to the far right instead.
[email protected]1ba39c002011-08-03 21:05:36380 corner_flags = kRoundedTopLeftCorner;
[email protected]61987b0c2011-06-03 18:39:27381 window_frame.origin.x += NSWidth(base_rect) - NSWidth(window_frame);
[email protected]7d791652010-12-01 16:34:49382 }
383 } else {
[email protected]61987b0c2011-06-03 18:39:27384 // Use the default position in the lower left corner of the content area.
[email protected]1ba39c002011-08-03 21:05:36385 corner_flags = kRoundedTopRightCorner;
[email protected]7d791652010-12-01 16:34:49386 }
[email protected]1ba39c002011-08-03 21:05:36387
388 corner_flags |= OSDependentCornerFlags(window_frame);
389
390 [[window_ contentView] setCornerFlags:corner_flags];
[email protected]7d791652010-12-01 16:34:49391 [window_ setFrame:window_frame display:YES];
392}
393
[email protected]61987b0c2011-06-03 18:39:27394void StatusBubbleMac::MouseMoved(
395 const gfx::Point& location, bool left_content) {
396 if (!left_content)
397 SetFrameAvoidingMouse([window_ frame], location);
398}
399
[email protected]7d791652010-12-01 16:34:49400void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
[email protected]ff19e412011-04-20 23:13:30401 UpdateSizeAndPosition();
[email protected]7d791652010-12-01 16:34:49402}
403
404void StatusBubbleMac::Create() {
[email protected]a0c10182011-03-06 20:58:35405 DCHECK(!window_);
[email protected]7d791652010-12-01 16:34:49406
[email protected]52a025a2014-02-15 03:47:42407 window_ = [[StatusBubbleWindow alloc]
408 initWithContentRect:ui::kWindowSizeDeterminedLater
409 styleMask:NSBorderlessWindowMask
410 backing:NSBackingStoreBuffered
411 defer:YES];
[email protected]7d791652010-12-01 16:34:49412 [window_ setMovableByWindowBackground:NO];
413 [window_ setBackgroundColor:[NSColor clearColor]];
414 [window_ setLevel:NSNormalWindowLevel];
415 [window_ setOpaque:NO];
416 [window_ setHasShadow:NO];
417
418 // We do not need to worry about the bubble outliving |parent_| because our
419 // teardown sequence in BWC guarantees that |parent_| outlives the status
420 // bubble and that the StatusBubble is torn down completely prior to the
421 // window going away.
[email protected]a8522032013-06-24 22:51:46422 base::scoped_nsobject<BubbleView> view(
[email protected]7d791652010-12-01 16:34:49423 [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
424 [window_ setContentView:view];
425
426 [window_ setAlphaValue:0.0];
427
[email protected]625cc3e2011-09-22 00:16:13428 // TODO(dtseng): Ignore until we provide NSAccessibility support.
429 [window_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
[email protected]9ad0f5bb2014-02-11 01:10:59430 forAttribute:NSAccessibilityRoleAttribute];
[email protected]625cc3e2011-09-22 00:16:13431
[email protected]7d791652010-12-01 16:34:49432 [view setCornerFlags:kRoundedTopRightCorner];
433 MouseMoved(gfx::Point(), false);
434}
435
436void StatusBubbleMac::Attach() {
[email protected]a0c10182011-03-06 20:58:35437 DCHECK(!is_attached());
438
[email protected]bf8651a2011-03-09 22:02:46439 [window_ orderFront:nil];
[email protected]a0c10182011-03-06 20:58:35440 [parent_ addChildWindow:window_ ordered:NSWindowAbove];
441
442 [[window_ contentView] setThemeProvider:parent_];
[email protected]7d791652010-12-01 16:34:49443}
444
445void StatusBubbleMac::Detach() {
[email protected]a0c10182011-03-06 20:58:35446 DCHECK(is_attached());
447
[email protected]9ad0f5bb2014-02-11 01:10:59448 // Magic setFrame: See http://crbug.com/58506 and http://crrev.com/3564021 .
[email protected]a0c10182011-03-06 20:58:35449 [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
450 [parent_ removeChildWindow:window_]; // See crbug.com/28107 ...
451 [window_ orderOut:nil]; // ... and crbug.com/29054.
452
453 [[window_ contentView] setThemeProvider:nil];
[email protected]7d791652010-12-01 16:34:49454}
455
[email protected]52a025a2014-02-15 03:47:42456void StatusBubbleMac::AnimationDidStop() {
[email protected]7d791652010-12-01 16:34:49457 DCHECK([NSThread isMainThread]);
458 DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
459 DCHECK(is_attached());
460
[email protected]52a025a2014-02-15 03:47:42461 if (state_ == kBubbleShowingFadeIn) {
462 DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
463 SetState(kBubbleShown);
464 } else {
465 DCHECK_EQ([[window_ animator] alphaValue], 0.0);
466 SetState(kBubbleHidden);
[email protected]7d791652010-12-01 16:34:49467 }
468}
469
470void StatusBubbleMac::SetState(StatusBubbleState state) {
[email protected]7d791652010-12-01 16:34:49471 if (state == state_)
472 return;
473
[email protected]5afebbd2012-04-23 23:39:16474 if (state == kBubbleHidden) {
475 // When hidden (with alpha of 0), make the window have the minimum size,
476 // while still keeping the same origin. It's important to not set the
477 // origin to 0,0 as that will cause the window to use more space in
478 // Expose/Mission Control. See http://crbug.com/81969.
479 //
480 // Also, doing it this way instead of detaching the window avoids bugs with
481 // Spaces and Cmd-`. See http://crbug.com/31821 and http://crbug.com/61629.
482 NSRect frame = [window_ frame];
[email protected]9ad0f5bb2014-02-11 01:10:59483 frame.size = ui::kWindowSizeDeterminedLater.size;
[email protected]5afebbd2012-04-23 23:39:16484 [window_ setFrame:frame display:YES];
485 }
[email protected]7d791652010-12-01 16:34:49486
487 if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
488 [delegate_ statusBubbleWillEnterState:state];
489
490 state_ = state;
491}
492
493void StatusBubbleMac::Fade(bool show) {
494 DCHECK([NSThread isMainThread]);
495
496 StatusBubbleState fade_state = kBubbleShowingFadeIn;
497 StatusBubbleState target_state = kBubbleShown;
498 NSTimeInterval full_duration = kShowFadeInDurationSeconds;
499 CGFloat opacity = kBubbleOpacity;
500
501 if (!show) {
502 fade_state = kBubbleHidingFadeOut;
503 target_state = kBubbleHidden;
504 full_duration = kHideFadeOutDurationSeconds;
505 opacity = 0.0;
506 }
507
508 DCHECK(state_ == fade_state || state_ == target_state);
509
510 if (state_ == target_state)
511 return;
512
513 if (immediate_) {
514 [window_ setAlphaValue:opacity];
515 SetState(target_state);
516 return;
517 }
518
519 // If an incomplete transition has left the opacity somewhere between 0 and
520 // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
521 NSTimeInterval duration =
522 full_duration *
523 fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
524
525 // 0.0 will not cancel an in-progress animation.
526 if (duration == 0.0)
527 duration = kMinimumTimeInterval;
528
[email protected]52a025a2014-02-15 03:47:42529 // Cancel an in-progress transition and replace it with this fade.
530 AnimateWindowAlpha(opacity, duration);
531}
532
533void StatusBubbleMac::AnimateWindowAlpha(CGFloat alpha,
534 NSTimeInterval duration) {
535 completion_handler_factory_.InvalidateWeakPtrs();
536 base::WeakPtr<StatusBubbleMac> weak_ptr(
537 completion_handler_factory_.GetWeakPtr());
538 [window_
539 runAnimationGroup:^(NSAnimationContext* context) {
540 [context setDuration:duration];
541 [[window_ animator] setAlphaValue:alpha];
542 }
543 completionHandler:^{
544 if (weak_ptr)
545 weak_ptr->AnimationDidStop();
546 }];
[email protected]7d791652010-12-01 16:34:49547}
548
549void StatusBubbleMac::StartTimer(int64 delay_ms) {
550 DCHECK([NSThread isMainThread]);
551 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
552
553 if (immediate_) {
554 TimerFired();
555 return;
556 }
557
558 // There can only be one running timer.
559 CancelTimer();
560
[email protected]3264d162013-05-08 22:02:11561 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
[email protected]a932d9e2011-09-29 01:14:34562 base::Bind(&StatusBubbleMac::TimerFired, timer_factory_.GetWeakPtr()),
[email protected]35152752012-04-09 04:19:49563 base::TimeDelta::FromMilliseconds(delay_ms));
[email protected]7d791652010-12-01 16:34:49564}
565
566void StatusBubbleMac::CancelTimer() {
567 DCHECK([NSThread isMainThread]);
568
[email protected]a932d9e2011-09-29 01:14:34569 if (timer_factory_.HasWeakPtrs())
570 timer_factory_.InvalidateWeakPtrs();
[email protected]7d791652010-12-01 16:34:49571}
572
573void StatusBubbleMac::TimerFired() {
574 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
575 DCHECK([NSThread isMainThread]);
576
577 if (state_ == kBubbleShowingTimer) {
578 SetState(kBubbleShowingFadeIn);
579 Fade(true);
580 } else {
581 SetState(kBubbleHidingFadeOut);
582 Fade(false);
583 }
584}
585
586void StatusBubbleMac::StartShowing() {
[email protected]7d791652010-12-01 16:34:49587 if (state_ == kBubbleHidden) {
588 // Arrange to begin fading in after a delay.
589 SetState(kBubbleShowingTimer);
[email protected]9ad0f5bb2014-02-11 01:10:59590 StartTimer(kShowDelayMS);
[email protected]7d791652010-12-01 16:34:49591 } else if (state_ == kBubbleHidingFadeOut) {
592 // Cancel the fade-out in progress and replace it with a fade in.
593 SetState(kBubbleShowingFadeIn);
594 Fade(true);
595 } else if (state_ == kBubbleHidingTimer) {
596 // The bubble was already shown but was waiting to begin fading out. It's
597 // given a stay of execution.
598 SetState(kBubbleShown);
599 CancelTimer();
600 } else if (state_ == kBubbleShowingTimer) {
601 // The timer was already running but nothing was showing yet. Reaching
602 // this point means that there is a new request to show something. Start
603 // over again by resetting the timer, effectively invalidating the earlier
604 // request.
[email protected]9ad0f5bb2014-02-11 01:10:59605 StartTimer(kShowDelayMS);
[email protected]7d791652010-12-01 16:34:49606 }
607
608 // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
609 // alone.
610}
611
612void StatusBubbleMac::StartHiding() {
613 if (state_ == kBubbleShown) {
614 // Arrange to begin fading out after a delay.
615 SetState(kBubbleHidingTimer);
[email protected]9ad0f5bb2014-02-11 01:10:59616 StartTimer(kHideDelayMS);
[email protected]7d791652010-12-01 16:34:49617 } else if (state_ == kBubbleShowingFadeIn) {
618 // Cancel the fade-in in progress and replace it with a fade out.
619 SetState(kBubbleHidingFadeOut);
620 Fade(false);
621 } else if (state_ == kBubbleShowingTimer) {
622 // The bubble was already hidden but was waiting to begin fading in. Too
623 // bad, it won't get the opportunity now.
624 SetState(kBubbleHidden);
625 CancelTimer();
626 }
627
628 // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
629 // kBubbleHidingTimer, leave everything alone. The timer is not reset as
630 // with kBubbleShowingTimer in StartShowing() because a subsequent request
631 // to hide something while one is already in flight does not invalidate the
632 // earlier request.
633}
634
635void StatusBubbleMac::CancelExpandTimer() {
636 DCHECK([NSThread isMainThread]);
[email protected]a932d9e2011-09-29 01:14:34637 expand_timer_factory_.InvalidateWeakPtrs();
[email protected]7d791652010-12-01 16:34:49638}
639
[email protected]61987b0c2011-06-03 18:39:27640// Get the current location of the mouse in screen coordinates. To make this
641// class testable, all code should use this method rather than using
642// NSEvent mouseLocation directly.
643gfx::Point StatusBubbleMac::GetMouseLocation() {
644 NSPoint p = [NSEvent mouseLocation];
645 --p.y; // The docs say the y coord starts at 1 not 0; don't ask why.
646 return gfx::Point(p.x, p.y);
647}
648
[email protected]7d791652010-12-01 16:34:49649void StatusBubbleMac::ExpandBubble() {
650 // Calculate the width available for expanded and standard bubbles.
651 NSRect window_frame = CalculateWindowFrame(/*expand=*/true);
652 CGFloat max_bubble_width = NSWidth(window_frame);
653 CGFloat standard_bubble_width =
654 NSWidth(CalculateWindowFrame(/*expand=*/false));
655
656 // Generate the URL string that fits in the expanded bubble.
657 NSFont* font = [[window_ contentView] font];
[email protected]035fa4c2013-11-07 11:27:33658 gfx::FontList font_list_chr(
659 gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
[email protected]ebc9b662014-01-30 03:37:33660 base::string16 expanded_url = ElideUrl(
[email protected]035fa4c2013-11-07 11:27:33661 url_, font_list_chr, max_bubble_width, languages_);
[email protected]7d791652010-12-01 16:34:49662
663 // Scale width from gfx::Font in view coordinates to window coordinates.
664 int required_width_for_string =
[email protected]eaa4f142014-01-23 09:35:49665 gfx::GetStringWidth(expanded_url, font_list_chr) +
[email protected]7d791652010-12-01 16:34:49666 kTextPadding * 2 + kBubbleViewTextPositionX;
667 NSSize scaled_width = NSMakeSize(required_width_for_string, 0);
668 scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil];
669 required_width_for_string = scaled_width.width;
670
671 // The expanded width must be at least as wide as the standard width, but no
672 // wider than the maximum width for its parent frame.
673 int expanded_bubble_width =
674 std::max(standard_bubble_width,
675 std::min(max_bubble_width,
676 static_cast<CGFloat>(required_width_for_string)));
677
678 SetText(expanded_url, true);
679 is_expanded_ = true;
680 window_frame.size.width = expanded_bubble_width;
681
682 // In testing, don't do any animation.
683 if (immediate_) {
684 [window_ setFrame:window_frame display:YES];
685 return;
686 }
687
688 NSRect actual_window_frame = [window_ frame];
689 // Adjust status bubble origin if bubble was moved to the right.
690 // TODO(alekseys): fix for RTL.
691 if (NSMinX(actual_window_frame) > NSMinX(window_frame)) {
692 actual_window_frame.origin.x =
693 NSMaxX(actual_window_frame) - NSWidth(window_frame);
694 }
695 actual_window_frame.size.width = NSWidth(window_frame);
696
697 // Do not expand if it's going to cover mouse location.
[email protected]61987b0c2011-06-03 18:39:27698 gfx::Point p = GetMouseLocation();
699 if (NSPointInRect(NSMakePoint(p.x(), p.y()), actual_window_frame))
[email protected]7d791652010-12-01 16:34:49700 return;
701
[email protected]1ba39c002011-08-03 21:05:36702 // Get the current corner flags and see what needs to change based on the
703 // expansion. This is only needed on Lion, which has rounded window bottoms.
704 if (base::mac::IsOSLionOrLater()) {
705 unsigned long corner_flags = [[window_ contentView] cornerFlags];
706 corner_flags |= OSDependentCornerFlags(actual_window_frame);
707 [[window_ contentView] setCornerFlags:corner_flags];
708 }
709
[email protected]7d791652010-12-01 16:34:49710 [NSAnimationContext beginGrouping];
[email protected]9ad0f5bb2014-02-11 01:10:59711 [[NSAnimationContext currentContext] setDuration:kExpansionDurationSeconds];
[email protected]7d791652010-12-01 16:34:49712 [[window_ animator] setFrame:actual_window_frame display:YES];
713 [NSAnimationContext endGrouping];
714}
715
716void StatusBubbleMac::UpdateSizeAndPosition() {
717 if (!window_)
718 return;
719
[email protected]61987b0c2011-06-03 18:39:27720 SetFrameAvoidingMouse(CalculateWindowFrame(/*expand=*/false),
721 GetMouseLocation());
[email protected]7d791652010-12-01 16:34:49722}
723
724void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
725 DCHECK(parent);
[email protected]a0c10182011-03-06 20:58:35726 DCHECK(is_attached());
[email protected]7d791652010-12-01 16:34:49727
728 Detach();
729 parent_ = parent;
[email protected]7d791652010-12-01 16:34:49730 Attach();
731 UpdateSizeAndPosition();
732}
733
734NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) {
735 DCHECK(parent_);
736
737 NSRect screenRect;
738 if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
739 screenRect = [delegate_ statusBubbleBaseFrame];
740 screenRect.origin = [parent_ convertBaseToScreen:screenRect.origin];
741 } else {
742 screenRect = [parent_ frame];
743 }
744
745 NSSize size = NSMakeSize(0, kWindowHeight);
746 size = [[parent_ contentView] convertSize:size toView:nil];
747
748 if (expanded_width) {
749 size.width = screenRect.size.width;
750 } else {
751 size.width = kWindowWidthPercent * screenRect.size.width;
752 }
753
754 screenRect.size = size;
755 return screenRect;
756}
[email protected]1ba39c002011-08-03 21:05:36757
758unsigned long StatusBubbleMac::OSDependentCornerFlags(NSRect window_frame) {
759 unsigned long corner_flags = 0;
760
761 if (base::mac::IsOSLionOrLater()) {
762 NSRect parent_frame = [parent_ frame];
763
764 // Round the bottom corners when they're right up against the
765 // corresponding edge of the parent window, or when below the parent
766 // window.
767 if (NSMinY(window_frame) <= NSMinY(parent_frame)) {
768 if (NSMinX(window_frame) == NSMinX(parent_frame)) {
769 corner_flags |= kRoundedBottomLeftCorner;
770 }
771
772 if (NSMaxX(window_frame) == NSMaxX(parent_frame)) {
773 corner_flags |= kRoundedBottomRightCorner;
774 }
775 }
776
777 // Round the top corners when the bubble is below the parent window.
778 if (NSMinY(window_frame) < NSMinY(parent_frame)) {
779 corner_flags |= kRoundedTopLeftCorner | kRoundedTopRightCorner;
780 }
781 }
782
783 return corner_flags;
784}