chromium/ash/system/tray/tray_bubble_view.cc

// 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/skia_conversions.h"
#include "ui/views/accessibility/view_accessibility.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::NonClientFrameView;
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);
  // We force to create contents background since the bubble border background
  // is not shown in this view.
  set_force_create_contents_background(true);
  // Bubbles that use transparent colors should not paint their ClientViews to a
  // layer as doing so could result in visual artifacts.
  SetPaintClientToLayer(false);
  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());

  if (init_params.translucent) {
    // TODO(crbug.com/40832096): In the dark light mode feature, remove layer
    // creation in children views of this view to improve performance.
    SetPaintToLayer(ui::LAYER_TEXTURED);
    layer()->SetFillsBoundsOpaquely(false);
    layer()->SetRoundedCornerRadius(
        gfx::RoundedCornersF{static_cast<float>(params_.corner_radius)});
    layer()->SetIsFastRoundedCorner(true);
    layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
    layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
  } else {
    // 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.
    SetPaintToLayer(ui::LAYER_NOT_DRAWN);

    if (!init_params.transparent) {
      SetPaintToLayer();
      layer()->SetRoundedCornerRadius(
          gfx::RoundedCornersF{static_cast<float>(params_.corner_radius)});
    }
  }

  if (init_params.transparent) {
    set_color(SK_ColorTRANSPARENT);
  }

  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);
  UpdateAccessibleIgnoredState();
}

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();
  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();
}

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);
}

ui::LayerType TrayBubbleView::GetLayerType() const {
  if (params_.translucent) {
    return ui::LAYER_NOT_DRAWN;
  }
  return ui::LAYER_TEXTURED;
}

std::unique_ptr<NonClientFrameView> TrayBubbleView::CreateNonClientFrameView(
    Widget* widget) {
  // Create the customized bubble border.
  std::unique_ptr<BubbleBorder> bubble_border =
      std::make_unique<BubbleBorder>(arrow(), BubbleBorder::NO_SHADOW);
  if (params_.corner_radius) {
    bubble_border->SetCornerRadius(params_.corner_radius);
  }
  bubble_border->set_avoid_shadow_overlap(true);
  if (params_.insets.has_value()) {
    bubble_border->set_insets(params_.insets.value());
  }

  auto frame = BubbleDialogDelegateView::CreateNonClientFrameView(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::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  if (delegate_ && CanActivate()) {
    node_data->SetNameChecked(delegate_->GetAccessibleNameForBubble());
  }
}

void TrayBubbleView::OnThemeChanged() {
  views::BubbleDialogDelegateView::OnThemeChanged();
  if (params_.transparent) {
    return;
  }

  SetBorder(std::make_unique<views::HighlightBorder>(
      params_.corner_radius,
      chromeos::features::IsJellyrollEnabled()
          ? views::HighlightBorder::Type::kHighlightBorderOnShadow
          : views::HighlightBorder::Type::kHighlightBorder1));
  set_color(
      GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemBaseElevated));
}

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::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());
}

BEGIN_METADATA(TrayBubbleView)
END_METADATA

}  // namespace ash