diff options
Diffstat (limited to 'chrome/browser/ui/views/status_bubble_views.cc')
-rw-r--r-- | chrome/browser/ui/views/status_bubble_views.cc | 835 |
1 files changed, 835 insertions, 0 deletions
diff --git a/chrome/browser/ui/views/status_bubble_views.cc b/chrome/browser/ui/views/status_bubble_views.cc new file mode 100644 index 0000000..234b6f0 --- /dev/null +++ b/chrome/browser/ui/views/status_bubble_views.cc @@ -0,0 +1,835 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/views/status_bubble_views.h" + +#include <algorithm> + +#include "app/linear_animation.h" +#include "app/resource_bundle.h" +#include "app/text_elider.h" +#include "base/i18n/rtl.h" +#include "base/message_loop.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/themes/browser_theme_provider.h" +#include "gfx/canvas_skia.h" +#include "gfx/point.h" +#include "googleurl/src/gurl.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "net/base/net_util.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/core/SkRect.h" +#include "views/controls/label.h" +#include "views/controls/scrollbar/native_scroll_bar.h" +#include "views/screen.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" +#include "views/window/window.h" + +using views::Widget; + +// The alpha and color of the bubble's shadow. +static const SkColor kShadowColor = SkColorSetARGB(30, 0, 0, 0); + +// The roundedness of the edges of our bubble. +static const int kBubbleCornerRadius = 4; + +// How close the mouse can get to the infobubble before it starts sliding +// off-screen. +static const int kMousePadding = 20; + +// The horizontal offset of the text within the status bubble, not including the +// outer shadow ring. +static const int kTextPositionX = 3; + +// The minimum horizontal space between the (right) end of the text and the edge +// of the status bubble, not including the outer shadow ring. +static const int kTextHorizPadding = 1; + +// Delays before we start hiding or showing the bubble after we receive a +// show or hide request. +static const int kShowDelay = 80; +static const int kHideDelay = 250; + +// How long each fade should last for. +static const int kShowFadeDurationMS = 120; +static const int kHideFadeDurationMS = 200; +static const int kFramerate = 25; + +// How long each expansion step should take. +static const int kMinExpansionStepDurationMS = 20; +static const int kMaxExpansionStepDurationMS = 150; + +// View ----------------------------------------------------------------------- +// StatusView manages the display of the bubble, applying text changes and +// fading in or out the bubble as required. +class StatusBubbleViews::StatusView : public views::Label, + public LinearAnimation, + public AnimationDelegate { + public: + StatusView(StatusBubble* status_bubble, views::Widget* popup, + ThemeProvider* theme_provider) + : ALLOW_THIS_IN_INITIALIZER_LIST(LinearAnimation(kFramerate, this)), + stage_(BUBBLE_HIDDEN), + style_(STYLE_STANDARD), + ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)), + status_bubble_(status_bubble), + popup_(popup), + opacity_start_(0), + opacity_end_(0), + theme_provider_(theme_provider) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + gfx::Font font(rb.GetFont(ResourceBundle::BaseFont)); + SetFont(font); + } + + virtual ~StatusView() { + Stop(); + CancelTimer(); + } + + // The bubble can be in one of many stages: + enum BubbleStage { + BUBBLE_HIDDEN, // Entirely BUBBLE_HIDDEN. + BUBBLE_HIDING_FADE, // In a fade-out transition. + BUBBLE_HIDING_TIMER, // Waiting before a fade-out. + BUBBLE_SHOWING_TIMER, // Waiting before a fade-in. + BUBBLE_SHOWING_FADE, // In a fade-in transition. + BUBBLE_SHOWN // Fully visible. + }; + + enum BubbleStyle { + STYLE_BOTTOM, + STYLE_FLOATING, + STYLE_STANDARD, + STYLE_STANDARD_RIGHT + }; + + // Set the bubble text to a certain value, hides the bubble if text is + // an empty string. Trigger animation sequence to display if + // |should_animate_open|. + void SetText(const string16& text, bool should_animate_open); + + BubbleStage GetState() const { return stage_; } + + void SetStyle(BubbleStyle style); + + BubbleStyle GetStyle() const { return style_; } + + // Show the bubble instantly. + void Show(); + + // Hide the bubble instantly. + void Hide(); + + // Resets any timers we have. Typically called when the user moves a + // mouse. + void ResetTimer(); + + private: + class InitialTimer; + + // Manage the timers that control the delay before a fade begins or ends. + void StartTimer(int time); + void OnTimer(); + void CancelTimer(); + void RestartTimer(int delay); + + // Manage the fades and starting and stopping the animations correctly. + void StartFade(double start, double end, int duration); + void StartHiding(); + void StartShowing(); + + // Animation functions. + double GetCurrentOpacity(); + void SetOpacity(double opacity); + void AnimateToState(double state); + void AnimationEnded(const Animation* animation); + + virtual void Paint(gfx::Canvas* canvas); + + BubbleStage stage_; + BubbleStyle style_; + + ScopedRunnableMethodFactory<StatusBubbleViews::StatusView> timer_factory_; + + // Manager, owns us. + StatusBubble* status_bubble_; + + // Handle to the widget that contains us. + views::Widget* popup_; + + // The currently-displayed text. + string16 text_; + + // Start and end opacities for the current transition - note that as a + // fade-in can easily turn into a fade out, opacity_start_ is sometimes + // a value between 0 and 1. + double opacity_start_; + double opacity_end_; + + // Holds the theme provider of the frame that created us. + ThemeProvider* theme_provider_; +}; + +void StatusBubbleViews::StatusView::SetText(const string16& text, + bool should_animate_open) { + if (text.empty()) { + // The string was empty. + StartHiding(); + } else { + // We want to show the string. + text_ = text; + if (should_animate_open) + StartShowing(); + } + + SchedulePaint(); +} + +void StatusBubbleViews::StatusView::Show() { + Stop(); + CancelTimer(); + SetOpacity(1.0); + popup_->Show(); + stage_ = BUBBLE_SHOWN; + PaintNow(); +} + +void StatusBubbleViews::StatusView::Hide() { + Stop(); + CancelTimer(); + SetOpacity(0.0); + text_.clear(); + popup_->Hide(); + stage_ = BUBBLE_HIDDEN; +} + +void StatusBubbleViews::StatusView::StartTimer(int time) { + if (!timer_factory_.empty()) + timer_factory_.RevokeAll(); + + MessageLoop::current()->PostDelayedTask(FROM_HERE, + timer_factory_.NewRunnableMethod(&StatusBubbleViews::StatusView::OnTimer), + time); +} + +void StatusBubbleViews::StatusView::OnTimer() { + if (stage_ == BUBBLE_HIDING_TIMER) { + stage_ = BUBBLE_HIDING_FADE; + StartFade(1.0, 0.0, kHideFadeDurationMS); + } else if (stage_ == BUBBLE_SHOWING_TIMER) { + stage_ = BUBBLE_SHOWING_FADE; + StartFade(0.0, 1.0, kShowFadeDurationMS); + } +} + +void StatusBubbleViews::StatusView::CancelTimer() { + if (!timer_factory_.empty()) + timer_factory_.RevokeAll(); +} + +void StatusBubbleViews::StatusView::RestartTimer(int delay) { + CancelTimer(); + StartTimer(delay); +} + +void StatusBubbleViews::StatusView::ResetTimer() { + if (stage_ == BUBBLE_SHOWING_TIMER) { + // We hadn't yet begun showing anything when we received a new request + // for something to show, so we start from scratch. + RestartTimer(kShowDelay); + } +} + +void StatusBubbleViews::StatusView::StartFade(double start, + double end, + int duration) { + opacity_start_ = start; + opacity_end_ = end; + + // This will also reset the currently-occurring animation. + SetDuration(duration); + Start(); +} + +void StatusBubbleViews::StatusView::StartHiding() { + if (stage_ == BUBBLE_SHOWN) { + stage_ = BUBBLE_HIDING_TIMER; + StartTimer(kHideDelay); + } else if (stage_ == BUBBLE_SHOWING_TIMER) { + stage_ = BUBBLE_HIDDEN; + CancelTimer(); + } else if (stage_ == BUBBLE_SHOWING_FADE) { + stage_ = BUBBLE_HIDING_FADE; + // Figure out where we are in the current fade. + double current_opacity = GetCurrentOpacity(); + + // Start a fade in the opposite direction. + StartFade(current_opacity, 0.0, + static_cast<int>(kHideFadeDurationMS * current_opacity)); + } +} + +void StatusBubbleViews::StatusView::StartShowing() { + if (stage_ == BUBBLE_HIDDEN) { + popup_->Show(); + stage_ = BUBBLE_SHOWING_TIMER; + StartTimer(kShowDelay); + } else if (stage_ == BUBBLE_HIDING_TIMER) { + stage_ = BUBBLE_SHOWN; + CancelTimer(); + } else if (stage_ == BUBBLE_HIDING_FADE) { + // We're partway through a fade. + stage_ = BUBBLE_SHOWING_FADE; + + // Figure out where we are in the current fade. + double current_opacity = GetCurrentOpacity(); + + // Start a fade in the opposite direction. + StartFade(current_opacity, 1.0, + static_cast<int>(kShowFadeDurationMS * current_opacity)); + } else if (stage_ == BUBBLE_SHOWING_TIMER) { + // We hadn't yet begun showing anything when we received a new request + // for something to show, so we start from scratch. + ResetTimer(); + } +} + +// Animation functions. +double StatusBubbleViews::StatusView::GetCurrentOpacity() { + return opacity_start_ + (opacity_end_ - opacity_start_) * + LinearAnimation::GetCurrentValue(); +} + +void StatusBubbleViews::StatusView::SetOpacity(double opacity) { + popup_->SetOpacity(static_cast<unsigned char>(opacity * 255)); + SchedulePaint(); +} + +void StatusBubbleViews::StatusView::AnimateToState(double state) { + SetOpacity(GetCurrentOpacity()); +} + +void StatusBubbleViews::StatusView::AnimationEnded( + const Animation* animation) { + SetOpacity(opacity_end_); + + if (stage_ == BUBBLE_HIDING_FADE) { + stage_ = BUBBLE_HIDDEN; + popup_->Hide(); + } else if (stage_ == BUBBLE_SHOWING_FADE) { + stage_ = BUBBLE_SHOWN; + } +} + +void StatusBubbleViews::StatusView::SetStyle(BubbleStyle style) { + if (style_ != style) { + style_ = style; + SchedulePaint(); + } +} + +void StatusBubbleViews::StatusView::Paint(gfx::Canvas* canvas) { + SkPaint paint; + paint.setStyle(SkPaint::kFill_Style); + paint.setFlags(SkPaint::kAntiAlias_Flag); + SkColor toolbar_color = + theme_provider_->GetColor(BrowserThemeProvider::COLOR_TOOLBAR); + paint.setColor(toolbar_color); + + gfx::Rect popup_bounds; + popup_->GetBounds(&popup_bounds, true); + + // Figure out how to round the bubble's four corners. + SkScalar rad[8]; + + // Top Edges - if the bubble is in its bottom position (sticking downwards), + // then we square the top edges. Otherwise, we square the edges based on the + // position of the bubble within the window (the bubble is positioned in the + // southeast corner in RTL and in the southwest corner in LTR). + if (style_ == STYLE_BOTTOM) { + // Top Left corner. + rad[0] = 0; + rad[1] = 0; + + // Top Right corner. + rad[2] = 0; + rad[3] = 0; + } else { + if (base::i18n::IsRTL() != (style_ == STYLE_STANDARD_RIGHT)) { + // The text is RtL or the bubble is on the right side (but not both). + + // Top Left corner. + rad[0] = SkIntToScalar(kBubbleCornerRadius); + rad[1] = SkIntToScalar(kBubbleCornerRadius); + + // Top Right corner. + rad[2] = 0; + rad[3] = 0; + } else { + // Top Left corner. + rad[0] = 0; + rad[1] = 0; + + // Top Right corner. + rad[2] = SkIntToScalar(kBubbleCornerRadius); + rad[3] = SkIntToScalar(kBubbleCornerRadius); + } + } + + // Bottom edges - square these off if the bubble is in its standard position + // (sticking upward). + if (style_ == STYLE_STANDARD || style_ == STYLE_STANDARD_RIGHT) { + // Bottom Right Corner. + rad[4] = 0; + rad[5] = 0; + + // Bottom Left Corner. + rad[6] = 0; + rad[7] = 0; + } else { + // Bottom Right Corner. + rad[4] = SkIntToScalar(kBubbleCornerRadius); + rad[5] = SkIntToScalar(kBubbleCornerRadius); + + // Bottom Left Corner. + rad[6] = SkIntToScalar(kBubbleCornerRadius); + rad[7] = SkIntToScalar(kBubbleCornerRadius); + } + + // Draw the bubble's shadow. + int width = popup_bounds.width(); + int height = popup_bounds.height(); + SkRect rect; + rect.set(0, 0, + SkIntToScalar(width), + SkIntToScalar(height)); + SkPath shadow_path; + shadow_path.addRoundRect(rect, rad, SkPath::kCW_Direction); + SkPaint shadow_paint; + shadow_paint.setFlags(SkPaint::kAntiAlias_Flag); + shadow_paint.setColor(kShadowColor); + canvas->AsCanvasSkia()->drawPath(shadow_path, shadow_paint); + + // Draw the bubble. + rect.set(SkIntToScalar(kShadowThickness), + SkIntToScalar(kShadowThickness), + SkIntToScalar(width - kShadowThickness), + SkIntToScalar(height - kShadowThickness)); + SkPath path; + path.addRoundRect(rect, rad, SkPath::kCW_Direction); + canvas->AsCanvasSkia()->drawPath(path, paint); + + // Draw highlight text and then the text body. In order to make sure the text + // is aligned to the right on RTL UIs, we mirror the text bounds if the + // locale is RTL. + int text_width = std::min( + views::Label::font().GetStringWidth(UTF16ToWide(text_)), + width - (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding); + int text_height = height - (kShadowThickness * 2); + gfx::Rect body_bounds(kShadowThickness + kTextPositionX, + kShadowThickness, + std::max(0, text_width), + std::max(0, text_height)); + body_bounds.set_x(MirroredLeftPointForRect(body_bounds)); + SkColor text_color = + theme_provider_->GetColor(BrowserThemeProvider::COLOR_TAB_TEXT); + + // DrawStringInt doesn't handle alpha, so we'll do the blending ourselves. + text_color = SkColorSetARGB( + SkColorGetA(text_color), + (SkColorGetR(text_color) + SkColorGetR(toolbar_color)) / 2, + (SkColorGetG(text_color) + SkColorGetR(toolbar_color)) / 2, + (SkColorGetB(text_color) + SkColorGetR(toolbar_color)) / 2); + canvas->DrawStringInt(UTF16ToWide(text_), + views::Label::font(), + text_color, + body_bounds.x(), + body_bounds.y(), + body_bounds.width(), + body_bounds.height()); +} + +// StatusViewExpander --------------------------------------------------------- +// Manages the expansion and contraction of the status bubble as it accommodates +// URLs too long to fit in the standard bubble. Changes are passed through the +// StatusView to paint. +class StatusBubbleViews::StatusViewExpander : public LinearAnimation, + public AnimationDelegate { + public: + StatusViewExpander(StatusBubbleViews* status_bubble, + StatusView* status_view) + : ALLOW_THIS_IN_INITIALIZER_LIST(LinearAnimation(kFramerate, this)), + status_bubble_(status_bubble), + status_view_(status_view), + expansion_start_(0), + expansion_end_(0) { + } + + // Manage the expansion of the bubble. + void StartExpansion(string16 expanded_text, int current_width, + int expansion_end); + + // Set width of fully expanded bubble. + void SetExpandedWidth(int expanded_width); + + private: + // Animation functions. + int GetCurrentBubbleWidth(); + void SetBubbleWidth(int width); + void AnimateToState(double state); + void AnimationEnded(const Animation* animation); + + // Manager that owns us. + StatusBubbleViews* status_bubble_; + + // Change the bounds and text of this view. + StatusView* status_view_; + + // Text elided (if needed) to fit maximum status bar width. + string16 expanded_text_; + + // Widths at expansion start and end. + int expansion_start_; + int expansion_end_; +}; + +void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) { + SetBubbleWidth(GetCurrentBubbleWidth()); +} + +void StatusBubbleViews::StatusViewExpander::AnimationEnded( + const Animation* animation) { + SetBubbleWidth(expansion_end_); + status_view_->SetText(expanded_text_, false); +} + +void StatusBubbleViews::StatusViewExpander::StartExpansion( + string16 expanded_text, int expansion_start, + int expansion_end) { + expanded_text_ = expanded_text; + expansion_start_ = expansion_start; + expansion_end_ = expansion_end; + int min_duration = std::max(kMinExpansionStepDurationMS, + static_cast<int>(kMaxExpansionStepDurationMS * + (expansion_end - expansion_start) / 100.0)); + SetDuration(std::min(kMaxExpansionStepDurationMS, min_duration)); + Start(); +} + +int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() { + return static_cast<int>(expansion_start_ + + (expansion_end_ - expansion_start_) * LinearAnimation::GetCurrentValue()); +} + +void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) { + status_bubble_->SetBubbleWidth(width); + status_view_->SchedulePaint(); +} + +// StatusBubble --------------------------------------------------------------- + +const int StatusBubbleViews::kShadowThickness = 1; + +StatusBubbleViews::StatusBubbleViews(views::View* base_view) + : offset_(0), + popup_(NULL), + opacity_(0), + base_view_(base_view), + view_(NULL), + download_shelf_is_visible_(false), + is_expanded_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(expand_timer_factory_(this)) { + expand_view_.reset(); +} + +StatusBubbleViews::~StatusBubbleViews() { + CancelExpandTimer(); + if (popup_.get()) + popup_->CloseNow(); +} + +void StatusBubbleViews::Init() { + if (!popup_.get()) { + popup_.reset(Widget::CreatePopupWidget(Widget::Transparent, + Widget::NotAcceptEvents, + Widget::NotDeleteOnDestroy, + Widget::MirrorOriginInRTL)); + views::Widget* frame = base_view_->GetWidget(); + if (!view_) + view_ = new StatusView(this, popup_.get(), frame->GetThemeProvider()); + if (!expand_view_.get()) + expand_view_.reset(new StatusViewExpander(this, view_)); + popup_->SetOpacity(0x00); + popup_->Init(frame->GetNativeView(), gfx::Rect()); + popup_->SetContentsView(view_); + Reposition(); + popup_->Show(); + } +} + +void StatusBubbleViews::Reposition() { + if (popup_.get()) { + gfx::Point top_left; + views::View::ConvertPointToScreen(base_view_, &top_left); + + popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), + top_left.y() + position_.y(), + size_.width(), size_.height())); + } +} + +gfx::Size StatusBubbleViews::GetPreferredSize() { + return gfx::Size(0, ResourceBundle::GetSharedInstance().GetFont( + ResourceBundle::BaseFont).GetHeight() + kTotalVerticalPadding); +} + +void StatusBubbleViews::SetBounds(int x, int y, int w, int h) { + original_position_.SetPoint(x, y); + position_.SetPoint(base_view_->MirroredXWithWidthInsideView(x, w), y); + size_.SetSize(w, h); + Reposition(); +} + +void StatusBubbleViews::SetStatus(const string16& status_text) { + if (size_.IsEmpty()) + return; // We have no bounds, don't attempt to show the popup. + + if (status_text_ == status_text && !status_text.empty()) + return; + + if (!IsFrameVisible()) + return; // Don't show anything if the parent isn't visible. + + Init(); + status_text_ = status_text; + if (!status_text_.empty()) { + view_->SetText(status_text, true); + view_->Show(); + } else if (!url_text_.empty()) { + view_->SetText(url_text_, true); + } else { + view_->SetText(string16(), true); + } +} + +void StatusBubbleViews::SetURL(const GURL& url, const string16& languages) { + languages_ = languages; + url_ = url; + if (size_.IsEmpty()) + return; // We have no bounds, don't attempt to show the popup. + + Init(); + + // If we want to clear a displayed URL but there is a status still to + // display, display that status instead. + if (url.is_empty() && !status_text_.empty()) { + url_text_ = string16(); + if (IsFrameVisible()) + view_->SetText(status_text_, true); + return; + } + + // Reset expansion state only when bubble is completely hidden. + if (view_->GetState() == StatusView::BUBBLE_HIDDEN) { + is_expanded_ = false; + SetBubbleWidth(GetStandardStatusBubbleWidth()); + } + + // Set Elided Text corresponding to the GURL object. + gfx::Rect popup_bounds; + popup_->GetBounds(&popup_bounds, true); + int text_width = static_cast<int>(popup_bounds.width() - + (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1); + url_text_ = gfx::ElideUrl(url, view_->Label::font(), + text_width, UTF16ToWideHack(languages)); + + std::wstring original_url_text = + UTF16ToWideHack(net::FormatUrl(url, UTF16ToUTF8(languages))); + + // An URL is always treated as a left-to-right string. On right-to-left UIs + // we need to explicitly mark the URL as LTR to make sure it is displayed + // correctly. + url_text_ = base::i18n::GetDisplayStringInLTRDirectionality(url_text_); + + if (IsFrameVisible()) { + view_->SetText(url_text_, true); + + CancelExpandTimer(); + + // If bubble is already in expanded state, shift to adjust to new text + // size (shrinking or expanding). Otherwise delay. + if (is_expanded_ && !url.is_empty()) + ExpandBubble(); + else if (original_url_text.length() > url_text_.length()) + MessageLoop::current()->PostDelayedTask(FROM_HERE, + expand_timer_factory_.NewRunnableMethod( + &StatusBubbleViews::ExpandBubble), kExpandHoverDelay); + } +} + +void StatusBubbleViews::Hide() { + status_text_ = string16(); + url_text_ = string16(); + if (view_) + view_->Hide(); +} + +void StatusBubbleViews::MouseMoved(const gfx::Point& location, + bool left_content) { + if (left_content) + return; + + if (view_) { + view_->ResetTimer(); + + if (view_->GetState() != StatusView::BUBBLE_HIDDEN && + view_->GetState() != StatusView::BUBBLE_HIDING_FADE && + view_->GetState() != StatusView::BUBBLE_HIDING_TIMER) { + AvoidMouse(location); + } + } +} + +void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) { + download_shelf_is_visible_ = visible; +} + +void StatusBubbleViews::AvoidMouse(const gfx::Point& location) { + // Get the position of the frame. + gfx::Point top_left; + views::View::ConvertPointToScreen(base_view_, &top_left); + // Border included. + int window_width = base_view_->GetLocalBounds(true).width(); + + // Get the cursor position relative to the popup. + gfx::Point relative_location = location; + if (base::i18n::IsRTL()) { + int top_right_x = top_left.x() + window_width; + relative_location.set_x(top_right_x - relative_location.x()); + } else { + relative_location.set_x( + relative_location.x() - (top_left.x() + position_.x())); + } + relative_location.set_y( + relative_location.y() - (top_left.y() + position_.y())); + + // If the mouse is in a position where we think it would move the + // status bubble, figure out where and how the bubble should be moved. + if (relative_location.y() > -kMousePadding && + relative_location.x() < size_.width() + kMousePadding) { + int offset = kMousePadding + relative_location.y(); + + // Make the movement non-linear. + offset = offset * offset / kMousePadding; + + // When the mouse is entering from the right, we want the offset to be + // scaled by how horizontally far away the cursor is from the bubble. + if (relative_location.x() > size_.width()) { + offset = static_cast<int>(static_cast<float>(offset) * ( + static_cast<float>(kMousePadding - + (relative_location.x() - size_.width())) / + static_cast<float>(kMousePadding))); + } + + // Cap the offset and change the visual presentation of the bubble + // depending on where it ends up (so that rounded corners square off + // and mate to the edges of the tab content). + if (offset >= size_.height() - kShadowThickness * 2) { + offset = size_.height() - kShadowThickness * 2; + view_->SetStyle(StatusView::STYLE_BOTTOM); + } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) { + view_->SetStyle(StatusView::STYLE_FLOATING); + } else { + view_->SetStyle(StatusView::STYLE_STANDARD); + } + + // Check if the bubble sticks out from the monitor or will obscure + // download shelf. + gfx::NativeView widget = base_view_->GetWidget()->GetNativeView(); + gfx::Rect monitor_rect = + views::Screen::GetMonitorWorkAreaNearestWindow(widget); + const int bubble_bottom_y = top_left.y() + position_.y() + size_.height(); + + if (bubble_bottom_y + offset > monitor_rect.height() || + (download_shelf_is_visible_ && + (view_->GetStyle() == StatusView::STYLE_FLOATING || + view_->GetStyle() == StatusView::STYLE_BOTTOM))) { + // The offset is still too large. Move the bubble to the right and reset + // Y offset_ to zero. + view_->SetStyle(StatusView::STYLE_STANDARD_RIGHT); + offset_ = 0; + + // Subtract border width + bubble width. + int right_position_x = window_width - (position_.x() + size_.width()); + popup_->SetBounds(gfx::Rect(top_left.x() + right_position_x, + top_left.y() + position_.y(), + size_.width(), size_.height())); + } else { + offset_ = offset; + popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), + top_left.y() + position_.y() + offset_, + size_.width(), size_.height())); + } + } else if (offset_ != 0 || + view_->GetStyle() == StatusView::STYLE_STANDARD_RIGHT) { + offset_ = 0; + view_->SetStyle(StatusView::STYLE_STANDARD); + popup_->SetBounds(gfx::Rect(top_left.x() + position_.x(), + top_left.y() + position_.y(), + size_.width(), size_.height())); + } +} + +bool StatusBubbleViews::IsFrameVisible() { + views::Widget* frame = base_view_->GetWidget(); + if (!frame->IsVisible()) + return false; + + views::Window* window = frame->GetWindow(); + return !window || !window->IsMinimized(); +} + +void StatusBubbleViews::ExpandBubble() { + // Elide URL to maximum possible size, then check actual length (it may + // still be too long to fit) before expanding bubble. + gfx::Rect popup_bounds; + popup_->GetBounds(&popup_bounds, true); + int max_status_bubble_width = GetMaxStatusBubbleWidth(); + url_text_ = gfx::ElideUrl(url_, view_->Label::font(), + max_status_bubble_width, UTF16ToWideHack(languages_)); + int expanded_bubble_width =std::max(GetStandardStatusBubbleWidth(), + std::min(view_->Label::font().GetStringWidth(UTF16ToWide(url_text_)) + + (kShadowThickness * 2) + kTextPositionX + + kTextHorizPadding + 1, + max_status_bubble_width)); + is_expanded_ = true; + expand_view_->StartExpansion(url_text_, popup_bounds.width(), + expanded_bubble_width); +} + +int StatusBubbleViews::GetStandardStatusBubbleWidth() { + return base_view_->bounds().width() / 3; +} + +int StatusBubbleViews::GetMaxStatusBubbleWidth() { + return static_cast<int>(std::max(0, base_view_->bounds().width() - + (kShadowThickness * 2) - kTextPositionX - kTextHorizPadding - 1 - + views::NativeScrollBar::GetVerticalScrollBarWidth())); +} + +void StatusBubbleViews::SetBubbleWidth(int width) { + size_.set_width(width); + SetBounds(original_position_.x(), original_position_.y(), + size_.width(), size_.height()); +} + +void StatusBubbleViews::CancelExpandTimer() { + if (!expand_timer_factory_.empty()) + expand_timer_factory_.RevokeAll(); +} |