| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/system/toast/anchored_nudge.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "ash/public/cpp/shelf_config.h" |
| #include "ash/public/cpp/shelf_types.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shelf/hotseat_widget.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shell.h" |
| #include "ash/system/toast/nudge_constants.h" |
| #include "ash/system/toast/system_nudge_view.h" |
| #include "ash/wm/work_area_insets.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/i18n/rtl.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/mojom/dialog_button.mojom.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/native_ui_types.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/bubble/bubble_dialog_delegate_view.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/dialog_client_view.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Offsets the bottom of work area to account for the current hotseat state. |
| void AdjustWorkAreaBoundsForHotseatState(const HotseatWidget* hotseat_widget, |
| gfx::Rect& work_area_bounds) { |
| switch (hotseat_widget->state()) { |
| case HotseatState::kExtended: |
| work_area_bounds.set_height(work_area_bounds.height() - |
| hotseat_widget->GetHotseatSize() - |
| ShelfConfig::Get()->hotseat_bottom_padding()); |
| break; |
| case HotseatState::kShownHomeLauncher: |
| work_area_bounds.set_height(hotseat_widget->GetTargetBounds().y() - |
| work_area_bounds.y()); |
| break; |
| case HotseatState::kHidden: |
| case HotseatState::kShownClamshell: |
| case HotseatState::kNone: |
| // Do nothing. |
| return; |
| } |
| } |
| |
| // Returns true if the provided arrow is located at a corner. |
| bool CalculateIsCornerAnchored(views::BubbleBorder::Arrow arrow) { |
| switch (arrow) { |
| case views::BubbleBorder::Arrow::TOP_LEFT: |
| case views::BubbleBorder::Arrow::TOP_RIGHT: |
| case views::BubbleBorder::Arrow::BOTTOM_LEFT: |
| case views::BubbleBorder::Arrow::BOTTOM_RIGHT: |
| case views::BubbleBorder::Arrow::LEFT_TOP: |
| case views::BubbleBorder::Arrow::RIGHT_TOP: |
| case views::BubbleBorder::Arrow::LEFT_BOTTOM: |
| case views::BubbleBorder::Arrow::RIGHT_BOTTOM: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| gfx::Point GetAnchorPoint(views::Widget* anchor_widget, |
| views::BubbleBorder::Arrow corner) { |
| const bool is_rtl = base::i18n::IsRTL(); |
| auto bounds = anchor_widget->GetWindowBoundsInScreen(); |
| |
| const gfx::Point bottom_left = |
| gfx::Point(bounds.x() + kBubbleBorderInsets.left(), |
| bounds.bottom() - kBubbleBorderInsets.bottom()); |
| const gfx::Point bottom_right = |
| gfx::Point(bounds.right() - kBubbleBorderInsets.right(), |
| bounds.bottom() - kBubbleBorderInsets.bottom()); |
| |
| // Only support corners at the bottom of the widget. |
| switch (corner) { |
| case views::BubbleBorder::Arrow::BOTTOM_LEFT: |
| case views::BubbleBorder::Arrow::LEFT_BOTTOM: |
| return is_rtl ? bottom_right : bottom_left; |
| case views::BubbleBorder::Arrow::BOTTOM_RIGHT: |
| case views::BubbleBorder::Arrow::RIGHT_BOTTOM: |
| return is_rtl ? bottom_left : bottom_right; |
| default: |
| return is_rtl ? bottom_right : bottom_left; |
| } |
| } |
| |
| } // namespace |
| |
| AnchoredNudge::AnchoredNudge( |
| AnchoredNudgeData& nudge_data, |
| base::RepeatingCallback<void(/*has_hover_or_focus=*/bool)> |
| hover_or_focus_changed_callback) |
| : views::BubbleDialogDelegateView(nudge_data.GetAnchorView(), |
| nudge_data.arrow, |
| views::BubbleBorder::NO_SHADOW), |
| id_(nudge_data.id), |
| catalog_name_(nudge_data.catalog_name), |
| anchored_to_shelf_(nudge_data.anchored_to_shelf), |
| is_corner_anchored_(CalculateIsCornerAnchored(nudge_data.arrow)), |
| set_anchor_view_as_parent_(nudge_data.set_anchor_view_as_parent), |
| anchor_widget_(nudge_data.anchor_widget), |
| anchor_widget_corner_(nudge_data.arrow), |
| click_callback_(std::move(nudge_data.click_callback)), |
| dismiss_callback_(std::move(nudge_data.dismiss_callback)) { |
| SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone)); |
| SetBackgroundColor(SK_ColorTRANSPARENT); |
| set_margins(gfx::Insets()); |
| set_close_on_deactivate(false); |
| set_highlight_button_when_shown(nudge_data.highlight_anchor_button); |
| SetLayoutManager(std::make_unique<views::FlexLayout>()); |
| system_nudge_view_ = AddChildView(std::make_unique<SystemNudgeView>( |
| nudge_data, std::move(hover_or_focus_changed_callback))); |
| |
| // Make nudge not focus traversable if it does not have any buttons. |
| if (nudge_data.primary_button_text.empty()) { |
| set_focus_traversable_from_anchor_view(false); |
| } |
| |
| if (anchored_to_shelf_ || !GetAnchorView()) { |
| Shell::Get()->AddShellObserver(this); |
| } |
| |
| if (!nudge_data.announce_chromevox) { |
| SetAccessibleWindowRole(ax::mojom::Role::kNone); |
| } |
| } |
| |
| AnchoredNudge::~AnchoredNudge() { |
| if (!dismiss_callback_.is_null()) { |
| std::move(dismiss_callback_).Run(); |
| } |
| |
| if (anchored_to_shelf_) { |
| disable_shelf_auto_hide_.reset(); |
| } |
| |
| if (anchored_to_shelf_ || !GetAnchorView()) { |
| Shell::Get()->RemoveShellObserver(this); |
| } |
| |
| anchor_widget_ = nullptr; |
| } |
| |
| gfx::Rect AnchoredNudge::GetBubbleBounds() { |
| auto* root_window = GetWidget()->GetNativeWindow(); |
| |
| // This can happen during destruction. |
| if (!root_window) { |
| return gfx::Rect(); |
| } |
| |
| gfx::Rect bubble_bounds = views::BubbleDialogDelegateView::GetBubbleBounds(); |
| if (anchor_widget_) { |
| return bubble_bounds; |
| } |
| |
| gfx::Rect work_area_bounds = |
| WorkAreaInsets::ForWindow(root_window)->user_work_area_bounds(); |
| |
| auto* hotseat_widget = |
| RootWindowController::ForWindow(root_window)->shelf()->hotseat_widget(); |
| if (hotseat_widget) { |
| AdjustWorkAreaBoundsForHotseatState(hotseat_widget, work_area_bounds); |
| } |
| bubble_bounds.AdjustToFit(work_area_bounds); |
| |
| return bubble_bounds; |
| } |
| |
| void AnchoredNudge::OnBeforeBubbleWidgetInit(views::Widget::InitParams* params, |
| views::Widget* widget) const { |
| if (set_anchor_view_as_parent_ && GetAnchorView() && |
| GetAnchorView()->GetWidget()) { |
| params->parent = GetAnchorView()->GetWidget()->GetNativeView(); |
| return; |
| } |
| |
| if (anchor_widget_) { |
| params->parent = anchor_widget_->GetNativeView(); |
| return; |
| } |
| |
| params->parent = Shell::GetRootWindowForNewWindows()->GetChildById( |
| kShellWindowId_SettingBubbleContainer); |
| } |
| |
| std::unique_ptr<views::FrameView> AnchoredNudge::CreateFrameView( |
| views::Widget* widget) { |
| // Create the customized bubble border. |
| std::unique_ptr<views::BubbleBorder> bubble_border = |
| std::make_unique<views::BubbleBorder>(arrow(), |
| views::BubbleBorder::NO_SHADOW); |
| bubble_border->set_avoid_shadow_overlap(true); |
| bubble_border->set_insets(kBubbleBorderInsets); |
| |
| auto frame = BubbleDialogDelegateView::CreateFrameView(widget); |
| static_cast<views::BubbleFrameView*>(frame.get()) |
| ->SetBubbleBorder(std::move(bubble_border)); |
| return frame; |
| } |
| |
| void AnchoredNudge::AddedToWidget() { |
| // Do not attempt fitting the bubble inside the anchor view window. |
| GetBubbleFrameView()->set_use_anchor_window_bounds(false); |
| |
| // Remove accelerator so the nudge won't be closed when pressing the Esc key. |
| GetDialogClientView()->RemoveAccelerator( |
| ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE)); |
| |
| // Widget needs a native window in order to observe its shelf. |
| CHECK(GetWidget()->GetNativeWindow()); |
| auto* shelf = Shelf::ForWindow(GetWidget()->GetNativeWindow()); |
| |
| if (anchored_to_shelf_) { |
| DCHECK(GetAnchorView()); |
| SetArrowFromShelf(shelf); |
| disable_shelf_auto_hide_ = |
| std::make_unique<Shelf::ScopedDisableAutoHide>(shelf); |
| return; |
| } |
| |
| if (anchor_widget_) { |
| // Setting an `anchor_widget_` assumes that there is no anchor view, because |
| // widget anchoring is used when an anchor view cannot be set. |
| CHECK(!GetAnchorView()); |
| |
| anchor_widget_scoped_observation_.Observe(anchor_widget_); |
| gfx::Point anchor_point = |
| GetAnchorPoint(anchor_widget_, anchor_widget_corner_); |
| SetAnchorRect(gfx::Rect(anchor_point, gfx::Size(0, 0))); |
| return; |
| } |
| |
| if (!GetAnchorView()) { |
| shelf_observation_.Observe(shelf); |
| SetDefaultAnchorRect(); |
| } |
| } |
| |
| bool AnchoredNudge::OnMousePressed(const ui::MouseEvent& event) { |
| return true; |
| } |
| |
| bool AnchoredNudge::OnMouseDragged(const ui::MouseEvent& event) { |
| return true; |
| } |
| |
| void AnchoredNudge::OnMouseReleased(const ui::MouseEvent& event) { |
| if (event.IsOnlyLeftMouseButton() && !click_callback_.is_null()) { |
| std::move(click_callback_).Run(); |
| } |
| } |
| |
| void AnchoredNudge::OnGestureEvent(ui::GestureEvent* event) { |
| switch (event->type()) { |
| case ui::EventType::kGestureTap: { |
| if (!click_callback_.is_null()) { |
| std::move(click_callback_).Run(); |
| event->SetHandled(); |
| } |
| return; |
| } |
| default: { |
| // Do nothing. |
| } |
| } |
| } |
| |
| void AnchoredNudge::OnAutoHideStateChanged(ShelfAutoHideState new_state) { |
| if (!GetAnchorView()) { |
| SetDefaultAnchorRect(); |
| } |
| } |
| |
| void AnchoredNudge::OnHotseatStateChanged(HotseatState old_state, |
| HotseatState new_state) { |
| if (!GetAnchorView()) { |
| SetDefaultAnchorRect(); |
| } |
| } |
| |
| void AnchoredNudge::OnShelfAlignmentChanged(aura::Window* root_window, |
| ShelfAlignment old_alignment) { |
| if (!GetWidget() || !GetWidget()->GetNativeWindow()) { |
| return; |
| } |
| |
| // Nudges without an anchor view will be shown on their default location. |
| if (!GetAnchorView()) { |
| SetDefaultAnchorRect(); |
| return; |
| } |
| |
| // Nudges anchored to a view that exists in the shelf need to update their |
| // arrow value when the shelf alignment changes. |
| if (anchored_to_shelf_) { |
| auto* shelf = Shelf::ForWindow(GetWidget()->GetNativeWindow()); |
| if (shelf == Shelf::ForWindow(root_window)) { |
| SetArrowFromShelf(shelf); |
| } |
| } |
| } |
| |
| void AnchoredNudge::OnDisplayMetricsChanged(const display::Display& display, |
| uint32_t changed_metrics) { |
| if (GetAnchorView()) { |
| OnAnchorBoundsChanged(); |
| } else { |
| SetDefaultAnchorRect(); |
| } |
| } |
| |
| void AnchoredNudge::OnWidgetDestroying(views::Widget* widget) { |
| if (widget != anchor_widget_) { |
| return; |
| } |
| |
| anchor_widget_ = nullptr; |
| anchor_widget_scoped_observation_.Reset(); |
| } |
| |
| void AnchoredNudge::OnWidgetBoundsChanged(views::Widget* widget, |
| const gfx::Rect& new_bounds) { |
| if (widget != anchor_widget_) { |
| return; |
| } |
| |
| gfx::Point anchor_point = |
| GetAnchorPoint(anchor_widget_, anchor_widget_corner_); |
| SetAnchorRect(gfx::Rect(anchor_point, gfx::Size(0, 0))); |
| } |
| |
| void AnchoredNudge::SetArrowFromShelf(Shelf* shelf) { |
| if (is_corner_anchored_) { |
| SetArrow(shelf->SelectValueForShelfAlignment( |
| views::BubbleBorder::BOTTOM_RIGHT, views::BubbleBorder::LEFT_BOTTOM, |
| views::BubbleBorder::RIGHT_BOTTOM)); |
| } else { |
| SetArrow(shelf->SelectValueForShelfAlignment( |
| views::BubbleBorder::BOTTOM_CENTER, views::BubbleBorder::LEFT_CENTER, |
| views::BubbleBorder::RIGHT_CENTER)); |
| } |
| } |
| |
| void AnchoredNudge::SetDefaultAnchorRect() { |
| if (anchor_widget_) { |
| // The anchor position will be set by tracking the bounds of |
| // `anchor_widget_` and update when the widget bounds changed. |
| return; |
| } |
| |
| if (!GetWidget() || !GetWidget()->GetNativeWindow()) { |
| return; |
| } |
| |
| // The default location for a nudge without an `anchor_view` is the leading |
| // bottom corner of the work area bounds (bottom-left for LTR languages). |
| gfx::Rect work_area_bounds = |
| WorkAreaInsets::ForWindow(GetWidget()->GetNativeWindow()) |
| ->user_work_area_bounds(); |
| SetAnchorRect( |
| gfx::Rect(gfx::Point(base::i18n::IsRTL() ? work_area_bounds.right() |
| : work_area_bounds.x(), |
| work_area_bounds.bottom()), |
| gfx::Size(0, 0))); |
| } |
| |
| BEGIN_METADATA(AnchoredNudge) |
| END_METADATA |
| |
| } // namespace ash |