blob: dfa18af1a5871c1d35a5f1bb2fdfef4df320b37e [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"
rohitraoda190482015-04-28 00:28:4411#include "base/debug/stack_trace.h"
[email protected]1ba39c002011-08-03 21:05:3612#include "base/mac/mac_util.h"
[email protected]52a025a2014-02-15 03:47:4213#include "base/mac/scoped_block.h"
14#include "base/mac/sdk_forward_declarations.h"
[email protected]3cc55ad2013-07-17 22:17:4115#include "base/message_loop/message_loop.h"
[email protected]a2cba3372013-06-10 21:50:1316#include "base/strings/string_util.h"
[email protected]3268d7b72013-03-28 17:41:4317#include "base/strings/sys_string_conversions.h"
[email protected]5846d582013-06-08 16:02:1218#include "base/strings/utf_string_conversions.h"
[email protected]7d791652010-12-01 16:34:4919#import "chrome/browser/ui/cocoa/bubble_view.h"
rsleevi24f64dc22015-08-07 21:39:2120#include "components/url_formatter/elide_url.h"
21#include "components/url_formatter/url_formatter.h"
[email protected]a023dca2013-12-18 03:58:3622#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
23#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
24#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
[email protected]9c24e4302012-04-13 22:06:3525#include "ui/base/cocoa/window_size_constants.h"
[email protected]035fa4c2013-11-07 11:27:3326#include "ui/gfx/font_list.h"
tfarina655f81d2014-12-23 02:38:5027#include "ui/gfx/geometry/point.h"
andresantoso3c029bf2015-07-15 17:45:3428#include "ui/gfx/platform_font.h"
[email protected]035fa4c2013-11-07 11:27:3329#include "ui/gfx/text_elider.h"
[email protected]eaa4f142014-01-23 09:35:4930#include "ui/gfx/text_utils.h"
[email protected]7d791652010-12-01 16:34:4931
32namespace {
33
34const int kWindowHeight = 18;
35
36// The width of the bubble in relation to the width of the parent window.
37const CGFloat kWindowWidthPercent = 1.0 / 3.0;
38
39// How close the mouse can get to the infobubble before it starts sliding
40// off-screen.
41const int kMousePadding = 20;
42
43const int kTextPadding = 3;
44
[email protected]7d791652010-12-01 16:34:4945// The status bubble's maximum opacity, when fully faded in.
46const CGFloat kBubbleOpacity = 1.0;
47
48// Delay before showing or hiding the bubble after a SetStatus or SetURL call.
[email protected]9ad0f5bb2014-02-11 01:10:5949const int64 kShowDelayMS = 80;
50const int64 kHideDelayMS = 250;
[email protected]7d791652010-12-01 16:34:4951
52// How long each fade should last.
53const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
54const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
55
56// The minimum representable time interval. This can be used as the value
57// passed to +[NSAnimationContext setDuration:] to stop an in-progress
58// animation as quickly as possible.
59const NSTimeInterval kMinimumTimeInterval =
60 std::numeric_limits<NSTimeInterval>::min();
61
[email protected]9ad0f5bb2014-02-11 01:10:5962// How quickly the status bubble should expand.
63const CGFloat kExpansionDurationSeconds = 0.125;
[email protected]7d791652010-12-01 16:34:4964
65} // namespace
66
67@interface StatusBubbleAnimationDelegate : NSObject {
68 @private
[email protected]52a025a2014-02-15 03:47:4269 base::mac::ScopedBlock<void (^)(void)> completionHandler_;
[email protected]7d791652010-12-01 16:34:4970}
71
[email protected]52a025a2014-02-15 03:47:4272- (id)initWithCompletionHandler:(void (^)(void))completionHandler;
[email protected]7d791652010-12-01 16:34:4973
74// CAAnimation delegate method
75- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
76@end
77
78@implementation StatusBubbleAnimationDelegate
79
[email protected]52a025a2014-02-15 03:47:4280- (id)initWithCompletionHandler:(void (^)(void))completionHandler {
[email protected]7d791652010-12-01 16:34:4981 if ((self = [super init])) {
[email protected]52a025a2014-02-15 03:47:4282 completionHandler_.reset(completionHandler, base::scoped_policy::RETAIN);
[email protected]7d791652010-12-01 16:34:4983 }
84
85 return self;
86}
87
[email protected]52a025a2014-02-15 03:47:4288- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
89 completionHandler_.get()();
[email protected]7d791652010-12-01 16:34:4990}
91
[email protected]52a025a2014-02-15 03:47:4292@end
93
94@interface StatusBubbleWindow : NSWindow {
95 @private
96 void (^completionHandler_)(void);
97}
98
99- (id)animationForKey:(NSString *)key;
100- (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
101 completionHandler:(void (^)(void))completionHandler;
102@end
103
104@implementation StatusBubbleWindow
105
106- (id)animationForKey:(NSString *)key {
107 CAAnimation* animation = [super animationForKey:key];
108 // If completionHandler_ isn't nil, then this is the first of (potentially)
109 // multiple animations in a grouping; give it the completion handler. If
110 // completionHandler_ is nil, then some other animation was tagged with the
111 // completion handler.
112 if (completionHandler_) {
113 DCHECK(![NSAnimationContext respondsToSelector:
114 @selector(runAnimationGroup:completionHandler:)]);
115 StatusBubbleAnimationDelegate* animation_delegate =
116 [[StatusBubbleAnimationDelegate alloc]
117 initWithCompletionHandler:completionHandler_];
118 [animation setDelegate:animation_delegate];
119 completionHandler_ = nil;
120 }
121 return animation;
122}
123
124- (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
125 completionHandler:(void (^)(void))completionHandler {
126 if ([NSAnimationContext respondsToSelector:
127 @selector(runAnimationGroup:completionHandler:)]) {
128 [NSAnimationContext runAnimationGroup:changes
129 completionHandler:completionHandler];
130 } else {
131 // Mac OS 10.6 does not have completion handler callbacks at the Cocoa
132 // level, only at the CoreAnimation level. So intercept calls made to
133 // -animationForKey: and tag one of the animations with a delegate that will
134 // execute the completion handler.
135 completionHandler_ = completionHandler;
136 [NSAnimationContext beginGrouping];
137 changes([NSAnimationContext currentContext]);
138 // At this point, -animationForKey should have been called by CoreAnimation
139 // to set up the animation to run. Verify this.
140 DCHECK(completionHandler_ == nil);
141 [NSAnimationContext endGrouping];
142 }
[email protected]7d791652010-12-01 16:34:49143}
144
145@end
146
rohitraoda190482015-04-28 00:28:44147// Mac implementation of the status bubble.
148//
149// Child windows interact with Spaces in interesting ways, so this code has to
150// follow these rules:
151//
152// 1) NSWindows cannot have zero size. At times when the status bubble window
153// has no specific size (for example, when hidden), its size is set to
154// ui::kWindowSizeDeterminedLater.
155//
156// 2) Child window frames are in the coordinate space of the screen, not of the
157// parent window. If a child window has its origin at (0, 0), Spaces will
158// position it in the corner of the screen but group it with the parent
159// window in Spaces. This causes Chrome windows to have a large (mostly
160// blank) area in Spaces. To avoid this, child windows always have their
161// origin set to the lower-left corner of the window.
162//
163// 3) Detached child windows may show up as top-level windows in Spaces. To
164// avoid this, once the status bubble is Attach()ed to the parent, it is
165// never detached (except in rare cases when reparenting to a fullscreen
166// window).
167//
168// 4) To avoid unnecessary redraws, if a bubble is in the kBubbleHidden state,
169// its size is always set to ui::kWindowSizeDeterminedLater. The proper
170// width for the current URL or status text is not calculated until the
171// bubble leaves the kBubbleHidden state.
172
[email protected]7d791652010-12-01 16:34:49173StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
mohan.reddy1f994392014-09-17 05:30:05174 : parent_(parent),
[email protected]7d791652010-12-01 16:34:49175 delegate_(delegate),
176 window_(nil),
177 status_text_(nil),
178 url_text_(nil),
179 state_(kBubbleHidden),
180 immediate_(false),
mohan.reddy1f994392014-09-17 05:30:05181 is_expanded_(false),
182 timer_factory_(this),
183 expand_timer_factory_(this),
184 completion_handler_factory_(this) {
[email protected]a0c10182011-03-06 20:58:35185 Create();
186 Attach();
[email protected]7d791652010-12-01 16:34:49187}
188
189StatusBubbleMac::~StatusBubbleMac() {
[email protected]a0c10182011-03-06 20:58:35190 DCHECK(window_);
191
[email protected]7d791652010-12-01 16:34:49192 Hide();
193
[email protected]52a025a2014-02-15 03:47:42194 completion_handler_factory_.InvalidateWeakPtrs();
[email protected]a0c10182011-03-06 20:58:35195 Detach();
196 [window_ release];
197 window_ = nil;
[email protected]7d791652010-12-01 16:34:49198}
199
[email protected]dcd0249872013-12-06 23:58:45200void StatusBubbleMac::SetStatus(const base::string16& status) {
[email protected]7d791652010-12-01 16:34:49201 SetText(status, false);
202}
203
[email protected]65233052011-08-22 19:02:48204void StatusBubbleMac::SetURL(const GURL& url, const std::string& languages) {
[email protected]7d791652010-12-01 16:34:49205 url_ = url;
206 languages_ = languages;
207
rohitraoda190482015-04-28 00:28:44208 CGFloat bubble_width = NSWidth([window_ frame]);
[email protected]7d791652010-12-01 16:34:49209 if (state_ == kBubbleHidden) {
rohitraoda190482015-04-28 00:28:44210 // TODO(rohitrao): The window size is expected to be (1,1) whenever the
211 // window is hidden, but the GPU bots are hitting cases where this is not
212 // true. Instead of enforcing this invariant with a DCHECK, add temporary
213 // logging to try and debug it and fix up the window size if needed.
214 // This logging is temporary and should be removed: crbug.com/467998
215 NSRect frame = [window_ frame];
216 if (!CGSizeEqualToSize(frame.size, ui::kWindowSizeDeterminedLater.size)) {
217 LOG(ERROR) << "Window size should be (1,1), but is instead ("
218 << frame.size.width << "," << frame.size.height << ")";
219 LOG(ERROR) << base::debug::StackTrace().ToString();
220 frame.size = ui::kWindowSizeDeterminedLater.size;
221 [window_ setFrame:frame display:NO];
222 }
223 bubble_width = NSWidth(CalculateWindowFrame(/*expand=*/false));
[email protected]7d791652010-12-01 16:34:49224 }
225
rohitraoda190482015-04-28 00:28:44226 int text_width = static_cast<int>(bubble_width -
[email protected]7d791652010-12-01 16:34:49227 kBubbleViewTextPositionX -
228 kTextPadding);
229
230 // Scale from view to window coordinates before eliding URL string.
231 NSSize scaled_width = NSMakeSize(text_width, 0);
232 scaled_width = [[parent_ contentView] convertSize:scaled_width fromView:nil];
233 text_width = static_cast<int>(scaled_width.width);
234 NSFont* font = [[window_ contentView] font];
[email protected]035fa4c2013-11-07 11:27:33235 gfx::FontList font_list_chr(
andresantoso3c029bf2015-07-15 17:45:34236 gfx::Font(gfx::PlatformFont::CreateFromNativeFont(font)));
[email protected]7d791652010-12-01 16:34:49237
rsleevi24f64dc22015-08-07 21:39:21238 base::string16 original_url_text = url_formatter::FormatUrl(url, languages);
[email protected]dcd0249872013-12-06 23:58:45239 base::string16 status =
rsleevi24f64dc22015-08-07 21:39:21240 url_formatter::ElideUrl(url, font_list_chr, text_width, languages);
[email protected]7d791652010-12-01 16:34:49241
242 SetText(status, true);
243
244 // In testing, don't use animation. When ExpandBubble is tested, it is
245 // called explicitly.
246 if (immediate_)
247 return;
248 else
249 CancelExpandTimer();
250
251 // If the bubble has been expanded, the user has already hovered over a link
252 // to trigger the expanded state. Don't wait to change the bubble in this
253 // case -- immediately expand or contract to fit the URL.
254 if (is_expanded_ && !url.is_empty()) {
255 ExpandBubble();
256 } else if (original_url_text.length() > status.length()) {
[email protected]3264d162013-05-08 22:02:11257 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
[email protected]a932d9e2011-09-29 01:14:34258 base::Bind(&StatusBubbleMac::ExpandBubble,
259 expand_timer_factory_.GetWeakPtr()),
[email protected]9ad0f5bb2014-02-11 01:10:59260 base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS));
[email protected]7d791652010-12-01 16:34:49261 }
262}
263
[email protected]dcd0249872013-12-06 23:58:45264void StatusBubbleMac::SetText(const base::string16& text, bool is_url) {
[email protected]7d791652010-12-01 16:34:49265 // The status bubble allows the status and URL strings to be set
266 // independently. Whichever was set non-empty most recently will be the
267 // value displayed. When both are empty, the status bubble hides.
268
269 NSString* text_ns = base::SysUTF16ToNSString(text);
270
271 NSString** main;
272 NSString** backup;
273
274 if (is_url) {
275 main = &url_text_;
276 backup = &status_text_;
277 } else {
278 main = &status_text_;
279 backup = &url_text_;
280 }
281
282 // Don't return from this function early. It's important to make sure that
283 // all calls to StartShowing and StartHiding are made, so that all delays
284 // are observed properly. Specifically, if the state is currently
285 // kBubbleShowingTimer, the timer will need to be restarted even if
286 // [text_ns isEqualToString:*main] is true.
287
288 [*main autorelease];
289 *main = [text_ns retain];
290
291 bool show = true;
292 if ([*main length] > 0)
293 [[window_ contentView] setContent:*main];
294 else if ([*backup length] > 0)
295 [[window_ contentView] setContent:*backup];
296 else
297 show = false;
298
[email protected]61987b0c2011-06-03 18:39:27299 if (show) {
rohitraoda190482015-04-28 00:28:44300 // Call StartShowing() first to update the current bubble state before
301 // calculating a new size.
rohitrao7a15c812015-03-29 21:19:41302 StartShowing();
rohitraoda190482015-04-28 00:28:44303 UpdateSizeAndPosition();
[email protected]61987b0c2011-06-03 18:39:27304 } else {
[email protected]7d791652010-12-01 16:34:49305 StartHiding();
[email protected]61987b0c2011-06-03 18:39:27306 }
[email protected]7d791652010-12-01 16:34:49307}
308
309void StatusBubbleMac::Hide() {
310 CancelTimer();
311 CancelExpandTimer();
312 is_expanded_ = false;
313
314 bool fade_out = false;
315 if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
316 SetState(kBubbleHidingFadeOut);
317
318 if (!immediate_) {
319 // An animation is in progress. Cancel it by starting a new animation.
320 // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
321 fade_out = true;
[email protected]52a025a2014-02-15 03:47:42322 AnimateWindowAlpha(0.0, kMinimumTimeInterval);
[email protected]7d791652010-12-01 16:34:49323 }
324 }
325
rohitraoda190482015-04-28 00:28:44326 NSRect frame = CalculateWindowFrame(/*expand=*/false);
[email protected]7d791652010-12-01 16:34:49327 if (!fade_out) {
328 // No animation is in progress, so the opacity can be set directly.
329 [window_ setAlphaValue:0.0];
330 SetState(kBubbleHidden);
rohitraoda190482015-04-28 00:28:44331 frame.size = ui::kWindowSizeDeterminedLater.size;
[email protected]7d791652010-12-01 16:34:49332 }
333
334 // Stop any width animation and reset the bubble size.
335 if (!immediate_) {
336 [NSAnimationContext beginGrouping];
337 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
rohitraoda190482015-04-28 00:28:44338 [[window_ animator] setFrame:frame display:NO];
[email protected]7d791652010-12-01 16:34:49339 [NSAnimationContext endGrouping];
340 } else {
rohitraoda190482015-04-28 00:28:44341 [window_ setFrame:frame display:NO];
[email protected]7d791652010-12-01 16:34:49342 }
343
344 [status_text_ release];
345 status_text_ = nil;
346 [url_text_ release];
347 url_text_ = nil;
348}
349
[email protected]61987b0c2011-06-03 18:39:27350void StatusBubbleMac::SetFrameAvoidingMouse(
351 NSRect window_frame, const gfx::Point& mouse_pos) {
[email protected]7d791652010-12-01 16:34:49352 if (!window_)
353 return;
354
[email protected]61987b0c2011-06-03 18:39:27355 // Bubble's base rect in |parent_| (window base) coordinates.
356 NSRect base_rect;
357 if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
358 base_rect = [delegate_ statusBubbleBaseFrame];
359 } else {
360 base_rect = [[parent_ contentView] bounds];
361 base_rect = [[parent_ contentView] convertRect:base_rect toView:nil];
362 }
[email protected]7d791652010-12-01 16:34:49363
[email protected]61987b0c2011-06-03 18:39:27364 // To start, assume default positioning in the lower left corner.
365 // The window_frame position is in global (screen) coordinates.
366 window_frame.origin = [parent_ convertBaseToScreen:base_rect.origin];
[email protected]7d791652010-12-01 16:34:49367
[email protected]61987b0c2011-06-03 18:39:27368 // Get the cursor position relative to the top right corner of the bubble.
369 gfx::Point relative_pos(mouse_pos.x() - NSMaxX(window_frame),
370 mouse_pos.y() - NSMaxY(window_frame));
[email protected]7d791652010-12-01 16:34:49371
372 // If the mouse is in a position where we think it would move the
[email protected]1ba39c002011-08-03 21:05:36373 // status bubble, figure out where and how the bubble should be moved, and
374 // what sorts of corners it should have.
375 unsigned long corner_flags;
[email protected]61987b0c2011-06-03 18:39:27376 if (relative_pos.y() < kMousePadding &&
377 relative_pos.x() < kMousePadding) {
378 int offset = kMousePadding - relative_pos.y();
[email protected]7d791652010-12-01 16:34:49379
380 // Make the movement non-linear.
381 offset = offset * offset / kMousePadding;
382
383 // When the mouse is entering from the right, we want the offset to be
384 // scaled by how horizontally far away the cursor is from the bubble.
[email protected]61987b0c2011-06-03 18:39:27385 if (relative_pos.x() > 0) {
386 offset *= (kMousePadding - relative_pos.x()) / kMousePadding;
[email protected]7d791652010-12-01 16:34:49387 }
388
[email protected]61987b0c2011-06-03 18:39:27389 bool is_on_screen = true;
[email protected]7d791652010-12-01 16:34:49390 NSScreen* screen = [window_ screen];
391 if (screen &&
392 NSMinY([screen visibleFrame]) > NSMinY(window_frame) - offset) {
[email protected]61987b0c2011-06-03 18:39:27393 is_on_screen = false;
[email protected]7d791652010-12-01 16:34:49394 }
395
396 // If something is shown below tab contents (devtools, download shelf etc.),
397 // adjust the position to sit on top of it.
[email protected]61987b0c2011-06-03 18:39:27398 bool is_any_shelf_visible = NSMinY(base_rect) > 0;
[email protected]7d791652010-12-01 16:34:49399
[email protected]61987b0c2011-06-03 18:39:27400 if (is_on_screen && !is_any_shelf_visible) {
[email protected]7d791652010-12-01 16:34:49401 // Cap the offset and change the visual presentation of the bubble
402 // depending on where it ends up (so that rounded corners square off
403 // and mate to the edges of the tab content).
404 if (offset >= NSHeight(window_frame)) {
405 offset = NSHeight(window_frame);
[email protected]1ba39c002011-08-03 21:05:36406 corner_flags = kRoundedBottomLeftCorner | kRoundedBottomRightCorner;
[email protected]7d791652010-12-01 16:34:49407 } else if (offset > 0) {
[email protected]1ba39c002011-08-03 21:05:36408 corner_flags = kRoundedTopRightCorner |
409 kRoundedBottomLeftCorner |
410 kRoundedBottomRightCorner;
[email protected]7d791652010-12-01 16:34:49411 } else {
[email protected]1ba39c002011-08-03 21:05:36412 corner_flags = kRoundedTopRightCorner;
[email protected]7d791652010-12-01 16:34:49413 }
[email protected]61987b0c2011-06-03 18:39:27414
415 // Place the bubble on the left, but slightly lower.
[email protected]7d791652010-12-01 16:34:49416 window_frame.origin.y -= offset;
417 } else {
418 // Cannot move the bubble down without obscuring other content.
[email protected]61987b0c2011-06-03 18:39:27419 // Move it to the far right instead.
[email protected]1ba39c002011-08-03 21:05:36420 corner_flags = kRoundedTopLeftCorner;
[email protected]61987b0c2011-06-03 18:39:27421 window_frame.origin.x += NSWidth(base_rect) - NSWidth(window_frame);
[email protected]7d791652010-12-01 16:34:49422 }
423 } else {
[email protected]61987b0c2011-06-03 18:39:27424 // Use the default position in the lower left corner of the content area.
[email protected]1ba39c002011-08-03 21:05:36425 corner_flags = kRoundedTopRightCorner;
[email protected]7d791652010-12-01 16:34:49426 }
[email protected]1ba39c002011-08-03 21:05:36427
428 corner_flags |= OSDependentCornerFlags(window_frame);
429
430 [[window_ contentView] setCornerFlags:corner_flags];
[email protected]7d791652010-12-01 16:34:49431 [window_ setFrame:window_frame display:YES];
432}
433
[email protected]61987b0c2011-06-03 18:39:27434void StatusBubbleMac::MouseMoved(
435 const gfx::Point& location, bool left_content) {
436 if (!left_content)
437 SetFrameAvoidingMouse([window_ frame], location);
438}
439
[email protected]7d791652010-12-01 16:34:49440void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
[email protected]ff19e412011-04-20 23:13:30441 UpdateSizeAndPosition();
[email protected]7d791652010-12-01 16:34:49442}
443
444void StatusBubbleMac::Create() {
[email protected]a0c10182011-03-06 20:58:35445 DCHECK(!window_);
[email protected]7d791652010-12-01 16:34:49446
[email protected]52a025a2014-02-15 03:47:42447 window_ = [[StatusBubbleWindow alloc]
448 initWithContentRect:ui::kWindowSizeDeterminedLater
449 styleMask:NSBorderlessWindowMask
450 backing:NSBackingStoreBuffered
shessfa898142015-06-02 17:16:26451 defer:NO];
[email protected]7d791652010-12-01 16:34:49452 [window_ setMovableByWindowBackground:NO];
453 [window_ setBackgroundColor:[NSColor clearColor]];
454 [window_ setLevel:NSNormalWindowLevel];
455 [window_ setOpaque:NO];
456 [window_ setHasShadow:NO];
457
458 // We do not need to worry about the bubble outliving |parent_| because our
459 // teardown sequence in BWC guarantees that |parent_| outlives the status
460 // bubble and that the StatusBubble is torn down completely prior to the
461 // window going away.
[email protected]a8522032013-06-24 22:51:46462 base::scoped_nsobject<BubbleView> view(
[email protected]7d791652010-12-01 16:34:49463 [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
464 [window_ setContentView:view];
465
466 [window_ setAlphaValue:0.0];
467
[email protected]625cc3e2011-09-22 00:16:13468 // TODO(dtseng): Ignore until we provide NSAccessibility support.
469 [window_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
[email protected]9ad0f5bb2014-02-11 01:10:59470 forAttribute:NSAccessibilityRoleAttribute];
[email protected]625cc3e2011-09-22 00:16:13471
[email protected]7d791652010-12-01 16:34:49472 [view setCornerFlags:kRoundedTopRightCorner];
473 MouseMoved(gfx::Point(), false);
474}
475
476void StatusBubbleMac::Attach() {
[email protected]a0c10182011-03-06 20:58:35477 DCHECK(!is_attached());
478
[email protected]bf8651a2011-03-09 22:02:46479 [window_ orderFront:nil];
[email protected]a0c10182011-03-06 20:58:35480 [parent_ addChildWindow:window_ ordered:NSWindowAbove];
481
482 [[window_ contentView] setThemeProvider:parent_];
[email protected]7d791652010-12-01 16:34:49483}
484
485void StatusBubbleMac::Detach() {
[email protected]a0c10182011-03-06 20:58:35486 DCHECK(is_attached());
487
[email protected]9ad0f5bb2014-02-11 01:10:59488 // Magic setFrame: See http://crbug.com/58506 and http://crrev.com/3564021 .
rohitraoda190482015-04-28 00:28:44489 // TODO(rohitrao): Does the frame size actually matter here? Can we always
490 // set it to kWindowSizeDeterminedLater?
491 NSRect frame = [window_ frame];
492 frame.size = ui::kWindowSizeDeterminedLater.size;
493 if (state_ != kBubbleHidden) {
494 frame = CalculateWindowFrame(/*expand=*/false);
495 }
496 [window_ setFrame:frame display:NO];
[email protected]a0c10182011-03-06 20:58:35497 [parent_ removeChildWindow:window_]; // See crbug.com/28107 ...
498 [window_ orderOut:nil]; // ... and crbug.com/29054.
499
500 [[window_ contentView] setThemeProvider:nil];
[email protected]7d791652010-12-01 16:34:49501}
502
[email protected]52a025a2014-02-15 03:47:42503void StatusBubbleMac::AnimationDidStop() {
[email protected]7d791652010-12-01 16:34:49504 DCHECK([NSThread isMainThread]);
505 DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
506 DCHECK(is_attached());
507
[email protected]52a025a2014-02-15 03:47:42508 if (state_ == kBubbleShowingFadeIn) {
509 DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
510 SetState(kBubbleShown);
511 } else {
512 DCHECK_EQ([[window_ animator] alphaValue], 0.0);
513 SetState(kBubbleHidden);
[email protected]7d791652010-12-01 16:34:49514 }
515}
516
517void StatusBubbleMac::SetState(StatusBubbleState state) {
[email protected]7d791652010-12-01 16:34:49518 if (state == state_)
519 return;
520
[email protected]5afebbd2012-04-23 23:39:16521 if (state == kBubbleHidden) {
rohitraoda190482015-04-28 00:28:44522 is_expanded_ = false;
523
[email protected]5afebbd2012-04-23 23:39:16524 // When hidden (with alpha of 0), make the window have the minimum size,
525 // while still keeping the same origin. It's important to not set the
526 // origin to 0,0 as that will cause the window to use more space in
527 // Expose/Mission Control. See http://crbug.com/81969.
528 //
529 // Also, doing it this way instead of detaching the window avoids bugs with
530 // Spaces and Cmd-`. See http://crbug.com/31821 and http://crbug.com/61629.
531 NSRect frame = [window_ frame];
[email protected]9ad0f5bb2014-02-11 01:10:59532 frame.size = ui::kWindowSizeDeterminedLater.size;
[email protected]5afebbd2012-04-23 23:39:16533 [window_ setFrame:frame display:YES];
534 }
[email protected]7d791652010-12-01 16:34:49535
536 if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
537 [delegate_ statusBubbleWillEnterState:state];
538
539 state_ = state;
540}
541
542void StatusBubbleMac::Fade(bool show) {
543 DCHECK([NSThread isMainThread]);
544
545 StatusBubbleState fade_state = kBubbleShowingFadeIn;
546 StatusBubbleState target_state = kBubbleShown;
547 NSTimeInterval full_duration = kShowFadeInDurationSeconds;
548 CGFloat opacity = kBubbleOpacity;
549
550 if (!show) {
551 fade_state = kBubbleHidingFadeOut;
552 target_state = kBubbleHidden;
553 full_duration = kHideFadeOutDurationSeconds;
554 opacity = 0.0;
555 }
556
557 DCHECK(state_ == fade_state || state_ == target_state);
558
559 if (state_ == target_state)
560 return;
561
562 if (immediate_) {
563 [window_ setAlphaValue:opacity];
564 SetState(target_state);
565 return;
566 }
567
568 // If an incomplete transition has left the opacity somewhere between 0 and
569 // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
570 NSTimeInterval duration =
571 full_duration *
572 fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
573
574 // 0.0 will not cancel an in-progress animation.
575 if (duration == 0.0)
576 duration = kMinimumTimeInterval;
577
[email protected]52a025a2014-02-15 03:47:42578 // Cancel an in-progress transition and replace it with this fade.
579 AnimateWindowAlpha(opacity, duration);
580}
581
582void StatusBubbleMac::AnimateWindowAlpha(CGFloat alpha,
583 NSTimeInterval duration) {
584 completion_handler_factory_.InvalidateWeakPtrs();
585 base::WeakPtr<StatusBubbleMac> weak_ptr(
586 completion_handler_factory_.GetWeakPtr());
587 [window_
588 runAnimationGroup:^(NSAnimationContext* context) {
589 [context setDuration:duration];
590 [[window_ animator] setAlphaValue:alpha];
591 }
592 completionHandler:^{
593 if (weak_ptr)
594 weak_ptr->AnimationDidStop();
595 }];
[email protected]7d791652010-12-01 16:34:49596}
597
598void StatusBubbleMac::StartTimer(int64 delay_ms) {
599 DCHECK([NSThread isMainThread]);
600 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
601
602 if (immediate_) {
603 TimerFired();
604 return;
605 }
606
607 // There can only be one running timer.
608 CancelTimer();
609
[email protected]3264d162013-05-08 22:02:11610 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
[email protected]a932d9e2011-09-29 01:14:34611 base::Bind(&StatusBubbleMac::TimerFired, timer_factory_.GetWeakPtr()),
[email protected]35152752012-04-09 04:19:49612 base::TimeDelta::FromMilliseconds(delay_ms));
[email protected]7d791652010-12-01 16:34:49613}
614
615void StatusBubbleMac::CancelTimer() {
616 DCHECK([NSThread isMainThread]);
617
[email protected]a932d9e2011-09-29 01:14:34618 if (timer_factory_.HasWeakPtrs())
619 timer_factory_.InvalidateWeakPtrs();
[email protected]7d791652010-12-01 16:34:49620}
621
622void StatusBubbleMac::TimerFired() {
623 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
624 DCHECK([NSThread isMainThread]);
625
626 if (state_ == kBubbleShowingTimer) {
627 SetState(kBubbleShowingFadeIn);
628 Fade(true);
629 } else {
630 SetState(kBubbleHidingFadeOut);
631 Fade(false);
632 }
633}
634
635void StatusBubbleMac::StartShowing() {
[email protected]7d791652010-12-01 16:34:49636 if (state_ == kBubbleHidden) {
637 // Arrange to begin fading in after a delay.
638 SetState(kBubbleShowingTimer);
[email protected]9ad0f5bb2014-02-11 01:10:59639 StartTimer(kShowDelayMS);
[email protected]7d791652010-12-01 16:34:49640 } else if (state_ == kBubbleHidingFadeOut) {
641 // Cancel the fade-out in progress and replace it with a fade in.
642 SetState(kBubbleShowingFadeIn);
643 Fade(true);
644 } else if (state_ == kBubbleHidingTimer) {
645 // The bubble was already shown but was waiting to begin fading out. It's
646 // given a stay of execution.
647 SetState(kBubbleShown);
648 CancelTimer();
649 } else if (state_ == kBubbleShowingTimer) {
650 // The timer was already running but nothing was showing yet. Reaching
651 // this point means that there is a new request to show something. Start
652 // over again by resetting the timer, effectively invalidating the earlier
653 // request.
[email protected]9ad0f5bb2014-02-11 01:10:59654 StartTimer(kShowDelayMS);
[email protected]7d791652010-12-01 16:34:49655 }
656
657 // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
658 // alone.
659}
660
661void StatusBubbleMac::StartHiding() {
662 if (state_ == kBubbleShown) {
663 // Arrange to begin fading out after a delay.
664 SetState(kBubbleHidingTimer);
[email protected]9ad0f5bb2014-02-11 01:10:59665 StartTimer(kHideDelayMS);
[email protected]7d791652010-12-01 16:34:49666 } else if (state_ == kBubbleShowingFadeIn) {
667 // Cancel the fade-in in progress and replace it with a fade out.
668 SetState(kBubbleHidingFadeOut);
669 Fade(false);
670 } else if (state_ == kBubbleShowingTimer) {
671 // The bubble was already hidden but was waiting to begin fading in. Too
672 // bad, it won't get the opportunity now.
673 SetState(kBubbleHidden);
674 CancelTimer();
675 }
676
677 // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
678 // kBubbleHidingTimer, leave everything alone. The timer is not reset as
679 // with kBubbleShowingTimer in StartShowing() because a subsequent request
680 // to hide something while one is already in flight does not invalidate the
681 // earlier request.
682}
683
684void StatusBubbleMac::CancelExpandTimer() {
685 DCHECK([NSThread isMainThread]);
[email protected]a932d9e2011-09-29 01:14:34686 expand_timer_factory_.InvalidateWeakPtrs();
[email protected]7d791652010-12-01 16:34:49687}
688
[email protected]61987b0c2011-06-03 18:39:27689// Get the current location of the mouse in screen coordinates. To make this
690// class testable, all code should use this method rather than using
691// NSEvent mouseLocation directly.
692gfx::Point StatusBubbleMac::GetMouseLocation() {
693 NSPoint p = [NSEvent mouseLocation];
694 --p.y; // The docs say the y coord starts at 1 not 0; don't ask why.
695 return gfx::Point(p.x, p.y);
696}
697
[email protected]7d791652010-12-01 16:34:49698void StatusBubbleMac::ExpandBubble() {
699 // Calculate the width available for expanded and standard bubbles.
700 NSRect window_frame = CalculateWindowFrame(/*expand=*/true);
701 CGFloat max_bubble_width = NSWidth(window_frame);
702 CGFloat standard_bubble_width =
703 NSWidth(CalculateWindowFrame(/*expand=*/false));
704
705 // Generate the URL string that fits in the expanded bubble.
706 NSFont* font = [[window_ contentView] font];
[email protected]035fa4c2013-11-07 11:27:33707 gfx::FontList font_list_chr(
andresantoso3c029bf2015-07-15 17:45:34708 gfx::Font(gfx::PlatformFont::CreateFromNativeFont(font)));
rsleevi24f64dc22015-08-07 21:39:21709 base::string16 expanded_url = url_formatter::ElideUrl(
[email protected]035fa4c2013-11-07 11:27:33710 url_, font_list_chr, max_bubble_width, languages_);
[email protected]7d791652010-12-01 16:34:49711
712 // Scale width from gfx::Font in view coordinates to window coordinates.
713 int required_width_for_string =
[email protected]eaa4f142014-01-23 09:35:49714 gfx::GetStringWidth(expanded_url, font_list_chr) +
[email protected]7d791652010-12-01 16:34:49715 kTextPadding * 2 + kBubbleViewTextPositionX;
716 NSSize scaled_width = NSMakeSize(required_width_for_string, 0);
717 scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil];
718 required_width_for_string = scaled_width.width;
719
720 // The expanded width must be at least as wide as the standard width, but no
721 // wider than the maximum width for its parent frame.
722 int expanded_bubble_width =
723 std::max(standard_bubble_width,
724 std::min(max_bubble_width,
725 static_cast<CGFloat>(required_width_for_string)));
726
727 SetText(expanded_url, true);
728 is_expanded_ = true;
729 window_frame.size.width = expanded_bubble_width;
730
731 // In testing, don't do any animation.
732 if (immediate_) {
733 [window_ setFrame:window_frame display:YES];
734 return;
735 }
736
737 NSRect actual_window_frame = [window_ frame];
738 // Adjust status bubble origin if bubble was moved to the right.
739 // TODO(alekseys): fix for RTL.
740 if (NSMinX(actual_window_frame) > NSMinX(window_frame)) {
741 actual_window_frame.origin.x =
742 NSMaxX(actual_window_frame) - NSWidth(window_frame);
743 }
744 actual_window_frame.size.width = NSWidth(window_frame);
745
746 // Do not expand if it's going to cover mouse location.
[email protected]61987b0c2011-06-03 18:39:27747 gfx::Point p = GetMouseLocation();
748 if (NSPointInRect(NSMakePoint(p.x(), p.y()), actual_window_frame))
[email protected]7d791652010-12-01 16:34:49749 return;
750
[email protected]1ba39c002011-08-03 21:05:36751 // Get the current corner flags and see what needs to change based on the
752 // expansion. This is only needed on Lion, which has rounded window bottoms.
753 if (base::mac::IsOSLionOrLater()) {
754 unsigned long corner_flags = [[window_ contentView] cornerFlags];
755 corner_flags |= OSDependentCornerFlags(actual_window_frame);
756 [[window_ contentView] setCornerFlags:corner_flags];
757 }
758
[email protected]7d791652010-12-01 16:34:49759 [NSAnimationContext beginGrouping];
[email protected]9ad0f5bb2014-02-11 01:10:59760 [[NSAnimationContext currentContext] setDuration:kExpansionDurationSeconds];
[email protected]7d791652010-12-01 16:34:49761 [[window_ animator] setFrame:actual_window_frame display:YES];
762 [NSAnimationContext endGrouping];
763}
764
765void StatusBubbleMac::UpdateSizeAndPosition() {
766 if (!window_)
767 return;
768
rohitraoda190482015-04-28 00:28:44769 // There is no need to update the size if the bubble is hidden.
770 if (state_ == kBubbleHidden) {
771 // Verify that hidden bubbles always have size equal to
772 // ui::kWindowSizeDeterminedLater.
773
774 // TODO(rohitrao): The GPU bots are hitting cases where this is not true.
775 // Instead of enforcing this invariant with a DCHECK, add temporary logging
776 // to try and debug it and fix up the window size if needed.
777 // This logging is temporary and should be removed: crbug.com/467998
778 NSRect frame = [window_ frame];
779 if (!CGSizeEqualToSize(frame.size, ui::kWindowSizeDeterminedLater.size)) {
780 LOG(ERROR) << "Window size should be (1,1), but is instead ("
781 << frame.size.width << "," << frame.size.height << ")";
782 LOG(ERROR) << base::debug::StackTrace().ToString();
783 frame.size = ui::kWindowSizeDeterminedLater.size;
784 }
785
786 // During the fullscreen animation, the parent window's origin may change
787 // without updating the status bubble. To avoid animation glitches, always
788 // update the bubble's origin to match the parent's, even when hidden.
789 frame.origin = [parent_ frame].origin;
790 [window_ setFrame:frame display:NO];
791 return;
792 }
793
[email protected]61987b0c2011-06-03 18:39:27794 SetFrameAvoidingMouse(CalculateWindowFrame(/*expand=*/false),
795 GetMouseLocation());
[email protected]7d791652010-12-01 16:34:49796}
797
798void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
799 DCHECK(parent);
[email protected]a0c10182011-03-06 20:58:35800 DCHECK(is_attached());
[email protected]7d791652010-12-01 16:34:49801
802 Detach();
803 parent_ = parent;
[email protected]7d791652010-12-01 16:34:49804 Attach();
805 UpdateSizeAndPosition();
806}
807
808NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) {
809 DCHECK(parent_);
810
811 NSRect screenRect;
812 if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
813 screenRect = [delegate_ statusBubbleBaseFrame];
814 screenRect.origin = [parent_ convertBaseToScreen:screenRect.origin];
815 } else {
816 screenRect = [parent_ frame];
817 }
818
819 NSSize size = NSMakeSize(0, kWindowHeight);
820 size = [[parent_ contentView] convertSize:size toView:nil];
821
822 if (expanded_width) {
823 size.width = screenRect.size.width;
824 } else {
825 size.width = kWindowWidthPercent * screenRect.size.width;
826 }
827
828 screenRect.size = size;
829 return screenRect;
830}
[email protected]1ba39c002011-08-03 21:05:36831
832unsigned long StatusBubbleMac::OSDependentCornerFlags(NSRect window_frame) {
833 unsigned long corner_flags = 0;
834
835 if (base::mac::IsOSLionOrLater()) {
836 NSRect parent_frame = [parent_ frame];
837
838 // Round the bottom corners when they're right up against the
839 // corresponding edge of the parent window, or when below the parent
840 // window.
841 if (NSMinY(window_frame) <= NSMinY(parent_frame)) {
842 if (NSMinX(window_frame) == NSMinX(parent_frame)) {
843 corner_flags |= kRoundedBottomLeftCorner;
844 }
845
846 if (NSMaxX(window_frame) == NSMaxX(parent_frame)) {
847 corner_flags |= kRoundedBottomRightCorner;
848 }
849 }
850
851 // Round the top corners when the bubble is below the parent window.
852 if (NSMinY(window_frame) < NSMinY(parent_frame)) {
853 corner_flags |= kRoundedTopLeftCorner | kRoundedTopRightCorner;
854 }
855 }
856
857 return corner_flags;
858}