| // Copyright 2017 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 "ash/login/ui/login_base_bubble_view.h" |
| |
| #include <memory> |
| |
| #include "ash/login/ui/views_utils.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/shell.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/aura/client/focus_change_observer.h" |
| #include "ui/aura/client/focus_client.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/events/event_handler.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/background.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| |
| namespace ash { |
| namespace { |
| |
| // Total width of the bubble view. |
| constexpr int kBubbleTotalWidthDp = 192; |
| |
| // Padding around the bubble view. |
| constexpr int kBubblePaddingDp = 16; |
| |
| // Spacing between the child view inside the bubble view. |
| constexpr int kBubbleBetweenChildSpacingDp = 16; |
| |
| // Border radius of the rounded bubble. |
| constexpr int kBubbleBorderRadius = 8; |
| |
| // The amount of time for bubble show/hide animation. |
| constexpr base::TimeDelta kBubbleAnimationDuration = base::Milliseconds(300); |
| |
| } // namespace |
| |
| // This class handles keyboard, mouse, and focus events, and dismisses the |
| // associated bubble in response. |
| class LoginBubbleHandler : public ui::EventHandler { |
| public: |
| explicit LoginBubbleHandler(LoginBaseBubbleView* bubble) : bubble_(bubble) { |
| Shell::Get()->AddPreTargetHandler(this); |
| } |
| |
| LoginBubbleHandler(const LoginBubbleHandler&) = delete; |
| LoginBubbleHandler& operator=(const LoginBubbleHandler&) = delete; |
| |
| ~LoginBubbleHandler() override { Shell::Get()->RemovePreTargetHandler(this); } |
| |
| // ui::EventHandler: |
| void OnMouseEvent(ui::MouseEvent* event) override { |
| if (event->type() == ui::ET_MOUSE_PRESSED) |
| ProcessPressedEvent(event->AsLocatedEvent()); |
| } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| if (event->type() == ui::ET_GESTURE_TAP || |
| event->type() == ui::ET_GESTURE_TAP_DOWN) { |
| ProcessPressedEvent(event->AsLocatedEvent()); |
| } |
| } |
| |
| void OnKeyEvent(ui::KeyEvent* event) override { |
| if (event->type() != ui::ET_KEY_PRESSED || |
| event->key_code() == ui::VKEY_PROCESSKEY) { |
| return; |
| } |
| |
| if (!bubble_->GetVisible()) |
| return; |
| |
| // Hide the bubble if the bubble opener is about to lose focus from tab |
| // traversal. |
| if (bubble_->GetBubbleOpener() && bubble_->GetBubbleOpener()->HasFocus() && |
| event->key_code() != ui::VKEY_TAB) { |
| return; |
| } |
| |
| if (login_views_utils::HasFocusInAnyChildView(bubble_)) |
| return; |
| |
| if (!bubble_->is_persistent()) |
| bubble_->Hide(); |
| } |
| |
| private: |
| void ProcessPressedEvent(const ui::LocatedEvent* event) { |
| if (!bubble_->GetVisible()) |
| return; |
| |
| gfx::Point screen_location = event->location(); |
| ::wm::ConvertPointToScreen(static_cast<aura::Window*>(event->target()), |
| &screen_location); |
| |
| // Don't do anything with clicks inside the bubble. |
| if (bubble_->GetBoundsInScreen().Contains(screen_location)) |
| return; |
| |
| // Let the bubble opener handle clicks on itself. |
| if (bubble_->GetBubbleOpener() && |
| bubble_->GetBubbleOpener()->GetBoundsInScreen().Contains( |
| screen_location)) { |
| return; |
| } |
| |
| if (!bubble_->is_persistent()) |
| bubble_->Hide(); |
| } |
| |
| LoginBaseBubbleView* bubble_; |
| }; |
| |
| LoginBaseBubbleView::LoginBaseBubbleView(views::View* anchor_view) |
| : LoginBaseBubbleView(anchor_view, nullptr) {} |
| |
| LoginBaseBubbleView::LoginBaseBubbleView(views::View* anchor_view, |
| aura::Window* parent_window) |
| : anchor_view_(anchor_view), |
| bubble_handler_(std::make_unique<LoginBubbleHandler>(this)) { |
| views::BoxLayout* layout_manager = |
| SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical, |
| gfx::Insets(kBubblePaddingDp), kBubbleBetweenChildSpacingDp)); |
| layout_manager->set_cross_axis_alignment( |
| views::BoxLayout::CrossAxisAlignment::kStart); |
| |
| SetVisible(false); |
| } |
| |
| void LoginBaseBubbleView::EnsureLayer() { |
| if (layer()) |
| return; |
| // Layer rendering is needed for animation. |
| SetPaintToLayer(); |
| SkColor background_color = AshColorProvider::Get()->GetBaseLayerColor( |
| AshColorProvider::BaseLayerType::kTransparent80); |
| SetBackground(views::CreateRoundedRectBackground(background_color, |
| kBubbleBorderRadius)); |
| layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma); |
| layer()->SetFillsBoundsOpaquely(false); |
| } |
| |
| LoginBaseBubbleView::~LoginBaseBubbleView() = default; |
| |
| void LoginBaseBubbleView::Show() { |
| if (layer()) |
| layer()->GetAnimator()->RemoveObserver(this); |
| |
| SetSize(GetPreferredSize()); |
| SetPosition(CalculatePosition()); |
| |
| ScheduleAnimation(true /*visible*/); |
| |
| // Tell ChromeVox to read bubble contents. |
| if (notify_a11y_alert_on_show_) { |
| NotifyAccessibilityEvent(ax::mojom::Event::kAlert, |
| true /*send_native_event*/); |
| } |
| } |
| |
| void LoginBaseBubbleView::Hide() { |
| ScheduleAnimation(false /*visible*/); |
| } |
| |
| LoginButton* LoginBaseBubbleView::GetBubbleOpener() const { |
| return nullptr; |
| } |
| |
| gfx::Point LoginBaseBubbleView::CalculatePosition() { |
| if (!GetAnchorView()) |
| return gfx::Point(); |
| |
| // Views' positions are defined in the parents' coordinate system. Therefore, |
| // the position of the bubble needs to be returned in its parent's coordinate |
| // system. kTryBeforeThenAfter and kTryAfterThenBefore use strategies implying |
| // to know the bounds of the entire window; therefore, resulting position is |
| // calculated by using the root view's coordinates system. kShowAbove and |
| // kShowBelow are less complicated, they only use the coordinate system of the |
| // the anchor view's parent. |
| |
| // In RTL case, we are doing mirroring in `ConvertPointToTarget` when finding |
| // the position of the bubble. However, there's no need to mirror since the |
| // coordinate system is from right to left and the origin is the right upper |
| // corner. `GetMirroredXWithWidthInView` is called to cancel out the mirroring |
| // effect and returning the correct position for the bubble. |
| gfx::Point anchor_position = GetAnchorView()->bounds().origin(); |
| gfx::Point origin; |
| ConvertPointToTarget(GetAnchorView()->parent() /*source*/, |
| GetAnchorView()->GetWidget()->GetRootView() /*target*/, |
| &origin); |
| origin.set_x(parent()->GetMirroredXWithWidthInView( |
| origin.x(), GetAnchorView()->parent()->width())); |
| anchor_position += origin.OffsetFromOrigin(); |
| auto bounds = GetBoundsAvailableToShowBubble(); |
| gfx::Size bubble_size(width() + 2 * horizontal_padding_, |
| height() + vertical_padding_); |
| |
| gfx::Point result; |
| View* source; |
| switch (positioning_strategy_) { |
| case PositioningStrategy::kTryBeforeThenAfter: |
| result = login_views_utils::CalculateBubblePositionBeforeAfterStrategy( |
| {anchor_position, GetAnchorView()->size()}, bubble_size, bounds); |
| source = GetAnchorView()->GetWidget()->GetRootView(); |
| break; |
| case PositioningStrategy::kTryAfterThenBefore: |
| result = login_views_utils::CalculateBubblePositionAfterBeforeStrategy( |
| {anchor_position, GetAnchorView()->size()}, bubble_size, bounds); |
| source = GetAnchorView()->GetWidget()->GetRootView(); |
| break; |
| case PositioningStrategy::kShowAbove: { |
| gfx::Point top_center = GetAnchorView()->bounds().top_center(); |
| result = top_center - gfx::Vector2d(GetPreferredSize().width() / 2, |
| GetPreferredSize().height()); |
| source = GetAnchorView()->parent(); |
| break; |
| } |
| case PositioningStrategy::kShowBelow: { |
| result = GetAnchorView()->bounds().bottom_left(); |
| source = GetAnchorView()->parent(); |
| break; |
| } |
| } |
| // Get position of the bubble surrounded by paddings. |
| result.Offset(horizontal_padding_, 0); |
| ConvertPointToTarget(source /*source*/, parent() /*target*/, &result); |
| return result; |
| } |
| |
| void LoginBaseBubbleView::SetAnchorView(views::View* anchor_view) { |
| anchor_view_ = anchor_view; |
| } |
| |
| void LoginBaseBubbleView::OnLayerAnimationEnded( |
| ui::LayerAnimationSequence* sequence) { |
| layer()->GetAnimator()->RemoveObserver(this); |
| SetVisible(false); |
| DestroyLayer(); |
| } |
| |
| void LoginBaseBubbleView::OnLayerAnimationAborted( |
| ui::LayerAnimationSequence* sequence) { |
| // The animation for this view should never be aborted. |
| NOTREACHED(); |
| } |
| |
| gfx::Size LoginBaseBubbleView::CalculatePreferredSize() const { |
| gfx::Size size; |
| size.set_width(kBubbleTotalWidthDp); |
| size.set_height(GetHeightForWidth(kBubbleTotalWidthDp)); |
| return size; |
| } |
| |
| void LoginBaseBubbleView::Layout() { |
| views::View::Layout(); |
| |
| // If a Layout() is called while the bubble is visible (i.e. due to Show()), |
| // its bounds may change because of the parent's LayoutManager. This allows |
| // the bubbles to always determine their own size and position. |
| if (GetVisible()) { |
| SetSize(GetPreferredSize()); |
| SetPosition(CalculatePosition()); |
| } |
| } |
| |
| void LoginBaseBubbleView::OnBlur() { |
| Hide(); |
| } |
| |
| void LoginBaseBubbleView::OnThemeChanged() { |
| views::View::OnThemeChanged(); |
| SkColor background_color = AshColorProvider::Get()->GetBaseLayerColor( |
| AshColorProvider::BaseLayerType::kTransparent80); |
| SetBackground(views::CreateRoundedRectBackground(background_color, |
| kBubbleBorderRadius)); |
| } |
| |
| void LoginBaseBubbleView::SetPadding(int horizontal_padding, |
| int vertical_padding) { |
| horizontal_padding_ = horizontal_padding; |
| vertical_padding_ = vertical_padding; |
| } |
| |
| gfx::Rect LoginBaseBubbleView::GetBoundsAvailableToShowBubble() const { |
| auto bounds = GetRootViewBounds(); |
| auto work_area = GetWorkArea(); |
| // Get min means here to exclude either shelf or virtual keyaboard. |
| bounds.set_height(std::min(bounds.height(), work_area.height())); |
| return bounds; |
| } |
| |
| gfx::Rect LoginBaseBubbleView::GetRootViewBounds() const { |
| return GetAnchorView()->GetWidget()->GetRootView()->GetLocalBounds(); |
| } |
| |
| gfx::Rect LoginBaseBubbleView::GetWorkArea() const { |
| return display::Screen::GetScreen() |
| ->GetDisplayNearestWindow(GetAnchorView()->GetWidget()->GetNativeWindow()) |
| .work_area(); |
| } |
| |
| void LoginBaseBubbleView::ScheduleAnimation(bool visible) { |
| if (GetBubbleOpener()) { |
| views::InkDrop::Get(GetBubbleOpener()) |
| ->AnimateToState(visible ? views::InkDropState::ACTIVATED |
| : views::InkDropState::DEACTIVATED, |
| nullptr /*event*/); |
| } |
| |
| if (layer()) |
| layer()->GetAnimator()->StopAnimating(); |
| |
| EnsureLayer(); |
| float opacity_start = 0.0f; |
| float opacity_end = 1.0f; |
| if (!visible) { |
| std::swap(opacity_start, opacity_end); |
| // We only need to handle animation ending if we're hiding the bubble. |
| layer()->GetAnimator()->AddObserver(this); |
| } else { |
| SetVisible(true); |
| } |
| |
| layer()->SetOpacity(opacity_start); |
| { |
| ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator()); |
| settings.SetTransitionDuration(kBubbleAnimationDuration); |
| settings.SetTweenType(visible ? gfx::Tween::EASE_OUT : gfx::Tween::EASE_IN); |
| settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| layer()->SetOpacity(opacity_end); |
| } |
| } |
| |
| } // namespace ash |