Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(257)

Side by Side Diff: chrome/browser/cocoa/status_bubble_mac.mm

Issue 269045: Mac status bubble delays and fades (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 11 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2009 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/cocoa/status_bubble_mac.h" 5 #include "chrome/browser/cocoa/status_bubble_mac.h"
6 6
7 #include <limits>
8
7 #include "app/gfx/text_elider.h" 9 #include "app/gfx/text_elider.h"
10 #include "base/compiler_specific.h"
11 #include "base/message_loop.h"
8 #include "base/string_util.h" 12 #include "base/string_util.h"
9 #include "base/sys_string_conversions.h" 13 #include "base/sys_string_conversions.h"
10 #import "chrome/browser/cocoa/bubble_view.h" 14 #import "chrome/browser/cocoa/bubble_view.h"
11 #include "googleurl/src/gurl.h" 15 #include "googleurl/src/gurl.h"
12 #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" 16 #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
13 #import "third_party/GTM/AppKit/GTMNSColor+Luminance.h" 17 #import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
14 #import "third_party/GTM/AppKit/GTMTheme.h" 18 #import "third_party/GTM/AppKit/GTMTheme.h"
15 19
16 namespace { 20 namespace {
17 21
18 const int kWindowHeight = 18; 22 const int kWindowHeight = 18;
23
19 // The width of the bubble in relation to the width of the parent window. 24 // The width of the bubble in relation to the width of the parent window.
20 const float kWindowWidthPercent = 1.0f/3.0f; 25 const double kWindowWidthPercent = 1.0 / 3.0;
21 26
22 // How close the mouse can get to the infobubble before it starts sliding 27 // How close the mouse can get to the infobubble before it starts sliding
23 // off-screen. 28 // off-screen.
24 const int kMousePadding = 20; 29 const int kMousePadding = 20;
25 30
26 const int kTextPadding = 3; 31 const int kTextPadding = 3;
27 32
28 // How long each fade should last for. 33 // The animation key used for fade-in and fade-out transitions.
29 const int kShowFadeDuration = 0.120f; 34 const NSString* kFadeAnimationKey = @"alphaValue";
30 const int kHideFadeDuration = 0.200f;
31 35
36 // The status bubble's maximum opacity, when fully faded in.
37 const CGFloat kBubbleOpacity = 1.0;
38
39 // Delay before showing or hiding the bubble after a SetStatus or SetURL call.
40 const int64 kShowDelayMilliseconds = 80;
41 const int64 kHideDelayMilliseconds = 250;
42
43 // How long each fade should last.
44 const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
45 const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
46
47 // The minimum representable time interval. This can be used as the value
48 // passed to +[NSAnimationContext setDuration:] to stop an in-progress
49 // animation as quickly as possible.
50 const NSTimeInterval kMinimumTimeInterval =
51 std::numeric_limits<NSTimeInterval>::min();
52
53 } // namespace
54
55 @interface StatusBubbleAnimationDelegate : NSObject {
56 @private
57 StatusBubbleMac* statusBubble_; // weak; owns us indirectly
32 } 58 }
33 59
34 // TODO(avi): 60 - (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble;
35 // - do display delay 61
62 // Invalidates this object so that no further calls will be made to
63 // statusBubble_. This should be called when statusBubble_ is released, to
64 // prevent attempts to call into the released object.
65 - (void)invalidate;
66
67 // CAAnimation delegate method
68 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
69 @end
70
71 @implementation StatusBubbleAnimationDelegate
72
73 - (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble {
74 if ((self = [super init])) {
75 statusBubble_ = statusBubble;
76 }
77
78 return self;
79 }
80
81 - (void)invalidate {
82 statusBubble_ = NULL;
83 }
84
85 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
86 if (statusBubble_)
87 statusBubble_->AnimationDidStop(animation, finished ? true : false);
88 }
89
90 @end
36 91
37 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate) 92 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
38 : parent_(parent), 93 : ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)),
94 parent_(parent),
39 delegate_(delegate), 95 delegate_(delegate),
40 window_(nil), 96 window_(nil),
41 status_text_(nil), 97 status_text_(nil),
42 url_text_(nil) { 98 url_text_(nil),
99 state_(kBubbleHidden),
100 immediate_(false) {
43 } 101 }
44 102
45 StatusBubbleMac::~StatusBubbleMac() { 103 StatusBubbleMac::~StatusBubbleMac() {
46 Hide(); 104 Hide();
105
106 if (window_) {
107 [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate];
108 [parent_ removeChildWindow:window_];
109 [window_ release];
110 window_ = nil;
111 }
47 } 112 }
48 113
49 void StatusBubbleMac::SetStatus(const std::wstring& status) { 114 void StatusBubbleMac::SetStatus(const std::wstring& status) {
50 Create(); 115 Create();
51 116
52 NSString* status_ns = base::SysWideToNSString(status); 117 SetText(status, false);
53
54 SetStatus(status_ns, false);
55 } 118 }
56 119
57 void StatusBubbleMac::SetURL(const GURL& url, const std::wstring& languages) { 120 void StatusBubbleMac::SetURL(const GURL& url, const std::wstring& languages) {
58 Create(); 121 Create();
59 122
60 NSRect frame = [window_ frame]; 123 NSRect frame = [window_ frame];
61 int text_width = static_cast<int>(frame.size.width - 124 int text_width = static_cast<int>(frame.size.width -
62 kBubbleViewTextPositionX - 125 kBubbleViewTextPositionX -
63 kTextPadding); 126 kTextPadding);
64 NSFont* font = [[window_ contentView] font]; 127 NSFont* font = [[window_ contentView] font];
65 gfx::Font font_chr = 128 gfx::Font font_chr =
66 gfx::Font::CreateFont(base::SysNSStringToWide([font fontName]), 129 gfx::Font::CreateFont(base::SysNSStringToWide([font fontName]),
67 [font pointSize]); 130 [font pointSize]);
68 131
69 std::wstring status = gfx::ElideUrl(url, font_chr, text_width, languages); 132 std::wstring status = gfx::ElideUrl(url, font_chr, text_width, languages);
70 NSString* status_ns = base::SysWideToNSString(status);
71 133
72 SetStatus(status_ns, true); 134 SetText(status, true);
73 } 135 }
74 136
75 void StatusBubbleMac::SetStatus(NSString* status, bool is_url) { 137 void StatusBubbleMac::SetText(const std::wstring& text, bool is_url) {
138 // The status bubble allows the status and URL strings to be set
139 // independently. Whichever was set non-empty most recently will be the
140 // value displayed. When both are empty, the status bubble hides.
141
142 NSString* text_ns = base::SysWideToNSString(text);
143
76 NSString** main; 144 NSString** main;
77 NSString** backup; 145 NSString** backup;
78 146
79 if (is_url) { 147 if (is_url) {
80 main = &url_text_; 148 main = &url_text_;
81 backup = &status_text_; 149 backup = &status_text_;
82 } else { 150 } else {
83 main = &status_text_; 151 main = &status_text_;
84 backup = &url_text_; 152 backup = &url_text_;
85 } 153 }
86 154
87 if ([status isEqualToString:*main]) 155 // Don't return from this function early. It's important to make sure that
88 return; 156 // all calls to StartShowing and StartHiding are made, so that all delays
157 // are observed properly. Specifically, if the state is currently
158 // kBubbleShowingTimer, the timer will need to be restarted even if
159 // [text_ns isEqualToString:*main] is true.
89 160
90 [*main release]; 161 [*main autorelease];
91 *main = [status retain]; 162 *main = [text_ns retain];
92 if ([*main length] > 0) { 163
164 bool show = true;
165 if ([*main length] > 0)
93 [[window_ contentView] setContent:*main]; 166 [[window_ contentView] setContent:*main];
94 } else if ([*backup length] > 0) { 167 else if ([*backup length] > 0)
95 [[window_ contentView] setContent:*backup]; 168 [[window_ contentView] setContent:*backup];
96 } else { 169 else
97 Hide(); 170 show = false;
98 }
99 171
100 FadeIn(); 172 if (show)
173 StartShowing();
174 else
175 StartHiding();
101 } 176 }
102 177
103 void StatusBubbleMac::Hide() { 178 void StatusBubbleMac::Hide() {
104 FadeOut(); 179 CancelTimer();
105 180
106 if (window_) { 181 bool fade_out = false;
107 [parent_ removeChildWindow:window_]; 182 if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
108 [window_ release]; 183 SetState(kBubbleHidingFadeOut);
109 window_ = nil; 184
185 if (!immediate_) {
186 // An animation is in progress. Cancel it by starting a new animation.
187 // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
188 fade_out = true;
189 [NSAnimationContext beginGrouping];
190 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
191 [[window_ animator] setAlphaValue:0.0];
192 [NSAnimationContext endGrouping];
193 }
194 }
195
196 if (!fade_out) {
197 // No animation is in progress, so the opacity can be set directly.
198 [window_ setAlphaValue:0.0];
199 SetState(kBubbleHidden);
110 } 200 }
111 201
112 [status_text_ release]; 202 [status_text_ release];
113 status_text_ = nil; 203 status_text_ = nil;
114 [url_text_ release]; 204 [url_text_ release];
115 url_text_ = nil; 205 url_text_ = nil;
116 } 206 }
117 207
118 void StatusBubbleMac::MouseMoved() { 208 void StatusBubbleMac::MouseMoved() {
119 if (!window_) 209 if (!window_)
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
158 [[window_ contentView] setCornerFlags: 248 [[window_ contentView] setCornerFlags:
159 kRoundedBottomLeftCorner | kRoundedBottomRightCorner]; 249 kRoundedBottomLeftCorner | kRoundedBottomRightCorner];
160 } else if (offset > 0) { 250 } else if (offset > 0) {
161 [[window_ contentView] setCornerFlags: 251 [[window_ contentView] setCornerFlags:
162 kRoundedTopRightCorner | kRoundedBottomLeftCorner | 252 kRoundedTopRightCorner | kRoundedBottomLeftCorner |
163 kRoundedBottomRightCorner]; 253 kRoundedBottomRightCorner];
164 } else { 254 } else {
165 [[window_ contentView] setCornerFlags:kRoundedTopRightCorner]; 255 [[window_ contentView] setCornerFlags:kRoundedTopRightCorner];
166 } 256 }
167 257
168 offset_ = offset;
169 window_frame.origin.y -= offset; 258 window_frame.origin.y -= offset;
170 } else { 259 } else {
171 offset_ = 0;
172 [[window_ contentView] setCornerFlags:kRoundedTopRightCorner]; 260 [[window_ contentView] setCornerFlags:kRoundedTopRightCorner];
173 } 261 }
174 262
175 [window_ setFrame:window_frame display:YES]; 263 [window_ setFrame:window_frame display:YES];
176 } 264 }
177 265
178 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) { 266 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
179 } 267 }
180 268
181 void StatusBubbleMac::Create() { 269 void StatusBubbleMac::Create() {
(...skipping 12 matching lines...) Expand all
194 [window_ setHasShadow:NO]; 282 [window_ setHasShadow:NO];
195 283
196 // We do not need to worry about the bubble outliving |parent_| because our 284 // We do not need to worry about the bubble outliving |parent_| because our
197 // teardown sequence in BWC guarantees that |parent_| outlives the status 285 // teardown sequence in BWC guarantees that |parent_| outlives the status
198 // bubble and that the StatusBubble is torn down completely prior to the 286 // bubble and that the StatusBubble is torn down completely prior to the
199 // window going away. 287 // window going away.
200 scoped_nsobject<BubbleView> view( 288 scoped_nsobject<BubbleView> view(
201 [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]); 289 [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
202 [window_ setContentView:view]; 290 [window_ setContentView:view];
203 291
204 [parent_ addChildWindow:window_ ordered:NSWindowAbove]; 292 [window_ setAlphaValue:0.0];
205 293
206 [window_ setAlphaValue:0.0f]; 294 // Set a delegate for the fade-in and fade-out transitions to be notified
295 // when fades are complete. The ownership model is for window_ to own
296 // animation_dictionary, which owns animation, which owns
297 // animation_delegate.
298 CAAnimation* animation = [[window_ animationForKey:kFadeAnimationKey] copy];
299 [animation autorelease];
300 StatusBubbleAnimationDelegate* animation_delegate =
301 [[StatusBubbleAnimationDelegate alloc] initWithStatusBubble:this];
302 [animation_delegate autorelease];
303 [animation setDelegate:animation_delegate];
304 NSMutableDictionary* animation_dictionary =
305 [NSMutableDictionary dictionaryWithDictionary:[window_ animations]];
306 [animation_dictionary setObject:animation forKey:kFadeAnimationKey];
307 [window_ setAnimations:animation_dictionary];
207 308
208 offset_ = 0; 309 Attach();
310
209 [view setCornerFlags:kRoundedTopRightCorner]; 311 [view setCornerFlags:kRoundedTopRightCorner];
210 MouseMoved(); 312 MouseMoved();
211 } 313 }
212 314
213 void StatusBubbleMac::FadeIn() { 315 void StatusBubbleMac::Attach() {
316 // If the parent window is offscreen when the child is added, the child will
317 // never be displayed, even when the parent moves on-screen. This method
318 // may be called several times during the process of creating or showing a
319 // status bubble to attach the bubble to its parent window.
320 if (![window_ parentWindow] && [parent_ isVisible])
321 [parent_ addChildWindow:window_ ordered:NSWindowAbove];
322 }
323
324 void StatusBubbleMac::AnimationDidStop(CAAnimation* animation, bool finished) {
325 DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
326
327 if (finished) {
328 // Because of the mechanism used to interrupt animations, this is never
329 // actually called with finished set to false. If animations ever become
330 // directly interruptible, the check will ensure that state_ remains
331 // properly synchronized.
332 if (state_ == kBubbleShowingFadeIn) {
333 DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
334 state_ = kBubbleShown;
335 } else {
336 DCHECK_EQ([[window_ animator] alphaValue], 0.0);
337 state_ = kBubbleHidden;
338 }
339 }
340 }
341
342 void StatusBubbleMac::SetState(StatusBubbleState state) {
343 if (state == state_)
344 return;
345
346 if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
347 [delegate_ statusBubbleWillEnterState:state];
348
349 state_ = state;
350 }
351
352 void StatusBubbleMac::Fade(bool show) {
353 StatusBubbleState fade_state = kBubbleShowingFadeIn;
354 StatusBubbleState target_state = kBubbleShown;
355 NSTimeInterval full_duration = kShowFadeInDurationSeconds;
356 CGFloat opacity = kBubbleOpacity;
357
358 if (!show) {
359 fade_state = kBubbleHidingFadeOut;
360 target_state = kBubbleHidden;
361 full_duration = kHideFadeOutDurationSeconds;
362 opacity = 0.0;
363 }
364
365 DCHECK(state_ == fade_state || state_ == target_state);
366
367 if (state_ == target_state)
368 return;
369
370 Attach();
371
372 if (immediate_) {
373 [window_ setAlphaValue:opacity];
374 SetState(target_state);
375 return;
376 }
377
378 // If an incomplete transition has left the opacity somewhere between 0 and
379 // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
380 NSTimeInterval duration =
381 full_duration *
382 fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
383
384 // 0.0 will not cancel an in-progress animation.
385 if (duration == 0.0)
386 duration = kMinimumTimeInterval;
387
388 // This will cancel an in-progress transition and replace it with this fade.
214 [NSAnimationContext beginGrouping]; 389 [NSAnimationContext beginGrouping];
215 [[NSAnimationContext currentContext] setDuration:kShowFadeDuration]; 390 [[NSAnimationContext currentContext] setDuration:duration];
216 [[window_ animator] setAlphaValue:1.0f]; 391 [[window_ animator] setAlphaValue:opacity];
217 [NSAnimationContext endGrouping]; 392 [NSAnimationContext endGrouping];
218 } 393 }
219 394
220 void StatusBubbleMac::FadeOut() { 395 void StatusBubbleMac::StartTimer(int64 delay_ms) {
221 [NSAnimationContext beginGrouping]; 396 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
222 [[NSAnimationContext currentContext] setDuration:kHideFadeDuration]; 397
223 [[window_ animator] setAlphaValue:0.0f]; 398 if (immediate_) {
224 [NSAnimationContext endGrouping]; 399 TimerFired();
400 return;
401 }
402
403 // There can only be one running timer.
404 CancelTimer();
405
406 MessageLoop::current()->PostDelayedTask(
407 FROM_HERE,
408 timer_factory_.NewRunnableMethod(&StatusBubbleMac::TimerFired),
409 delay_ms);
410 }
411
412 void StatusBubbleMac::CancelTimer() {
413 if (!timer_factory_.empty())
414 timer_factory_.RevokeAll();
415 }
416
417 void StatusBubbleMac::TimerFired() {
418 DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
419
420 if (state_ == kBubbleShowingTimer) {
421 SetState(kBubbleShowingFadeIn);
422 Fade(true);
423 } else {
424 SetState(kBubbleHidingFadeOut);
425 Fade(false);
426 }
427 }
428
429 void StatusBubbleMac::StartShowing() {
430 Attach();
431
432 if (state_ == kBubbleHidden) {
433 // Arrange to begin fading in after a delay.
434 SetState(kBubbleShowingTimer);
435 StartTimer(kShowDelayMilliseconds);
436 } else if (state_ == kBubbleHidingFadeOut) {
437 // Cancel the fade-out in progress and replace it with a fade in.
438 SetState(kBubbleShowingFadeIn);
439 Fade(true);
440 } else if (state_ == kBubbleHidingTimer) {
441 // The bubble was already shown but was waiting to begin fading out. It's
442 // given a stay of execution.
443 SetState(kBubbleShown);
444 CancelTimer();
445 } else if (state_ == kBubbleShowingTimer) {
446 // The timer was already running but nothing was showing yet. Reaching
447 // this point means that there is a new request to show something. Start
448 // over again by resetting the timer, effectively invalidating the earlier
449 // request.
450 StartTimer(kShowDelayMilliseconds);
451 }
452
453 // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
454 // alone.
455 }
456
457 void StatusBubbleMac::StartHiding() {
458 if (state_ == kBubbleShown) {
459 // Arrange to begin fading out after a delay.
460 SetState(kBubbleHidingTimer);
461 StartTimer(kHideDelayMilliseconds);
462 } else if (state_ == kBubbleShowingFadeIn) {
463 // Cancel the fade-in in progress and replace it with a fade out.
464 SetState(kBubbleHidingFadeOut);
465 Fade(false);
466 } else if (state_ == kBubbleShowingTimer) {
467 // The bubble was already hidden but was waiting to begin fading in. Too
468 // bad, it won't get the opportunity now.
469 SetState(kBubbleHidden);
470 CancelTimer();
471 }
472
473 // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
474 // kBubbleHidingTimer, leave everything alone. The timer is not reset as
475 // with kBubbleShowingTimer in StartShowing() because a subsequent request
476 // to hide something while one is already in flight does not invalidate the
477 // earlier request.
225 } 478 }
226 479
227 void StatusBubbleMac::UpdateSizeAndPosition() { 480 void StatusBubbleMac::UpdateSizeAndPosition() {
228 if (!window_) 481 if (!window_)
229 return; 482 return;
230 483
231 [window_ setFrame:CalculateWindowFrame() display:YES]; 484 [window_ setFrame:CalculateWindowFrame() display:YES];
232 } 485 }
233 486
234 NSRect StatusBubbleMac::CalculateWindowFrame() { 487 NSRect StatusBubbleMac::CalculateWindowFrame() {
235 DCHECK(parent_); 488 DCHECK(parent_);
236 489
237 NSRect rect = [parent_ frame]; 490 NSRect rect = [parent_ frame];
238 rect.size.height = kWindowHeight; 491 rect.size.height = kWindowHeight;
239 rect.size.width = static_cast<int>(kWindowWidthPercent * rect.size.width); 492 rect.size.width = static_cast<int>(kWindowWidthPercent * rect.size.width);
240 return rect; 493 return rect;
241 } 494 }
OLDNEW
« no previous file with comments | « chrome/browser/cocoa/status_bubble_mac.h ('k') | chrome/browser/cocoa/status_bubble_mac_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698