| OLD | NEW |
| 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> | 7 #include <limits> |
| 8 | 8 |
| 9 #include "app/text_elider.h" | 9 #include "app/text_elider.h" |
| 10 #include "base/compiler_specific.h" | 10 #include "base/compiler_specific.h" |
| 11 #include "base/message_loop.h" | 11 #include "base/message_loop.h" |
| 12 #include "base/string_util.h" | 12 #include "base/string_util.h" |
| 13 #include "base/sys_string_conversions.h" | 13 #include "base/sys_string_conversions.h" |
| 14 #include "base/utf_string_conversions.h" | 14 #include "base/utf_string_conversions.h" |
| 15 #import "chrome/browser/cocoa/bubble_view.h" | 15 #import "chrome/browser/cocoa/bubble_view.h" |
| 16 #include "gfx/point.h" | 16 #include "gfx/point.h" |
| 17 #include "googleurl/src/gurl.h" | 17 #include "net/base/net_util.h" |
| 18 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" | 18 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" |
| 19 #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" | 19 #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" |
| 20 #import "third_party/GTM/AppKit/GTMNSColor+Luminance.h" | 20 #import "third_party/GTM/AppKit/GTMNSColor+Luminance.h" |
| 21 | 21 |
| 22 namespace { | 22 namespace { |
| 23 | 23 |
| 24 const int kWindowHeight = 18; | 24 const int kWindowHeight = 18; |
| 25 | 25 |
| 26 // The width of the bubble in relation to the width of the parent window. | 26 // The width of the bubble in relation to the width of the parent window. |
| 27 const double kWindowWidthPercent = 1.0 / 3.0; | 27 const double kWindowWidthPercent = 1.0 / 3.0; |
| (...skipping 17 matching lines...) Expand all Loading... |
| 45 // How long each fade should last. | 45 // How long each fade should last. |
| 46 const NSTimeInterval kShowFadeInDurationSeconds = 0.120; | 46 const NSTimeInterval kShowFadeInDurationSeconds = 0.120; |
| 47 const NSTimeInterval kHideFadeOutDurationSeconds = 0.200; | 47 const NSTimeInterval kHideFadeOutDurationSeconds = 0.200; |
| 48 | 48 |
| 49 // The minimum representable time interval. This can be used as the value | 49 // The minimum representable time interval. This can be used as the value |
| 50 // passed to +[NSAnimationContext setDuration:] to stop an in-progress | 50 // passed to +[NSAnimationContext setDuration:] to stop an in-progress |
| 51 // animation as quickly as possible. | 51 // animation as quickly as possible. |
| 52 const NSTimeInterval kMinimumTimeInterval = | 52 const NSTimeInterval kMinimumTimeInterval = |
| 53 std::numeric_limits<NSTimeInterval>::min(); | 53 std::numeric_limits<NSTimeInterval>::min(); |
| 54 | 54 |
| 55 // How quickly the status bubble should expand, in seconds. |
| 56 const CGFloat kExpansionDuration = 0.125; |
| 57 |
| 55 } // namespace | 58 } // namespace |
| 56 | 59 |
| 57 @interface StatusBubbleAnimationDelegate : NSObject { | 60 @interface StatusBubbleAnimationDelegate : NSObject { |
| 58 @private | 61 @private |
| 59 StatusBubbleMac* statusBubble_; // weak; owns us indirectly | 62 StatusBubbleMac* statusBubble_; // weak; owns us indirectly |
| 60 } | 63 } |
| 61 | 64 |
| 62 - (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble; | 65 - (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble; |
| 63 | 66 |
| 64 // Invalidates this object so that no further calls will be made to | 67 // Invalidates this object so that no further calls will be made to |
| (...skipping 21 matching lines...) Expand all Loading... |
| 86 | 89 |
| 87 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { | 90 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { |
| 88 if (statusBubble_) | 91 if (statusBubble_) |
| 89 statusBubble_->AnimationDidStop(animation, finished ? true : false); | 92 statusBubble_->AnimationDidStop(animation, finished ? true : false); |
| 90 } | 93 } |
| 91 | 94 |
| 92 @end | 95 @end |
| 93 | 96 |
| 94 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate) | 97 StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate) |
| 95 : ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)), | 98 : ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)), |
| 99 ALLOW_THIS_IN_INITIALIZER_LIST(expand_timer_factory_(this)), |
| 96 parent_(parent), | 100 parent_(parent), |
| 97 delegate_(delegate), | 101 delegate_(delegate), |
| 98 window_(nil), | 102 window_(nil), |
| 99 status_text_(nil), | 103 status_text_(nil), |
| 100 url_text_(nil), | 104 url_text_(nil), |
| 101 state_(kBubbleHidden), | 105 state_(kBubbleHidden), |
| 102 immediate_(false) { | 106 immediate_(false), |
| 107 is_expanded_(false) { |
| 103 } | 108 } |
| 104 | 109 |
| 105 StatusBubbleMac::~StatusBubbleMac() { | 110 StatusBubbleMac::~StatusBubbleMac() { |
| 106 Hide(); | 111 Hide(); |
| 107 | 112 |
| 108 if (window_) { | 113 if (window_) { |
| 109 [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate]; | 114 [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate]; |
| 110 Detach(); | 115 Detach(); |
| 111 [window_ release]; | 116 [window_ release]; |
| 112 window_ = nil; | 117 window_ = nil; |
| 113 } | 118 } |
| 114 } | 119 } |
| 115 | 120 |
| 116 void StatusBubbleMac::SetStatus(const string16& status) { | 121 void StatusBubbleMac::SetStatus(const string16& status) { |
| 117 Create(); | 122 Create(); |
| 118 | 123 |
| 119 SetText(status, false); | 124 SetText(status, false); |
| 120 } | 125 } |
| 121 | 126 |
| 122 void StatusBubbleMac::SetURL(const GURL& url, const string16& languages) { | 127 void StatusBubbleMac::SetURL(const GURL& url, const string16& languages) { |
| 128 url_ = url; |
| 129 languages_ = languages; |
| 130 |
| 123 Create(); | 131 Create(); |
| 124 | 132 |
| 125 NSRect frame = [window_ frame]; | 133 NSRect frame = [window_ frame]; |
| 126 int text_width = static_cast<int>(frame.size.width - | 134 |
| 135 // Reset frame size when bubble is hidden. |
| 136 if (state_ == kBubbleHidden) { |
| 137 is_expanded_ = false; |
| 138 frame.size.width = NSWidth(CalculateWindowFrame(/*expand=*/false)); |
| 139 [window_ setFrame:frame display:NO]; |
| 140 } |
| 141 |
| 142 int text_width = static_cast<int>(NSWidth(frame) - |
| 127 kBubbleViewTextPositionX - | 143 kBubbleViewTextPositionX - |
| 128 kTextPadding); | 144 kTextPadding); |
| 145 |
| 146 // Scale from view to window coordinates before eliding URL string. |
| 147 NSSize scaled_width = NSMakeSize(text_width, 0); |
| 148 scaled_width = [[parent_ contentView] convertSize:scaled_width fromView:nil]; |
| 149 text_width = static_cast<int>(scaled_width.width); |
| 129 NSFont* font = [[window_ contentView] font]; | 150 NSFont* font = [[window_ contentView] font]; |
| 130 gfx::Font font_chr(base::SysNSStringToWide([font fontName]), | 151 gfx::Font font_chr(base::SysNSStringToWide([font fontName]), |
| 131 [font pointSize]); | 152 [font pointSize]); |
| 132 | 153 |
| 154 string16 original_url_text = net::FormatUrl(url, UTF16ToUTF8(languages)); |
| 133 string16 status = WideToUTF16(gfx::ElideUrl(url, font_chr, text_width, | 155 string16 status = WideToUTF16(gfx::ElideUrl(url, font_chr, text_width, |
| 134 UTF16ToWideHack(languages))); | 156 UTF16ToWideHack(languages))); |
| 135 | 157 |
| 136 SetText(status, true); | 158 SetText(status, true); |
| 159 |
| 160 // In testing, don't use animation. When ExpandBubble is tested, it is |
| 161 // called explicitly. |
| 162 if (immediate_) |
| 163 return; |
| 164 else |
| 165 CancelExpandTimer(); |
| 166 |
| 167 // If the bubble has been expanded, the user has already hovered over a link |
| 168 // to trigger the expanded state. Don't wait to change the bubble in this |
| 169 // case -- immediately expand or contract to fit the URL. |
| 170 if (is_expanded_ && !url.is_empty()) { |
| 171 ExpandBubble(); |
| 172 } else if (original_url_text.length() > status.length()) { |
| 173 MessageLoop::current()->PostDelayedTask(FROM_HERE, |
| 174 expand_timer_factory_.NewRunnableMethod( |
| 175 &StatusBubbleMac::ExpandBubble), kExpandHoverDelay); |
| 176 } |
| 137 } | 177 } |
| 138 | 178 |
| 139 void StatusBubbleMac::SetText(const string16& text, bool is_url) { | 179 void StatusBubbleMac::SetText(const string16& text, bool is_url) { |
| 140 // The status bubble allows the status and URL strings to be set | 180 // The status bubble allows the status and URL strings to be set |
| 141 // independently. Whichever was set non-empty most recently will be the | 181 // independently. Whichever was set non-empty most recently will be the |
| 142 // value displayed. When both are empty, the status bubble hides. | 182 // value displayed. When both are empty, the status bubble hides. |
| 143 | 183 |
| 144 NSString* text_ns = base::SysUTF16ToNSString(text); | 184 NSString* text_ns = base::SysUTF16ToNSString(text); |
| 145 | 185 |
| 146 NSString** main; | 186 NSString** main; |
| (...skipping 25 matching lines...) Expand all Loading... |
| 172 show = false; | 212 show = false; |
| 173 | 213 |
| 174 if (show) | 214 if (show) |
| 175 StartShowing(); | 215 StartShowing(); |
| 176 else | 216 else |
| 177 StartHiding(); | 217 StartHiding(); |
| 178 } | 218 } |
| 179 | 219 |
| 180 void StatusBubbleMac::Hide() { | 220 void StatusBubbleMac::Hide() { |
| 181 CancelTimer(); | 221 CancelTimer(); |
| 222 CancelExpandTimer(); |
| 223 is_expanded_ = false; |
| 182 | 224 |
| 183 bool fade_out = false; | 225 bool fade_out = false; |
| 184 if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) { | 226 if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) { |
| 185 SetState(kBubbleHidingFadeOut); | 227 SetState(kBubbleHidingFadeOut); |
| 186 | 228 |
| 187 if (!immediate_) { | 229 if (!immediate_) { |
| 188 // An animation is in progress. Cancel it by starting a new animation. | 230 // An animation is in progress. Cancel it by starting a new animation. |
| 189 // Use kMinimumTimeInterval to set the opacity as rapidly as possible. | 231 // Use kMinimumTimeInterval to set the opacity as rapidly as possible. |
| 190 fade_out = true; | 232 fade_out = true; |
| 191 [NSAnimationContext beginGrouping]; | 233 [NSAnimationContext beginGrouping]; |
| 192 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval]; | 234 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval]; |
| 193 [[window_ animator] setAlphaValue:0.0]; | 235 [[window_ animator] setAlphaValue:0.0]; |
| 194 [NSAnimationContext endGrouping]; | 236 [NSAnimationContext endGrouping]; |
| 195 } | 237 } |
| 196 } | 238 } |
| 197 | 239 |
| 198 if (!fade_out) { | 240 if (!fade_out) { |
| 199 // No animation is in progress, so the opacity can be set directly. | 241 // No animation is in progress, so the opacity can be set directly. |
| 200 [window_ setAlphaValue:0.0]; | 242 [window_ setAlphaValue:0.0]; |
| 201 SetState(kBubbleHidden); | 243 SetState(kBubbleHidden); |
| 202 } | 244 } |
| 203 | 245 |
| 246 // Stop any width animation and reset the bubble size. |
| 247 if (!immediate_) { |
| 248 [NSAnimationContext beginGrouping]; |
| 249 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval]; |
| 250 [[window_ animator] setFrame:CalculateWindowFrame(/*expand=*/false) |
| 251 display:NO]; |
| 252 [NSAnimationContext endGrouping]; |
| 253 } else { |
| 254 [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO]; |
| 255 } |
| 256 |
| 204 [status_text_ release]; | 257 [status_text_ release]; |
| 205 status_text_ = nil; | 258 status_text_ = nil; |
| 206 [url_text_ release]; | 259 [url_text_ release]; |
| 207 url_text_ = nil; | 260 url_text_ = nil; |
| 208 } | 261 } |
| 209 | 262 |
| 210 void StatusBubbleMac::MouseMoved( | 263 void StatusBubbleMac::MouseMoved( |
| 211 const gfx::Point& location, bool left_content) { | 264 const gfx::Point& location, bool left_content) { |
| 212 if (left_content) | 265 if (left_content) |
| 213 return; | 266 return; |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 291 } | 344 } |
| 292 | 345 |
| 293 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) { | 346 void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) { |
| 294 } | 347 } |
| 295 | 348 |
| 296 void StatusBubbleMac::Create() { | 349 void StatusBubbleMac::Create() { |
| 297 if (window_) | 350 if (window_) |
| 298 return; | 351 return; |
| 299 | 352 |
| 300 // TODO(avi):fix this for RTL | 353 // TODO(avi):fix this for RTL |
| 301 NSRect window_rect = CalculateWindowFrame(); | 354 NSRect window_rect = CalculateWindowFrame(/*expand=*/false); |
| 302 // initWithContentRect has origin in screen coords and size in scaled window | 355 // initWithContentRect has origin in screen coords and size in scaled window |
| 303 // coordinates. | 356 // coordinates. |
| 304 window_rect.size = | 357 window_rect.size = |
| 305 [[parent_ contentView] convertSize:window_rect.size fromView:nil]; | 358 [[parent_ contentView] convertSize:window_rect.size fromView:nil]; |
| 306 window_ = [[NSWindow alloc] initWithContentRect:window_rect | 359 window_ = [[NSWindow alloc] initWithContentRect:window_rect |
| 307 styleMask:NSBorderlessWindowMask | 360 styleMask:NSBorderlessWindowMask |
| 308 backing:NSBackingStoreBuffered | 361 backing:NSBackingStoreBuffered |
| 309 defer:YES]; | 362 defer:YES]; |
| 310 [window_ setMovableByWindowBackground:NO]; | 363 [window_ setMovableByWindowBackground:NO]; |
| 311 [window_ setBackgroundColor:[NSColor clearColor]]; | 364 [window_ setBackgroundColor:[NSColor clearColor]]; |
| (...skipping 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 528 CancelTimer(); | 581 CancelTimer(); |
| 529 } | 582 } |
| 530 | 583 |
| 531 // If the state is kBubbleHidden, kBubbleHidingFadeOut, or | 584 // If the state is kBubbleHidden, kBubbleHidingFadeOut, or |
| 532 // kBubbleHidingTimer, leave everything alone. The timer is not reset as | 585 // kBubbleHidingTimer, leave everything alone. The timer is not reset as |
| 533 // with kBubbleShowingTimer in StartShowing() because a subsequent request | 586 // with kBubbleShowingTimer in StartShowing() because a subsequent request |
| 534 // to hide something while one is already in flight does not invalidate the | 587 // to hide something while one is already in flight does not invalidate the |
| 535 // earlier request. | 588 // earlier request. |
| 536 } | 589 } |
| 537 | 590 |
| 591 void StatusBubbleMac::CancelExpandTimer() { |
| 592 DCHECK([NSThread isMainThread]); |
| 593 expand_timer_factory_.RevokeAll(); |
| 594 } |
| 595 |
| 596 void StatusBubbleMac::ExpandBubble() { |
| 597 // Calculate the width available for expanded and standard bubbles. |
| 598 NSRect window_frame = CalculateWindowFrame(/*expand=*/true); |
| 599 CGFloat max_bubble_width = NSWidth(window_frame); |
| 600 CGFloat standard_bubble_width = |
| 601 NSWidth(CalculateWindowFrame(/*expand=*/false)); |
| 602 |
| 603 // Generate the URL string that fits in the expanded bubble. |
| 604 NSFont* font = [[window_ contentView] font]; |
| 605 gfx::Font font_chr(base::SysNSStringToWide([font fontName]), |
| 606 [font pointSize]); |
| 607 string16 expanded_url = WideToUTF16(gfx::ElideUrl(url_, font_chr, |
| 608 max_bubble_width, UTF16ToWide(languages_))); |
| 609 |
| 610 // Scale width from gfx::Font in view coordinates to window coordinates. |
| 611 int required_width_for_string = |
| 612 font_chr.GetStringWidth(UTF16ToWide(expanded_url)) + |
| 613 kTextPadding * 2 + kBubbleViewTextPositionX; |
| 614 NSSize scaled_width = NSMakeSize(required_width_for_string, 0); |
| 615 scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil]; |
| 616 required_width_for_string = scaled_width.width; |
| 617 |
| 618 // The expanded width must be at least as wide as the standard width, but no |
| 619 // wider than the maximum width for its parent frame. |
| 620 int expanded_bubble_width = |
| 621 std::max(standard_bubble_width, |
| 622 std::min(max_bubble_width, |
| 623 static_cast<CGFloat>(required_width_for_string))); |
| 624 |
| 625 SetText(expanded_url, true); |
| 626 is_expanded_ = true; |
| 627 window_frame.size.width = expanded_bubble_width; |
| 628 |
| 629 // In testing, don't do any animation. |
| 630 if (immediate_) { |
| 631 [window_ setFrame:window_frame display:YES]; |
| 632 return; |
| 633 } |
| 634 |
| 635 [NSAnimationContext beginGrouping]; |
| 636 [[NSAnimationContext currentContext] setDuration:kExpansionDuration]; |
| 637 [[window_ animator] setFrame:window_frame display:YES]; |
| 638 [NSAnimationContext endGrouping]; |
| 639 } |
| 640 |
| 538 void StatusBubbleMac::UpdateSizeAndPosition() { | 641 void StatusBubbleMac::UpdateSizeAndPosition() { |
| 539 if (!window_) | 642 if (!window_) |
| 540 return; | 643 return; |
| 541 | 644 |
| 542 [window_ setFrame:CalculateWindowFrame() display:YES]; | 645 [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:YES]; |
| 543 } | 646 } |
| 544 | 647 |
| 545 void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) { | 648 void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) { |
| 546 DCHECK(parent); | 649 DCHECK(parent); |
| 547 | 650 |
| 548 // If not attached, just update our member variable and position. | 651 // If not attached, just update our member variable and position. |
| 549 if (!is_attached()) { | 652 if (!is_attached()) { |
| 550 parent_ = parent; | 653 parent_ = parent; |
| 551 [[window_ contentView] setThemeProvider:parent]; | 654 [[window_ contentView] setThemeProvider:parent]; |
| 552 UpdateSizeAndPosition(); | 655 UpdateSizeAndPosition(); |
| 553 return; | 656 return; |
| 554 } | 657 } |
| 555 | 658 |
| 556 Detach(); | 659 Detach(); |
| 557 parent_ = parent; | 660 parent_ = parent; |
| 558 [[window_ contentView] setThemeProvider:parent]; | 661 [[window_ contentView] setThemeProvider:parent]; |
| 559 Attach(); | 662 Attach(); |
| 560 UpdateSizeAndPosition(); | 663 UpdateSizeAndPosition(); |
| 561 } | 664 } |
| 562 | 665 |
| 563 NSRect StatusBubbleMac::CalculateWindowFrame() { | 666 NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) { |
| 564 DCHECK(parent_); | 667 DCHECK(parent_); |
| 565 | 668 |
| 566 NSSize size = NSMakeSize(0, kWindowHeight); | 669 NSSize size = NSMakeSize(0, kWindowHeight); |
| 567 size = [[parent_ contentView] convertSize:size toView:nil]; | 670 size = [[parent_ contentView] convertSize:size toView:nil]; |
| 568 | 671 |
| 569 NSRect rect = [parent_ frame]; | 672 NSRect rect = [parent_ frame]; |
| 570 rect.size.height = size.height; | 673 rect.size.height = size.height; |
| 571 rect.size.width = static_cast<int>(kWindowWidthPercent * rect.size.width); | 674 |
| 675 const int kScrollbarWidth = 16; // see BrowserWindowController. |
| 676 if (expanded_width) { |
| 677 rect.size.width = NSWidth(rect) - kScrollbarWidth; |
| 678 } else { |
| 679 rect.size.width = static_cast<int>(kWindowWidthPercent * NSWidth(rect)); |
| 680 } |
| 681 |
| 572 return rect; | 682 return rect; |
| 573 } | 683 } |
| OLD | NEW |