| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/status_bubble_views.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/i18n/rtl.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/timer/timer.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "cc/paint/paint_flags.h" |
| #include "chrome/browser/ui/color/chrome_color_id.h" |
| #include "chrome/browser/ui/views/chrome_widget_sublevel.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "components/url_formatter/url_formatter.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "third_party/skia/include/pathops/SkPathOps.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/animation/linear_animation.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/rrect_f.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| #include "ui/gfx/scoped_canvas.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/gfx/text_utils.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/views/animation/animation_delegate_views.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/scrollbar/scroll_bar_views.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/style/typography.h" |
| #include "ui/views/views_features.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/widget.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "ash/public/cpp/window_properties.h" |
| #include "ui/aura/window.h" |
| #endif |
| |
| namespace { |
| |
| // The roundedness of the edges of our bubble. |
| constexpr int kBubbleCornerRadius = 4; |
| |
| // How close the mouse can get to the infobubble before it starts sliding |
| // off-screen. |
| constexpr int kMousePadding = 20; |
| |
| // The minimum horizontal space between the edges of the text and the edges of |
| // the status bubble, not including the outer shadow ring. |
| constexpr int kTextHorizPadding = 5; |
| |
| // Delays before we start hiding or showing the bubble after we receive a |
| // show or hide request. |
| constexpr auto kShowDelay = base::Milliseconds(80); |
| constexpr auto kHideDelay = base::Milliseconds(250); |
| |
| // How long each fade should last for. |
| constexpr auto kShowFadeDuration = base::Milliseconds(120); |
| constexpr auto kHideFadeDuration = base::Milliseconds(200); |
| constexpr int kFramerate = 25; |
| |
| // How long each expansion step should take. |
| constexpr auto kMinExpansionStepDuration = base::Milliseconds(20); |
| constexpr auto kMaxExpansionStepDuration = base::Milliseconds(150); |
| |
| // How long to delay before destroying an unused status bubble widget. |
| constexpr auto kDestroyPopupDelay = base::Seconds(10); |
| |
| const gfx::FontList& GetFont() { |
| return views::style::GetFont(views::style::CONTEXT_LABEL, |
| views::style::STYLE_PRIMARY); |
| } |
| |
| } // namespace |
| |
| // StatusBubbleViews::StatusViewAnimation -------------------------------------- |
| class StatusBubbleViews::StatusViewAnimation |
| : public gfx::LinearAnimation, |
| public views::AnimationDelegateViews { |
| public: |
| StatusViewAnimation(StatusView* status_view, |
| float opacity_start, |
| float opacity_end); |
| StatusViewAnimation(const StatusViewAnimation&) = delete; |
| StatusViewAnimation& operator=(const StatusViewAnimation&) = delete; |
| ~StatusViewAnimation() override; |
| |
| float GetCurrentOpacity(); |
| |
| private: |
| // gfx::LinearAnimation: |
| void AnimateToState(double state) override; |
| |
| // gfx::AnimationDelegate: |
| void AnimationEnded(const Animation* animation) override; |
| |
| raw_ptr<StatusView> status_view_; |
| |
| // 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. |
| float opacity_start_; |
| float opacity_end_; |
| }; |
| |
| // StatusBubbleViews::StatusView ----------------------------------------------- |
| // |
| // StatusView manages the display of the bubble, applying text changes and |
| // fading in or out the bubble as required. |
| class StatusBubbleViews::StatusView : public views::View { |
| public: |
| METADATA_HEADER(StatusView); |
| |
| // The bubble can be in one of many states: |
| enum class BubbleState { |
| kHidden, |
| kPreFadeIn, |
| kFadingIn, |
| kShown, |
| kPreFadeOut, |
| kFadingOut, |
| }; |
| |
| enum class BubbleStyle { |
| kBottom, |
| kFloating, |
| kStandard, |
| kStandardRight, |
| }; |
| |
| explicit StatusView(StatusBubbleViews* status_bubble); |
| StatusView(const StatusView&) = delete; |
| StatusView& operator=(const StatusView&) = delete; |
| ~StatusView() override; |
| |
| // views::View: |
| gfx::Insets GetInsets() const override; |
| |
| const std::u16string& GetText() const; |
| void SetText(const std::u16string& text); |
| |
| BubbleState GetState() const { return state_; } |
| |
| BubbleStyle GetStyle() const { return style_; } |
| void SetStyle(BubbleStyle style); |
| |
| // If |text| is empty, hides the bubble; otherwise, sets the bubble text to |
| // |text| and shows the bubble. |
| void AnimateForText(const std::u16string& text); |
| |
| // Show the bubble instantly. |
| void ShowInstantly(); |
| |
| // Hide the bubble instantly; this may destroy the bubble and view. |
| void HideInstantly(); |
| |
| // Resets any timers we have. Typically called when the user moves a mouse. |
| void ResetTimer(); |
| |
| // This call backs the StatusView in order to fade the bubble in and out. |
| void SetOpacity(float opacity); |
| |
| // Depending on the state of the bubble this will hide the popup or not. |
| void OnAnimationEnded(); |
| |
| gfx::Animation* animation() { return animation_.get(); } |
| |
| bool IsDestroyPopupTimerRunning() const; |
| |
| protected: |
| // views::View: |
| void OnThemeChanged() override; |
| |
| private: |
| class InitialTimer; |
| |
| // Manage the timers that control the delay before a fade begins or ends. |
| void StartTimer(base::TimeDelta time); |
| void OnTimer(); |
| void CancelTimer(); |
| void RestartTimer(base::TimeDelta delay); |
| |
| // Manage the fades and starting and stopping the animations correctly. |
| void StartFade(float start, float end, base::TimeDelta duration); |
| void StartHiding(); |
| void StartShowing(); |
| |
| // Set the text label's colors according to the theme. |
| void SetTextLabelColors(views::Label* label); |
| |
| // views::View: |
| void OnPaint(gfx::Canvas* canvas) override; |
| |
| BubbleState state_ = BubbleState::kHidden; |
| BubbleStyle style_ = BubbleStyle::kStandard; |
| |
| std::unique_ptr<StatusViewAnimation> animation_; |
| |
| // The status bubble that manages the popup widget and this view. |
| raw_ptr<StatusBubbleViews> status_bubble_; |
| |
| // The currently-displayed text. |
| views::Label* text_; |
| |
| // A timer used to delay destruction of the popup widget. This is meant to |
| // balance the performance tradeoffs of rapid creation/destruction and the |
| // memory savings of closing the widget when it's hidden and unused. |
| base::OneShotTimer destroy_popup_timer_; |
| |
| base::CallbackListSubscription paint_as_active_subscription_; |
| |
| base::WeakPtrFactory<StatusBubbleViews::StatusView> timer_factory_{this}; |
| }; |
| using StatusView = StatusBubbleViews::StatusView; |
| |
| StatusView::StatusView(StatusBubbleViews* status_bubble) |
| : status_bubble_(status_bubble) { |
| animation_ = std::make_unique<StatusViewAnimation>(this, 0, 0); |
| |
| SetUseDefaultFillLayout(true); |
| |
| std::unique_ptr<views::Label> text = std::make_unique<views::Label>(); |
| // Don't move this after AddChildView() since this function would trigger |
| // repaint which should not happen in the constructor. |
| SetTextLabelColors(text.get()); |
| text->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| text_ = AddChildView(std::move(text)); |
| |
| paint_as_active_subscription_ = |
| status_bubble_->base_view() |
| ->GetWidget() |
| ->RegisterPaintAsActiveChangedCallback(base::BindRepeating( |
| &StatusView::SetTextLabelColors, base::Unretained(this), text_)); |
| } |
| |
| StatusView::~StatusView() { |
| animation_->Stop(); |
| CancelTimer(); |
| } |
| |
| gfx::Insets StatusView::GetInsets() const { |
| return gfx::Insets::VH(kShadowThickness, |
| kShadowThickness + kTextHorizPadding); |
| } |
| |
| const std::u16string& StatusView::GetText() const { |
| return text_->GetText(); |
| } |
| |
| void StatusView::SetText(const std::u16string& text) { |
| if (text == GetText()) |
| return; |
| |
| text_->SetText(text); |
| OnPropertyChanged(&text_, views::kPropertyEffectsNone); |
| } |
| |
| void StatusView::AnimateForText(const std::u16string& text) { |
| if (text.empty()) { |
| StartHiding(); |
| } else { |
| SetText(text); |
| StartShowing(); |
| } |
| } |
| |
| void StatusView::SetStyle(BubbleStyle style) { |
| if (style_ == style) |
| return; |
| |
| style_ = style; |
| OnPropertyChanged(&style_, views::kPropertyEffectsPaint); |
| } |
| |
| void StatusView::ShowInstantly() { |
| animation_->Stop(); |
| CancelTimer(); |
| SetOpacity(1.0); |
| state_ = BubbleState::kShown; |
| GetWidget()->ShowInactive(); |
| destroy_popup_timer_.Stop(); |
| } |
| |
| void StatusView::HideInstantly() { |
| animation_->Stop(); |
| CancelTimer(); |
| SetOpacity(0.0); |
| SetText(std::u16string()); |
| state_ = BubbleState::kHidden; |
| // Don't orderOut: the window on macOS. Doing so for a child window requires |
| // it to be detached/reattached, which may trigger a space switch. Instead, |
| // just leave the window fully transparent and unclickable. |
| GetWidget()->Hide(); |
| destroy_popup_timer_.Stop(); |
| // This isn't done in the constructor as tests may change the task runner |
| // after the fact. |
| destroy_popup_timer_.SetTaskRunner(status_bubble_->task_runner_.get()); |
| destroy_popup_timer_.Start(FROM_HERE, kDestroyPopupDelay, |
| status_bubble_.get(), |
| &StatusBubbleViews::DestroyPopup); |
| } |
| |
| void StatusView::ResetTimer() { |
| if (state_ == BubbleState::kPreFadeIn) { |
| // 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 StatusView::SetOpacity(float opacity) { |
| GetWidget()->SetOpacity(opacity); |
| } |
| |
| void StatusView::OnAnimationEnded() { |
| if (state_ == BubbleState::kFadingIn) |
| state_ = BubbleState::kShown; |
| else if (state_ == BubbleState::kFadingOut) |
| HideInstantly(); // This view may be destroyed after calling HideInstantly. |
| } |
| |
| bool StatusView::IsDestroyPopupTimerRunning() const { |
| return destroy_popup_timer_.IsRunning(); |
| } |
| |
| void StatusView::OnThemeChanged() { |
| views::View::OnThemeChanged(); |
| SetTextLabelColors(text_); |
| } |
| |
| void StatusView::StartTimer(base::TimeDelta time) { |
| if (timer_factory_.HasWeakPtrs()) |
| timer_factory_.InvalidateWeakPtrs(); |
| |
| status_bubble_->task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&StatusView::OnTimer, timer_factory_.GetWeakPtr()), time); |
| } |
| |
| void StatusView::OnTimer() { |
| if (state_ == BubbleState::kPreFadeOut) { |
| state_ = BubbleState::kFadingOut; |
| StartFade(1.0f, 0.0f, kHideFadeDuration); |
| } else if (state_ == BubbleState::kPreFadeIn) { |
| state_ = BubbleState::kFadingIn; |
| StartFade(0.0f, 1.0f, kShowFadeDuration); |
| } |
| } |
| |
| void StatusView::CancelTimer() { |
| if (timer_factory_.HasWeakPtrs()) |
| timer_factory_.InvalidateWeakPtrs(); |
| } |
| |
| void StatusView::RestartTimer(base::TimeDelta delay) { |
| CancelTimer(); |
| StartTimer(delay); |
| } |
| |
| void StatusView::StartFade(float start, float end, base::TimeDelta duration) { |
| animation_ = std::make_unique<StatusViewAnimation>(this, start, end); |
| |
| // This will also reset the currently-occurring animation. |
| animation_->SetDuration(duration); |
| animation_->Start(); |
| } |
| |
| void StatusView::StartHiding() { |
| if (state_ == BubbleState::kShown) { |
| state_ = BubbleState::kPreFadeOut; |
| StartTimer(kHideDelay); |
| } else if (state_ == BubbleState::kFadingIn) { |
| state_ = BubbleState::kFadingOut; |
| // Figure out where we are in the current fade. |
| float current_opacity = animation_->GetCurrentOpacity(); |
| |
| // Start a fade in the opposite direction. |
| StartFade(current_opacity, 0.0f, kHideFadeDuration * current_opacity); |
| } else if (state_ == BubbleState::kPreFadeIn) { |
| HideInstantly(); // This view may be destroyed after calling HideInstantly. |
| } |
| } |
| |
| void StatusView::StartShowing() { |
| destroy_popup_timer_.Stop(); |
| |
| if (state_ == BubbleState::kHidden) { |
| GetWidget()->ShowInactive(); |
| state_ = BubbleState::kPreFadeIn; |
| StartTimer(kShowDelay); |
| } else if (state_ == BubbleState::kPreFadeOut) { |
| state_ = BubbleState::kShown; |
| CancelTimer(); |
| } else if (state_ == BubbleState::kFadingOut) { |
| // We're partway through a fade. |
| state_ = BubbleState::kFadingIn; |
| |
| // Figure out where we are in the current fade. |
| float current_opacity = animation_->GetCurrentOpacity(); |
| |
| // Start a fade in the opposite direction. |
| StartFade(current_opacity, 1.0f, kShowFadeDuration * current_opacity); |
| } else if (state_ == BubbleState::kPreFadeIn) { |
| // We hadn't yet begun showing anything when we received a new request |
| // for something to show, so we start from scratch. |
| ResetTimer(); |
| } |
| } |
| |
| void StatusView::SetTextLabelColors(views::Label* text) { |
| const auto* color_provider = status_bubble_->base_view()->GetColorProvider(); |
| const bool active = |
| status_bubble_->base_view()->GetWidget()->ShouldPaintAsActive(); |
| SkColor bubble_color = color_provider->GetColor( |
| active ? kColorStatusBubbleBackgroundFrameActive |
| : kColorStatusBubbleBackgroundFrameInactive); |
| text->SetBackgroundColor(bubble_color); |
| // Text color is the background tab text color, adjusted if required. |
| text->SetEnabledColor(color_provider->GetColor( |
| active ? kColorStatusBubbleForegroundFrameActive |
| : kColorStatusBubbleForegroundFrameInactive)); |
| } |
| |
| void StatusView::OnPaint(gfx::Canvas* canvas) { |
| gfx::ScopedCanvas scoped(canvas); |
| float scale = canvas->UndoDeviceScaleFactor(); |
| const float radius = kBubbleCornerRadius * scale; |
| |
| SkScalar rad[8] = {}; |
| auto round_corner = [&rad, radius](gfx::RRectF::Corner c) { |
| int index = base::to_underlying(c); |
| rad[2 * index] = radius; |
| rad[2 * index + 1] = radius; |
| }; |
| |
| // 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_ != BubbleStyle::kBottom) { |
| if (base::i18n::IsRTL() != (style_ == BubbleStyle::kStandardRight)) { |
| // The text is RtL or the bubble is on the right side (but not both). |
| round_corner(gfx::RRectF::Corner::kUpperLeft); |
| } else { |
| round_corner(gfx::RRectF::Corner::kUpperRight); |
| } |
| } |
| |
| // Bottom edges - Keep these squared off if the bubble is in its standard |
| // position (sticking upward). |
| if (style_ != BubbleStyle::kStandard && |
| style_ != BubbleStyle::kStandardRight) { |
| round_corner(gfx::RRectF::Corner::kLowerRight); |
| round_corner(gfx::RRectF::Corner::kLowerLeft); |
| } else { |
| #if BUILDFLAG(IS_MAC) |
| // Mac's window has rounded corners, but the corner radius might be |
| // different on different versions. Status bubble will use its own round |
| // corner on Mac when there is no download shelf beneath. |
| if (!status_bubble_->download_shelf_is_visible_) { |
| if (base::i18n::IsRTL() != (style_ == BubbleStyle::kStandard)) |
| round_corner(gfx::RRectF::Corner::kLowerLeft); |
| else |
| round_corner(gfx::RRectF::Corner::kLowerRight); |
| } |
| #endif |
| } |
| |
| // Snap to pixels to avoid shadow blurriness. |
| gfx::Size scaled_size = gfx::ScaleToRoundedSize(size(), scale); |
| |
| // This needs to be pixel-aligned too. Floor is perferred here because a more |
| // conservative value prevents the bottom edge from occasionally leaving a gap |
| // where the web content is visible. |
| const int shadow_thickness_pixels = std::floor(kShadowThickness * scale); |
| |
| // The shadow will overlap the window frame. Clip it off when the bubble is |
| // docked. Otherwise when the bubble is floating preserve the full shadow so |
| // the bubble looks complete. |
| int clip_left = |
| style_ == BubbleStyle::kStandard ? shadow_thickness_pixels : 0; |
| int clip_right = |
| style_ == BubbleStyle::kStandardRight ? shadow_thickness_pixels : 0; |
| if (base::i18n::IsRTL()) |
| std::swap(clip_left, clip_right); |
| |
| const int clip_bottom = clip_left || clip_right ? shadow_thickness_pixels : 0; |
| gfx::Rect clip_rect(scaled_size); |
| clip_rect.Inset(gfx::Insets::TLBR(0, clip_left, clip_bottom, clip_right)); |
| canvas->ClipRect(clip_rect); |
| |
| gfx::RectF bubble_rect{gfx::SizeF(scaled_size)}; |
| // Reposition() moves the bubble down and to the left in order to overlap the |
| // client edge (or window frame when there's no client edge) by 1 DIP. We want |
| // a 1 pixel shadow on the innermost pixel of that overlap. So we inset the |
| // bubble bounds by 1 DIP minus 1 pixel. Failing to do this results in drawing |
| // further and further outside the window as the scale increases. |
| const int inset = shadow_thickness_pixels - 1; |
| bubble_rect.Inset( |
| gfx::InsetsF() |
| .set_left(style_ == BubbleStyle::kStandardRight ? 0 : inset) |
| .set_right(style_ == BubbleStyle::kStandardRight ? inset : 0) |
| .set_bottom(inset)); |
| // Align to pixel centers now that the layout is correct. |
| bubble_rect.Inset(0.5); |
| |
| SkPath path; |
| path.addRoundRect(gfx::RectFToSkRect(bubble_rect), rad); |
| |
| cc::PaintFlags flags; |
| flags.setStyle(cc::PaintFlags::kStroke_Style); |
| flags.setStrokeWidth(1); |
| flags.setAntiAlias(true); |
| |
| SkPath stroke_path; |
| flags.getFillPath(path, &stroke_path); |
| |
| // Get the fill path by subtracting the shadow so they align neatly. |
| SkPath fill_path; |
| Op(path, stroke_path, kDifference_SkPathOp, &fill_path); |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| |
| const auto* color_provider = status_bubble_->base_view()->GetColorProvider(); |
| const auto id = |
| status_bubble_->base_view()->GetWidget()->ShouldPaintAsActive() |
| ? kColorStatusBubbleBackgroundFrameActive |
| : kColorStatusBubbleBackgroundFrameInactive; |
| flags.setColor(color_provider->GetColor(id)); |
| canvas->sk_canvas()->drawPath(fill_path, flags); |
| |
| flags.setColor(color_provider->GetColor(kColorStatusBubbleShadow)); |
| canvas->sk_canvas()->drawPath(stroke_path, flags); |
| } |
| |
| DEFINE_ENUM_CONVERTERS(StatusView::BubbleState, |
| {StatusView::BubbleState::kHidden, u"kHidden"}, |
| {StatusView::BubbleState::kPreFadeIn, u"kPreFadeIn"}, |
| {StatusView::BubbleState::kFadingIn, u"kFadingIn"}, |
| {StatusView::BubbleState::kShown, u"kShown"}, |
| {StatusView::BubbleState::kPreFadeOut, u"kPreFadeOut"}, |
| {StatusView::BubbleState::kFadingOut, u"kFadingOut"}) |
| |
| DEFINE_ENUM_CONVERTERS(StatusView::BubbleStyle, |
| {StatusView::BubbleStyle::kBottom, u"kBottom"}, |
| {StatusView::BubbleStyle::kFloating, u"kFloating"}, |
| {StatusView::BubbleStyle::kStandard, u"kStandard"}, |
| {StatusView::BubbleStyle::kStandardRight, |
| u"kStandardRight"}) |
| |
| BEGIN_METADATA(StatusView, views::View) |
| ADD_PROPERTY_METADATA(std::u16string, Text) |
| ADD_READONLY_PROPERTY_METADATA(StatusView::BubbleState, State) |
| ADD_PROPERTY_METADATA(StatusView::BubbleStyle, Style) |
| END_METADATA |
| |
| // StatusBubbleViews::StatusViewAnimation -------------------------------------- |
| |
| StatusBubbleViews::StatusViewAnimation::StatusViewAnimation( |
| StatusView* status_view, |
| float opacity_start, |
| float opacity_end) |
| : gfx::LinearAnimation(this, kFramerate), |
| views::AnimationDelegateViews(status_view), |
| status_view_(status_view), |
| opacity_start_(opacity_start), |
| opacity_end_(opacity_end) {} |
| |
| StatusBubbleViews::StatusViewAnimation::~StatusViewAnimation() { |
| // Remove ourself as a delegate so that we don't get notified when |
| // animations end as a result of destruction. |
| set_delegate(nullptr); |
| } |
| |
| float StatusBubbleViews::StatusViewAnimation::GetCurrentOpacity() { |
| return static_cast<float>(opacity_start_ + |
| (opacity_end_ - opacity_start_) * |
| gfx::LinearAnimation::GetCurrentValue()); |
| } |
| |
| void StatusBubbleViews::StatusViewAnimation::AnimateToState(double state) { |
| status_view_->SetOpacity(GetCurrentOpacity()); |
| } |
| |
| void StatusBubbleViews::StatusViewAnimation::AnimationEnded( |
| const gfx::Animation* animation) { |
| status_view_->SetOpacity(opacity_end_); |
| status_view_->OnAnimationEnded(); |
| } |
| |
| // StatusBubbleViews::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 gfx::LinearAnimation, |
| public views::AnimationDelegateViews { |
| public: |
| StatusViewExpander(StatusBubbleViews* status_bubble, StatusView* status_view) |
| : gfx::LinearAnimation(this, kFramerate), |
| views::AnimationDelegateViews(status_view), |
| status_bubble_(status_bubble), |
| status_view_(status_view) {} |
| StatusViewExpander(const StatusViewExpander&) = delete; |
| StatusViewExpander& operator=(const StatusViewExpander&) = delete; |
| |
| // Manage the expansion of the bubble. |
| void StartExpansion(const std::u16string& expanded_text, |
| int current_width, |
| int expansion_end); |
| |
| private: |
| // Animation functions. |
| int GetCurrentBubbleWidth(); |
| void SetBubbleWidth(int width); |
| void AnimateToState(double state) override; |
| void AnimationEnded(const gfx::Animation* animation) override; |
| |
| // The status bubble that manages the popup widget and this object. |
| raw_ptr<StatusBubbleViews> status_bubble_; |
| |
| // Change the bounds and text of this view. |
| raw_ptr<StatusView> status_view_; |
| |
| // Text elided (if needed) to fit maximum status bar width. |
| std::u16string expanded_text_; |
| |
| // Widths at expansion start and end. |
| int expansion_start_ = 0; |
| int expansion_end_ = 0; |
| }; |
| |
| void StatusBubbleViews::StatusViewExpander::AnimateToState(double state) { |
| SetBubbleWidth(GetCurrentBubbleWidth()); |
| } |
| |
| void StatusBubbleViews::StatusViewExpander::AnimationEnded( |
| const gfx::Animation* animation) { |
| status_view_->SetText(expanded_text_); |
| SetBubbleWidth(expansion_end_); |
| // WARNING: crash data seems to indicate |this| may be deleted by the time |
| // SetBubbleWidth() returns. |
| } |
| |
| void StatusBubbleViews::StatusViewExpander::StartExpansion( |
| const std::u16string& expanded_text, |
| int expansion_start, |
| int expansion_end) { |
| expanded_text_ = expanded_text; |
| expansion_start_ = expansion_start; |
| expansion_end_ = expansion_end; |
| base::TimeDelta min_duration = std::max( |
| kMinExpansionStepDuration, |
| kMaxExpansionStepDuration * (expansion_end - expansion_start) / 100.0); |
| SetDuration(std::min(kMaxExpansionStepDuration, min_duration)); |
| Start(); |
| } |
| |
| int StatusBubbleViews::StatusViewExpander::GetCurrentBubbleWidth() { |
| return static_cast<int>(expansion_start_ + |
| (expansion_end_ - expansion_start_) * |
| gfx::LinearAnimation::GetCurrentValue()); |
| } |
| |
| void StatusBubbleViews::StatusViewExpander::SetBubbleWidth(int width) { |
| status_view_->SchedulePaint(); |
| status_bubble_->SetBubbleWidth(width); |
| // WARNING: crash data seems to indicate |this| may be deleted by the time |
| // SetBubbleWidth() returns. |
| } |
| |
| |
| // StatusBubbleViews ----------------------------------------------------------- |
| |
| const int StatusBubbleViews::kShadowThickness = 1; |
| |
| StatusBubbleViews::StatusBubbleViews(views::View* base_view) |
| : base_view_(base_view), |
| task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault().get()) {} |
| |
| StatusBubbleViews::~StatusBubbleViews() { |
| DestroyPopup(); |
| } |
| |
| void StatusBubbleViews::InitPopup() { |
| if (!popup_) { |
| DCHECK(!view_); |
| DCHECK(!expand_view_); |
| popup_ = std::make_unique<views::Widget>(); |
| |
| #if BUILDFLAG(IS_MAC) |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_TOOLTIP); |
| #else |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| #endif |
| #if BUILDFLAG(IS_WIN) |
| // On Windows use the software compositor to ensure that we don't block |
| // the UI thread blocking issue during command buffer creation. We can |
| // revert this change once http://crbug.com/125248 is fixed. |
| params.force_software_compositing = true; |
| #endif |
| params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; |
| params.accept_events = false; |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| views::Widget* frame = base_view_->GetWidget(); |
| params.parent = frame->GetNativeView(); |
| params.context = frame->GetNativeWindow(); |
| params.name = "StatusBubble"; |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| params.init_properties_container.SetProperty(ash::kHideInOverviewKey, true); |
| params.init_properties_container.SetProperty(ash::kHideInDeskMiniViewKey, |
| true); |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| popup_->Init(std::move(params)); |
| // We do our own animation and don't want any from the system. |
| popup_->SetVisibilityChangedAnimationsEnabled(false); |
| popup_->SetOpacity(0.f); |
| view_ = popup_->SetContentsView(std::make_unique<StatusView>(this)); |
| expand_view_ = std::make_unique<StatusViewExpander>(this, view_); |
| #if !BUILDFLAG(IS_MAC) |
| // Stack the popup above the base widget and below higher z-order windows. |
| // This is unnecessary and even detrimental on Mac, see CreateBubbleWidget. |
| if (base::FeatureList::IsEnabled(views::features::kWidgetLayering)) { |
| popup_->SetZOrderSublevel(ChromeWidgetSublevel::kSublevelHoverable); |
| } else { |
| popup_->StackAboveWidget(frame); |
| } |
| #endif |
| RepositionPopup(); |
| } |
| } |
| |
| void StatusBubbleViews::DestroyPopup() { |
| CancelExpandTimer(); |
| expand_view_.reset(); |
| view_ = nullptr; |
| // Move |popup_| to the stack to avoid reentrancy issues with CloseNow(). |
| if (std::unique_ptr<views::Widget> popup = std::move(popup_)) |
| popup->CloseNow(); |
| } |
| |
| void StatusBubbleViews::Reposition() { |
| // Overlap the client edge that's shown in restored mode, or when there is no |
| // client edge this makes the bubble snug with the corner of the window. |
| int overlap = kShadowThickness; |
| int height = GetPreferredHeight(); |
| int base_view_height = base_view_->bounds().height(); |
| gfx::Point origin(-overlap, base_view_height - height + overlap); |
| SetBounds(origin.x(), origin.y(), base_view_->bounds().width() / 3, height); |
| } |
| |
| void StatusBubbleViews::RepositionPopup() { |
| if (popup_.get()) { |
| gfx::Point top_left; |
| // TODO(flackr): Get the non-transformed point so that the status bubble |
| // popup window's position is consistent with the base_view_'s window. |
| 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())); |
| } |
| } |
| |
| int StatusBubbleViews::GetPreferredHeight() { |
| return GetFont().GetHeight() + kTotalVerticalPadding; |
| } |
| |
| void StatusBubbleViews::SetBounds(int x, int y, int w, int h) { |
| original_position_.SetPoint(x, y); |
| position_.SetPoint(base_view_->GetMirroredXWithWidthInView(x, w), y); |
| size_.SetSize(w, h); |
| RepositionPopup(); |
| |
| // Initializing the `popup_` views::Widget can trigger a window manager work |
| // area change that calls into this function while `view_` is still null, so |
| // check both `popup_` and `view_`. |
| if (popup_.get() && view_ && contains_mouse_) |
| AvoidMouse(last_mouse_moved_location_); |
| } |
| |
| int StatusBubbleViews::GetWidthForURL(const std::u16string& url_string) { |
| // Get the width of the elided url |
| int elided_url_width = gfx::GetStringWidth(url_string, GetFont()); |
| // Add proper paddings |
| return elided_url_width + (kShadowThickness + kTextHorizPadding) * 2 + 1; |
| } |
| |
| void StatusBubbleViews::OnThemeChanged() { |
| if (popup_) |
| popup_->ThemeChanged(); |
| } |
| |
| void StatusBubbleViews::SetStatus(const std::u16string& 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. |
| |
| status_text_ = status_text; |
| if (status_text_.empty() && url_text_.empty() && !popup_) |
| return; |
| |
| InitPopup(); |
| if (status_text_.empty()) { |
| view_->AnimateForText(url_text_); |
| } else { |
| view_->SetText(status_text_); |
| SetBubbleWidth(GetStandardStatusBubbleWidth()); |
| view_->ShowInstantly(); |
| } |
| } |
| |
| void StatusBubbleViews::SetURL(const GURL& url) { |
| url_ = url; |
| if (size_.IsEmpty()) |
| return; // We have no bounds, don't attempt to show the popup. |
| |
| if (url.is_empty() && status_text_.empty() && !popup_) |
| return; |
| |
| InitPopup(); |
| |
| // 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_ = std::u16string(); |
| if (IsFrameVisible()) |
| view_->AnimateForText(status_text_); |
| return; |
| } |
| |
| // Set Elided Text corresponding to the GURL object. |
| int text_width = static_cast<int>( |
| size_.width() - (kShadowThickness + kTextHorizPadding) * 2 - 1); |
| url_text_ = url_formatter::ElideUrl(url, GetFont(), text_width); |
| |
| // Get the width of the URL if the bubble width is the maximum size. |
| std::u16string full_size_elided_url = |
| url_formatter::ElideUrl(url, GetFont(), GetMaxStatusBubbleWidth()); |
| int url_width = GetWidthForURL(full_size_elided_url); |
| |
| // Get the width for the url if it is unexpanded. |
| int unexpanded_width = std::min(url_width, GetStandardStatusBubbleWidth()); |
| |
| // Reset expansion state only when bubble is completely hidden. |
| if (view_->GetState() == StatusView::BubbleState::kHidden) { |
| is_expanded_ = false; |
| url_text_ = url_formatter::ElideUrl(url, GetFont(), unexpanded_width); |
| SetBubbleWidth(unexpanded_width); |
| } |
| |
| if (IsFrameVisible()) { |
| // If bubble is not expanded & not empty, make it fit properly in the |
| // unexpanded bubble |
| if (!is_expanded_ & !url.is_empty()) { |
| url_text_ = url_formatter::ElideUrl(url, GetFont(), unexpanded_width); |
| SetBubbleWidth(unexpanded_width); |
| } |
| |
| 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 (url_formatter::FormatUrl(url).length() > |
| url_text_.length()) { |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&StatusBubbleViews::ExpandBubble, |
| expand_timer_factory_.GetWeakPtr()), |
| base::Milliseconds(kExpandHoverDelayMS)); |
| } |
| // 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. |
| view_->AnimateForText( |
| base::i18n::GetDisplayStringInLTRDirectionality(url_text_)); |
| } |
| } |
| |
| void StatusBubbleViews::Hide() { |
| status_text_ = std::u16string(); |
| url_text_ = std::u16string(); |
| if (view_) |
| view_->HideInstantly(); |
| } |
| |
| void StatusBubbleViews::MouseMoved(bool left_content) { |
| MouseMovedAt(display::Screen::GetScreen()->GetCursorScreenPoint(), |
| left_content); |
| } |
| |
| void StatusBubbleViews::MouseMovedAt(const gfx::Point& location, |
| bool left_content) { |
| contains_mouse_ = !left_content; |
| if (left_content) { |
| RepositionPopup(); |
| return; |
| } |
| last_mouse_moved_location_ = location; |
| |
| if (view_) { |
| view_->ResetTimer(); |
| |
| if (view_->GetState() != StatusView::BubbleState::kHidden && |
| view_->GetState() != StatusView::BubbleState::kFadingOut && |
| view_->GetState() != StatusView::BubbleState::kPreFadeOut) { |
| AvoidMouse(location); |
| } |
| } |
| } |
| |
| void StatusBubbleViews::UpdateDownloadShelfVisibility(bool visible) { |
| download_shelf_is_visible_ = visible; |
| } |
| |
| void StatusBubbleViews::AvoidMouse(const gfx::Point& location) { |
| DCHECK(view_); |
| // 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().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::BubbleStyle::kBottom); |
| } else if (offset > kBubbleCornerRadius / 2 - kShadowThickness) { |
| view_->SetStyle(StatusView::BubbleStyle::kFloating); |
| } else { |
| view_->SetStyle(StatusView::BubbleStyle::kStandard); |
| } |
| |
| // Check if the bubble sticks out from the monitor or will obscure |
| // download shelf. |
| gfx::NativeView view = base_view_->GetWidget()->GetNativeView(); |
| gfx::Rect monitor_rect = |
| display::Screen::GetScreen()->GetDisplayNearestView(view).work_area(); |
| 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::BubbleStyle::kFloating || |
| view_->GetStyle() == StatusView::BubbleStyle::kBottom))) { |
| // The offset is still too large. Move the bubble to the right and reset |
| // Y offset_ to zero. |
| view_->SetStyle(StatusView::BubbleStyle::kStandardRight); |
| 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::BubbleStyle::kStandardRight) { |
| offset_ = 0; |
| view_->SetStyle(StatusView::BubbleStyle::kStandard); |
| 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::Widget* window = frame->GetTopLevelWidget(); |
| return !window || !window->IsMinimized(); |
| } |
| |
| bool StatusBubbleViews::IsFrameMaximized() { |
| views::Widget* frame = base_view_->GetWidget(); |
| views::Widget* window = frame->GetTopLevelWidget(); |
| return window && window->IsMaximized(); |
| } |
| |
| void StatusBubbleViews::ExpandBubble() { |
| // Elide URL to maximum possible size, then check actual length (it may |
| // still be too long to fit) before expanding bubble. |
| url_text_ = |
| url_formatter::ElideUrl(url_, GetFont(), GetMaxStatusBubbleWidth()); |
| int expanded_bubble_width = |
| std::min(GetWidthForURL(url_text_), GetMaxStatusBubbleWidth()); |
| is_expanded_ = true; |
| expand_view_->StartExpansion(url_text_, size_.width(), expanded_bubble_width); |
| } |
| |
| int StatusBubbleViews::GetStandardStatusBubbleWidth() { |
| return base_view_->bounds().width() / 3; |
| } |
| |
| int StatusBubbleViews::GetMaxStatusBubbleWidth() { |
| const ui::NativeTheme* theme = base_view_->GetNativeTheme(); |
| return static_cast<int>( |
| std::max(0, base_view_->bounds().width() - |
| (kShadowThickness + kTextHorizPadding) * 2 - 1 - |
| views::ScrollBarViews::GetVerticalScrollBarWidth(theme))); |
| } |
| |
| 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_.HasWeakPtrs()) |
| expand_timer_factory_.InvalidateWeakPtrs(); |
| } |
| |
| gfx::Animation* StatusBubbleViews::GetShowHideAnimationForTest() { |
| return view_ ? view_->animation() : nullptr; |
| } |
| |
| bool StatusBubbleViews::IsDestroyPopupTimerRunningForTest() { |
| return view_ && view_->IsDestroyPopupTimerRunning(); |
| } |