| // 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 "ash/system/tray/tray_bubble_view.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <numeric> |
| |
| #include "ash/accelerators/accelerator_controller_impl.h" |
| #include "ash/accessibility/accessibility_controller.h" |
| #include "ash/bubble/bubble_constants.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/accelerators.h" |
| #include "ash/public/cpp/style/color_provider.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shell.h" |
| #include "ash/style/ash_color_id.h" |
| #include "ash/style/system_shadow.h" |
| #include "ash/system/notification_center/notification_center_tray.h" |
| #include "ash/system/tray/system_tray_notifier.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_utils.h" |
| #include "base/memory/raw_ptr.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/aura/env.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/accelerators/accelerator.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/mojom/dialog_button.mojom.h" |
| #include "ui/chromeos/styles/cros_tokens_color_mappings.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_type.h" |
| #include "ui/compositor_extra/shadow.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/events/event.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/rounded_corners_f.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/background.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/highlight_border.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/layout_manager_base.h" |
| #include "ui/views/painter.h" |
| #include "ui/views/views_delegate.h" |
| #include "ui/wm/core/shadow_types.h" |
| #include "ui/wm/core/window_util.h" |
| |
| using views::BubbleBorder; |
| using views::BubbleFrameView; |
| using views::FrameView; |
| using views::View; |
| using views::ViewsDelegate; |
| using views::Widget; |
| |
| namespace ash { |
| |
| namespace { |
| |
| BubbleBorder::Arrow GetArrowAlignment(ash::ShelfAlignment alignment) { |
| // The tray bubble is in a corner. In this case, we want the arrow to be |
| // flush with one side instead of centered on the bubble. |
| switch (alignment) { |
| case ash::ShelfAlignment::kBottom: |
| case ash::ShelfAlignment::kBottomLocked: |
| return base::i18n::IsRTL() ? BubbleBorder::BOTTOM_LEFT |
| : BubbleBorder::BOTTOM_RIGHT; |
| case ash::ShelfAlignment::kLeft: |
| return BubbleBorder::LEFT_BOTTOM; |
| case ash::ShelfAlignment::kRight: |
| return BubbleBorder::RIGHT_BOTTOM; |
| } |
| } |
| |
| // Detects any mouse movement. This is needed to detect mouse movements by the |
| // user over the bubble if the bubble got created underneath the cursor. |
| class MouseMoveDetectorHost : public views::MouseWatcherHost { |
| public: |
| MouseMoveDetectorHost(); |
| |
| MouseMoveDetectorHost(const MouseMoveDetectorHost&) = delete; |
| MouseMoveDetectorHost& operator=(const MouseMoveDetectorHost&) = delete; |
| |
| ~MouseMoveDetectorHost() override; |
| |
| bool Contains(const gfx::Point& screen_point, EventType type) override; |
| }; |
| |
| MouseMoveDetectorHost::MouseMoveDetectorHost() {} |
| |
| MouseMoveDetectorHost::~MouseMoveDetectorHost() {} |
| |
| bool MouseMoveDetectorHost::Contains(const gfx::Point& screen_point, |
| EventType type) { |
| return false; |
| } |
| |
| // Custom layout for the bubble-view. Does the default box-layout if there is |
| // enough height. Otherwise, makes sure the bottom rows are visible. |
| class BottomAlignedBoxLayout : public views::LayoutManagerBase { |
| public: |
| explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view) |
| : bubble_view_(bubble_view) { |
| box_layout_ = AddOwnedLayout(std::make_unique<views::BoxLayout>()); |
| box_layout_->SetDefaultFlex(1); |
| box_layout_->SetOrientation(views::BoxLayout::Orientation::kVertical); |
| } |
| |
| BottomAlignedBoxLayout(const BottomAlignedBoxLayout&) = delete; |
| BottomAlignedBoxLayout& operator=(const BottomAlignedBoxLayout&) = delete; |
| |
| ~BottomAlignedBoxLayout() override {} |
| |
| views::BoxLayout* box_layout() const { return box_layout_; } |
| |
| private: |
| views::ProposedLayout CalculateProposedLayout( |
| const views::SizeBounds& size_bounds) const override { |
| if (!size_bounds.is_fully_bounded() || |
| size_bounds.height() >= host_view()->GetPreferredSize({}).height() || |
| !bubble_view_->is_gesture_dragging()) { |
| return box_layout_->GetProposedLayout(size_bounds, PassKey()); |
| } |
| |
| views::ProposedLayout layout; |
| int consumed_height = 0; |
| for (auto i = host_view()->children().rbegin(); |
| i != host_view()->children().rend() && |
| consumed_height < size_bounds.height().value(); |
| ++i) { |
| auto& child = layout.child_layouts.emplace_back(*i, (*i)->GetVisible()); |
| if (!child.visible) { |
| continue; |
| } |
| gfx::Size size = child.child_view->GetPreferredSize({}); |
| child.bounds = gfx::Rect( |
| 0, size_bounds.height().value() - consumed_height - size.height(), |
| size_bounds.width().value(), size.height()); |
| consumed_height += size.height(); |
| } |
| |
| layout.host_size = |
| gfx::Size(size_bounds.width().value(), size_bounds.height().value()); |
| |
| return layout; |
| } |
| |
| raw_ptr<views::BoxLayout> box_layout_; |
| raw_ptr<TrayBubbleView> bubble_view_; |
| }; |
| |
| } // namespace |
| |
| TrayBubbleView::Delegate::Delegate() = default; |
| |
| TrayBubbleView::Delegate::~Delegate() = default; |
| |
| void TrayBubbleView::Delegate::BubbleViewDestroyed() {} |
| |
| void TrayBubbleView::Delegate::OnMouseEnteredView() {} |
| |
| void TrayBubbleView::Delegate::OnMouseExitedView() {} |
| |
| std::u16string TrayBubbleView::Delegate::GetAccessibleNameForBubble() { |
| return std::u16string(); |
| } |
| |
| bool TrayBubbleView::Delegate::ShouldEnableExtraKeyboardAccessibility() { |
| return false; |
| } |
| |
| base::WeakPtr<TrayBubbleView::Delegate> TrayBubbleView::Delegate::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| std::optional<AcceleratorAction> |
| TrayBubbleView::Delegate::GetAcceleratorAction() const { |
| // TODO(crbug/1234891) Make this a pure virtual function so all |
| // bubble delegates need to specify accelerator actions. |
| return std::nullopt; |
| } |
| |
| TrayBubbleView::InitParams::InitParams() = default; |
| |
| TrayBubbleView::InitParams::~InitParams() = default; |
| |
| TrayBubbleView::InitParams::InitParams(const InitParams& other) = default; |
| |
| TrayBubbleView::RerouteEventHandler::RerouteEventHandler( |
| TrayBubbleView* tray_bubble_view) |
| : tray_bubble_view_(tray_bubble_view) { |
| aura::Env::GetInstance()->AddPreTargetHandler( |
| this, ui::EventTarget::Priority::kSystem); |
| } |
| |
| TrayBubbleView::RerouteEventHandler::~RerouteEventHandler() { |
| aura::Env::GetInstance()->RemovePreTargetHandler(this); |
| } |
| |
| void TrayBubbleView::RerouteEventHandler::OnKeyEvent(ui::KeyEvent* event) { |
| // Do not handle a key event if it is targeted to the tray or its descendants, |
| // or if the target has the tray as a transient ancestor. RerouteEventHandler |
| // is for rerouting events which are not targetted to the tray. Those events |
| // should be handled by the target. |
| aura::Window* target = static_cast<aura::Window*>(event->target()); |
| aura::Window* tray_window = tray_bubble_view_->GetWidget()->GetNativeView(); |
| if (target && (tray_window->Contains(target) || |
| wm::HasTransientAncestor(target, tray_window))) { |
| return; |
| } |
| |
| // Only passes Tab, Shift+Tab, Esc to the widget as it can consume more key |
| // events. e.g. Alt+Tab can be consumed as focus traversal by FocusManager. |
| ui::KeyboardCode key_code = event->key_code(); |
| int flags = event->flags() & |
| (ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN | |
| ui::EF_COMMAND_DOWN | ui::EF_ALTGR_DOWN | ui::EF_MOD3_DOWN); |
| if ((key_code == ui::VKEY_TAB && flags == ui::EF_NONE) || |
| (key_code == ui::VKEY_TAB && flags == ui::EF_SHIFT_DOWN) || |
| (key_code == ui::VKEY_ESCAPE && flags == ui::EF_NONE) || |
| // Do not dismiss the bubble immediately when a user triggers a feedback |
| // report; if they're reporting an issue with the bubble we want the |
| // screenshot to contain it. |
| (key_code == ui::VKEY_I && |
| flags == (ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN))) { |
| // Make TrayBubbleView activatable as the following Widget::OnKeyEvent might |
| // try to activate it. |
| tray_bubble_view_->SetCanActivate(true); |
| |
| tray_bubble_view_->GetWidget()->OnKeyEvent(event); |
| |
| if (event->handled()) { |
| return; |
| } |
| } |
| |
| // For shelf pod bubble that is anchored to the shelf corner, popup |
| // notifications and the bubble can be shown at the same time. If the keyboard |
| // happens inside the popup collection, we need to let the keyboard event pass |
| // there to make sure notification popups can receive keyboard events. |
| if (target && |
| tray_bubble_view_->GetBubbleType() == TrayBubbleType::kShelfPodBubble && |
| tray_bubble_view_->IsAnchoredToShelfCorner()) { |
| auto* popup_collection = RootWindowController::ForWindow(target) |
| ->shelf() |
| ->GetStatusAreaWidget() |
| ->notification_center_tray() |
| ->popup_collection(); |
| |
| if (popup_collection->popup_collection_bounds().Contains( |
| target->GetActualBoundsInScreen())) { |
| return; |
| } |
| } |
| |
| // Always consumes key event not to pass it to other widgets. Calling |
| // StopPropagation here to make this consistent with |
| // MenuController::OnWillDispatchKeyEvent. |
| event->StopPropagation(); |
| |
| // To provide consistent behavior with a menu, process accelerator as a menu |
| // is open if the event is not handled by the widget. |
| ui::Accelerator accelerator(*event); |
| |
| // crbug/1212857 Immediately close the bubble if the accelerator action |
| // is going to do it and do not process the accelerator. If the accelerator |
| // action is executed asynchronously it will execute after the bubble has |
| // already been closed and it will result in the accelerator action reopening |
| // the bubble. |
| if (tray_bubble_view_->GetAcceleratorAction().has_value() && |
| AcceleratorControllerImpl::Get()->DoesAcceleratorMatchAction( |
| ui::Accelerator(*event), |
| tray_bubble_view_->GetAcceleratorAction().value())) { |
| tray_bubble_view_->CloseBubbleView(); |
| } else { |
| ViewsDelegate::GetInstance()->ProcessAcceleratorWhileMenuShowing( |
| accelerator); |
| } |
| } |
| |
| void TrayBubbleView::RerouteEventHandler::OnEvent(ui::Event* event) { |
| if (!tray_bubble_view_->set_can_activate_on_click_or_tap_) { |
| EventHandler::OnEvent(event); |
| return; |
| } |
| |
| if (event->type() == ui::EventType::kMousePressed || |
| event->type() == ui::EventType::kTouchPressed || |
| event->type() == ui::EventType::kGestureTap) { |
| tray_bubble_view_->SetCanActivate(true); |
| } |
| |
| EventHandler::OnEvent(event); |
| } |
| |
| TrayBubbleView::TrayBubbleView(const InitParams& init_params) |
| : BubbleDialogDelegateView(init_params.anchor_view, |
| GetArrowAlignment(init_params.shelf_alignment)), |
| params_(init_params), |
| layout_(nullptr), |
| delegate_(init_params.delegate), |
| preferred_width_(init_params.preferred_width), |
| is_gesture_dragging_(false), |
| set_can_activate_on_click_or_tap_( |
| init_params.set_can_activate_on_click_or_tap), |
| mouse_actively_entered_(false) { |
| // We set the dialog role because views::BubbleDialogDelegate defaults this to |
| // an alert dialog. This would make screen readers announce the whole of the |
| // system tray which is undesirable. |
| SetAccessibleWindowRole(ax::mojom::Role::kDialog); |
| SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone)); |
| |
| DCHECK(delegate_); |
| DCHECK(params_.parent_window); |
| // anchor_widget() is computed by BubbleDialogDelegateView(). |
| DCHECK((init_params.anchor_mode != TrayBubbleView::AnchorMode::kView) || |
| anchor_widget()); |
| set_parent_window(params_.parent_window); |
| AccessibilityController* controller = |
| Shell::Get()->accessibility_controller(); |
| SetCanActivate(controller->spoken_feedback().enabled() || |
| controller->dictation().enabled()); |
| SetNotifyEnterExitOnChild(true); |
| set_close_on_deactivate(init_params.close_on_deactivate); |
| set_margins(init_params.margin.has_value() ? init_params.margin.value() |
| : gfx::Insets()); |
| set_use_round_corners(false); |
| SetBackgroundColor(SK_ColorTRANSPARENT); |
| set_layer_type((params_.translucent || params_.transparent) |
| ? ui::LAYER_NOT_DRAWN |
| : ui::LAYER_TEXTURED); |
| |
| // Always create a layer so that the layer for FocusRing stays in this view's |
| // layer. Without it, the layer for FocusRing goes above the NativeViewHost |
| // and may steal events. |
| // TODO(crbug.com/40832096): In the dark light mode feature, remove layer |
| // creation in children views of this view to improve performance. |
| SetPaintToLayer(init_params.transparent ? ui::LAYER_NOT_DRAWN |
| : ui::LAYER_TEXTURED); |
| |
| if (!init_params.transparent) { |
| layer()->SetRoundedCornerRadius( |
| gfx::RoundedCornersF{static_cast<float>(params_.corner_radius)}); |
| layer()->SetIsFastRoundedCorner(true); |
| SetBorder(std::make_unique<views::HighlightBorder>( |
| params_.corner_radius, |
| views::HighlightBorder::Type::kHighlightBorderOnShadow)); |
| |
| const bool is_translucent = |
| init_params.translucent && chromeos::features::IsSystemBlurEnabled(); |
| |
| // A translucent TrayBubbleView initializes the widget with NOT_DRAWN_LAYER. |
| // Therefore the BubbleFrameView(FrameView) that provides the |
| // background will not be painted. Therefore TrayBubbleView should paint its |
| // own background. |
| SetBackground(views::CreateSolidBackground( |
| is_translucent ? cros_tokens::kCrosSysSystemBaseElevated |
| : cros_tokens::kCrosSysSystemBaseElevatedOpaque)); |
| layer()->SetFillsBoundsOpaquely(!is_translucent); |
| if (is_translucent) { |
| layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma); |
| layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality); |
| } |
| } |
| |
| if (params_.has_shadow) { |
| // Draws shadow on texture layer for large corner radius bubbles. |
| if (params_.has_large_corner_radius) { |
| shadow_ = SystemShadow::CreateShadowOnTextureLayer(params_.shadow_type); |
| shadow_->SetRoundedCornerRadius(params_.corner_radius); |
| } else if (features::IsSystemTrayShadowEnabled()) { |
| shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView( |
| this, params_.shadow_type); |
| shadow_->SetRoundedCornerRadius(params_.corner_radius); |
| } |
| } |
| |
| auto layout = std::make_unique<BottomAlignedBoxLayout>(this); |
| layout_ = SetLayoutManager(std::move(layout))->box_layout(); |
| |
| if (init_params.anchor_mode == AnchorMode::kRect) { |
| SetAnchorView(nullptr); |
| SetAnchorRect(init_params.anchor_rect); |
| } else { |
| SetAnchorView(init_params.anchor_view); |
| SetAnchorRect(gfx::Rect()); |
| } |
| |
| message_center::MessageCenter::Get()->AddObserver(this); |
| Shell::Get()->display_manager()->AddDisplayObserver(this); |
| |
| GetViewAccessibility().SetRole(ax::mojom::Role::kWindow); |
| UpdateAccessibleName(); |
| UpdateAccessibleIgnoredState(); |
| |
| name_changed_subscription_ = |
| GetViewAccessibility().AddStringAttributeChangedCallback( |
| ax::mojom::StringAttribute::kName, |
| base::BindRepeating(&TrayBubbleView::OnAXNameChanged, |
| base::Unretained(this))); |
| } |
| |
| TrayBubbleView::~TrayBubbleView() { |
| Shell::Get()->display_manager()->RemoveDisplayObserver(this); |
| message_center::MessageCenter::Get()->RemoveObserver(this); |
| |
| mouse_watcher_.reset(); |
| |
| if (delegate_) { |
| // Inform host items (models) that their views are being destroyed. |
| delegate_->BubbleViewDestroyed(); |
| } |
| } |
| |
| void TrayBubbleView::InitializeAndShowBubble() { |
| GetWidget()->Show(); |
| UpdateBubble(); |
| UpdateAccessibleName(); |
| UpdateAccessibleIgnoredState(); |
| |
| // Manually sets the shadow position since `CreateShadowOnTextureLayer` only |
| // constructs the shadow but doesn't deal with shadow positioning. |
| if (params_.has_shadow && params_.has_large_corner_radius) { |
| AddLayerToRegion(shadow_->GetLayer(), views::LayerRegion::kBelow); |
| shadow_->SetContentBounds(layer()->bounds()); |
| } |
| |
| // Register pre target event handler to reroute key |
| // events to the widget for activating the view or closing it. |
| if (!CanActivate() && params_.reroute_event_handler) { |
| reroute_event_handler_ = std::make_unique<RerouteEventHandler>(this); |
| } |
| |
| if (IsAnchoredToStatusArea()) { |
| NotifyTrayBubbleOpen(); |
| } |
| } |
| |
| void TrayBubbleView::UpdateBubble() { |
| if (GetWidget()) { |
| SizeToContents(); |
| GetWidget()->GetRootView()->SchedulePaint(); |
| } |
| } |
| |
| void TrayBubbleView::SetMaxHeight(int height) { |
| params_.max_height = height; |
| if (GetWidget()) { |
| SizeToContents(); |
| } |
| } |
| |
| void TrayBubbleView::SetBottomPadding(int padding) { |
| layout_->set_inside_border_insets(gfx::Insets::TLBR(0, 0, padding, 0)); |
| } |
| |
| void TrayBubbleView::SetPreferredWidth(int width) { |
| if (preferred_width_ == width) { |
| return; |
| } |
| preferred_width_ = width; |
| if (GetWidget()) { |
| SizeToContents(); |
| } |
| } |
| |
| gfx::Insets TrayBubbleView::GetBorderInsets() const { |
| auto* bubble_border = GetBubbleFrameView()->bubble_border(); |
| return bubble_border ? bubble_border->GetInsets() : gfx::Insets(); |
| } |
| |
| std::optional<AcceleratorAction> TrayBubbleView::GetAcceleratorAction() const { |
| return delegate_->GetAcceleratorAction(); |
| } |
| |
| void TrayBubbleView::ResetDelegate() { |
| reroute_event_handler_.reset(); |
| |
| delegate_ = nullptr; |
| UpdateAccessibleIgnoredState(); |
| GetViewAccessibility().SetName(std::u16string()); |
| } |
| |
| void TrayBubbleView::ChangeAnchorView(views::View* anchor_view) { |
| DCHECK_EQ(AnchorMode::kView, params_.anchor_mode); |
| BubbleDialogDelegateView::SetAnchorView(anchor_view); |
| } |
| |
| void TrayBubbleView::ChangeAnchorRect(const gfx::Rect& rect) { |
| DCHECK_EQ(AnchorMode::kRect, params_.anchor_mode); |
| BubbleDialogDelegateView::SetAnchorRect(rect); |
| } |
| |
| void TrayBubbleView::ChangeAnchorAlignment(ShelfAlignment alignment) { |
| SetArrow(GetArrowAlignment(alignment)); |
| } |
| |
| bool TrayBubbleView::IsAnchoredToStatusArea() const { |
| return params_.is_anchored_to_status_area; |
| } |
| |
| bool TrayBubbleView::IsAnchoredToShelfCorner() const { |
| return params_.anchor_to_shelf_corner; |
| } |
| |
| void TrayBubbleView::StopReroutingEvents() { |
| reroute_event_handler_.reset(); |
| } |
| |
| TrayBubbleView::TrayBubbleType TrayBubbleView::GetBubbleType() const { |
| return params_.type; |
| } |
| |
| void TrayBubbleView::OnWidgetClosing(Widget* widget) { |
| // We no longer need to watch key events for activation if the widget is |
| // closing. |
| reroute_event_handler_.reset(); |
| |
| if (IsAnchoredToStatusArea()) { |
| NotifyTrayBubbleClosed(); |
| } |
| |
| BubbleDialogDelegateView::OnWidgetClosing(widget); |
| } |
| |
| void TrayBubbleView::OnWidgetActivationChanged(Widget* widget, bool active) { |
| // We no longer need to watch key events for activation if the widget is |
| // activated. |
| reroute_event_handler_.reset(); |
| |
| BubbleDialogDelegateView::OnWidgetActivationChanged(widget, active); |
| } |
| |
| void TrayBubbleView::OnWidgetBoundsChanged(views::Widget* widget, |
| const gfx::Rect& bounds) { |
| Shell::Get()->system_tray_notifier()->NotifyTrayBubbleBoundsChanged(this); |
| } |
| |
| std::unique_ptr<FrameView> TrayBubbleView::CreateFrameView(Widget* widget) { |
| // Create the customized bubble border. |
| std::unique_ptr<BubbleBorder> bubble_border = |
| std::make_unique<BubbleBorder>(arrow(), BubbleBorder::NO_SHADOW); |
| if (GetParams().round_corners) { |
| bubble_border->set_rounded_corners(gfx::RoundedCornersF(GetCornerRadius())); |
| } |
| |
| bubble_border->SetColor(background_color()); |
| bubble_border->set_avoid_shadow_overlap(true); |
| if (params_.insets.has_value()) { |
| bubble_border->set_insets(params_.insets.value()); |
| } |
| |
| auto frame = BubbleDialogDelegateView::CreateFrameView(widget); |
| auto* frame_ptr = static_cast<views::BubbleFrameView*>(frame.get()); |
| frame_ptr->SetBubbleBorder(std::move(bubble_border)); |
| if (params_.anchor_mode == AnchorMode::kView) { |
| frame_ptr->set_use_anchor_window_bounds(false); |
| } |
| |
| return frame; |
| } |
| |
| bool TrayBubbleView::WidgetHasHitTestMask() const { |
| return true; |
| } |
| |
| void TrayBubbleView::GetWidgetHitTestMask(SkPath* mask) const { |
| DCHECK(mask); |
| mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds())); |
| } |
| |
| std::u16string TrayBubbleView::GetAccessibleWindowTitle() const { |
| if (delegate_) { |
| return delegate_->GetAccessibleNameForBubble(); |
| } else { |
| return std::u16string(); |
| } |
| } |
| |
| void TrayBubbleView::AddedToWidget() { |
| // If the view has a shadow on texture layer, should make it observe widget |
| // theme change to update its colors. The function is called here since we |
| // should guarantee that `GetWidget()` returns non-nullptr. |
| if (params_.has_shadow && params_.has_large_corner_radius) { |
| shadow_->ObserveColorProviderSource(GetWidget()); |
| } |
| } |
| |
| gfx::Size TrayBubbleView::CalculatePreferredSize( |
| const views::SizeBounds& available_size) const { |
| const int width = std::max(preferred_width_ - GetInsets().width(), 0); |
| const int height = std::transform_reduce( |
| children().cbegin(), children().cend(), GetInsets().height(), |
| std::plus<>(), [width](const views::View* child) { |
| return child->GetVisible() ? child->GetHeightForWidth(width) : 0; |
| }); |
| if (params_.use_fixed_height) { |
| return gfx::Size(preferred_width_, |
| (params_.max_height != 0) ? params_.max_height : height); |
| } |
| return gfx::Size(preferred_width_, (params_.max_height != 0) |
| ? std::min(height, params_.max_height) |
| : height); |
| } |
| |
| void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) { |
| mouse_watcher_.reset(); |
| if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) { |
| // The user actively moved the mouse over the bubble; inform the delegate. |
| delegate_->OnMouseEnteredView(); |
| mouse_actively_entered_ = true; |
| } else { |
| // The mouse was located over the bubble when it was first shown; use |
| // MouseWatcher to wait for user interaction before signaling the delegate. |
| mouse_watcher_ = std::make_unique<views::MouseWatcher>( |
| std::make_unique<MouseMoveDetectorHost>(), this); |
| mouse_watcher_->set_notify_on_exit_time(base::TimeDelta()); |
| mouse_watcher_->Start(GetWidget()->GetNativeWindow()); |
| } |
| } |
| |
| void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) { |
| // Disable any MouseWatcher waiting for user interaction inside the bubble. |
| mouse_watcher_.reset(); |
| // Only notify the delegate on exit if it was notified on enter. |
| if (delegate_ && mouse_actively_entered_) { |
| delegate_->OnMouseExitedView(); |
| } |
| } |
| |
| void TrayBubbleView::MouseMovedOutOfHost() { |
| // The user moved the mouse that was over the bubble when it was first shown. |
| if (delegate_) { |
| delegate_->OnMouseEnteredView(); |
| } |
| mouse_actively_entered_ = true; |
| mouse_watcher_.reset(); |
| } |
| |
| bool TrayBubbleView::ShouldUseFixedHeight() const { |
| return params_.use_fixed_height; |
| } |
| |
| void TrayBubbleView::SetShouldUseFixedHeight(bool shoud_use_fixed_height) { |
| params_.use_fixed_height = shoud_use_fixed_height; |
| } |
| |
| void TrayBubbleView::OnNotificationDisplayed( |
| const std::string& notification_id, |
| const message_center::DisplaySource source) { |
| // Stack bubble view at the bottom when a new popup is displayed so popup |
| // collection can be shown in the front. |
| if (source == message_center::DISPLAY_SOURCE_POPUP) { |
| aura::Window* tray_window = GetWidget()->GetNativeView(); |
| tray_window->parent()->StackChildAtBottom(tray_window); |
| } |
| } |
| |
| void TrayBubbleView::OnDisplayTabletStateChanged(display::TabletState state) { |
| if (display::IsTabletStateChanging(state)) { |
| // Do nothing when the tablet state is still in the process of transition. |
| return; |
| } |
| |
| aura::Window* tray_window = GetWidget()->GetNativeView(); |
| Shelf* shelf = Shelf::ForWindow(tray_window); |
| if (params_.anchor_mode == AnchorMode::kRect) { |
| SetAnchorRect(shelf->GetSystemTrayAnchorRect()); |
| } |
| |
| // The shelf alignment may change when transitioning between tablet and |
| // clamshell mode. In those cases, we need to update the shelf alighment. |
| if (ash::ShelfAlignment current_alignment = shelf->alignment(); |
| current_alignment != params_.shelf_alignment) { |
| params_.shelf_alignment = current_alignment; |
| ChangeAnchorAlignment(current_alignment); |
| } |
| |
| SetBubbleBorderInsets(GetTrayBubbleInsets(tray_window)); |
| UpdateBubble(); |
| } |
| |
| void TrayBubbleView::NotifyTrayBubbleOpen() { |
| DCHECK(IsAnchoredToStatusArea()); |
| |
| if (GetBubbleType() == TrayBubbleType::kShelfPodBubble) { |
| StatusAreaWidget::ForWindow(GetWidget()->GetNativeView()) |
| ->SetOpenShelfPodBubble(this); |
| } |
| |
| Shell::Get() |
| ->system_tray_notifier() |
| ->NotifyStatusAreaAnchoredBubbleVisibilityChanged(/*tray_bubble=*/this, |
| /*visible=*/true); |
| } |
| |
| void TrayBubbleView::NotifyTrayBubbleClosed() { |
| DCHECK(IsAnchoredToStatusArea()); |
| |
| auto* status_area = StatusAreaWidget::ForWindow(GetWidget()->GetNativeView()); |
| |
| // `TrayBubbleView` may live longer than `StatusAreaWidget`. |
| if (status_area && GetBubbleType() == TrayBubbleType::kShelfPodBubble) { |
| status_area->SetOpenShelfPodBubble(nullptr); |
| } |
| |
| Shell::Get() |
| ->system_tray_notifier() |
| ->NotifyStatusAreaAnchoredBubbleVisibilityChanged(/*tray_bubble=*/this, |
| /*visible=*/false); |
| } |
| |
| void TrayBubbleView::CloseBubbleView() { |
| if (!delegate_) { |
| return; |
| } |
| |
| delegate_->HideBubble(this); |
| } |
| |
| void TrayBubbleView::UpdateAccessibleName() { |
| if (delegate_->GetAccessibleNameForBubble().empty()) { |
| GetViewAccessibility().SetName( |
| delegate_->GetAccessibleNameForBubble(), |
| ax::mojom::NameFrom::kAttributeExplicitlyEmpty); |
| } else { |
| GetViewAccessibility().SetName(delegate_->GetAccessibleNameForBubble()); |
| } |
| } |
| |
| void TrayBubbleView::ChildPreferredSizeChanged(View* child) { |
| SizeToContents(); |
| } |
| |
| void TrayBubbleView::SetBubbleBorderInsets(gfx::Insets insets) { |
| if (GetBubbleFrameView()->bubble_border()) { |
| GetBubbleFrameView()->bubble_border()->set_insets(insets); |
| } |
| } |
| |
| void TrayBubbleView::UpdateAccessibleIgnoredState() { |
| GetViewAccessibility().SetIsIgnored(!delegate_ || !CanActivate()); |
| } |
| |
| void TrayBubbleView::OnAXNameChanged(ax::mojom::StringAttribute attribute, |
| const std::optional<std::string>& name) { |
| if (GetWidget()) { |
| GetWidget()->UpdateAccessibleNameForRootView(); |
| } |
| } |
| |
| BEGIN_METADATA(TrayBubbleView) |
| END_METADATA |
| |
| } // namespace ash |