#include "ash/system/holding_space/holding_space_tray_bubble.h"
#include <map>
#include <vector>
#include "ash/bubble/bubble_utils.h"
#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/holding_space/holding_space_metrics.h"
#include "ash/public/cpp/holding_space/holding_space_prefs.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/holding_space/holding_space_item_view.h"
#include "ash/system/holding_space/holding_space_tray.h"
#include "ash/system/holding_space/holding_space_ui.h"
#include "ash/system/holding_space/pinned_files_bubble.h"
#include "ash/system/holding_space/recent_files_bubble.h"
#include "ash/system/tray/tray_bubble_wrapper.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_utils.h"
#include "ash/wm/work_area_insets.h"
#include "base/containers/adapters.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/display/tablet_state.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"
#include "ui/views/animation/animation_delegate_views.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/proposed_layout.h"
#include "ui/views/view_observer.h"
namespace ash {
namespace {
constexpr base::TimeDelta kAnimationDuration = base::Milliseconds(167);
void FindVisibleHoldingSpaceItems(
views::View* parent,
std::vector<const HoldingSpaceItem*>* result) {
for (views::View* view : parent->children()) {
if (view->GetVisible() && HoldingSpaceItemView::IsInstance(view))
result->push_back(HoldingSpaceItemView::Cast(view)->item());
FindVisibleHoldingSpaceItems(view, result);
}
}
void RecordTimeFromFirstAvailabilityToFirstEntry(PrefService* prefs) {
base::Time time_of_first_availability =
holding_space_prefs::GetTimeOfFirstAvailability(prefs).value();
base::Time time_of_first_entry =
holding_space_prefs::GetTimeOfFirstEntry(prefs).value();
holding_space_metrics::RecordTimeFromFirstAvailabilityToFirstEntry(
time_of_first_entry - time_of_first_availability);
}
class HoldingSpaceTrayBubbleEventHandler : public ui::EventHandler {
public:
HoldingSpaceTrayBubbleEventHandler(HoldingSpaceTrayBubble* bubble,
HoldingSpaceViewDelegate* delegate)
: bubble_(bubble), delegate_(delegate) {
aura::Env::GetInstance()->AddPreTargetHandler(
this, ui::EventTarget::Priority::kSystem);
}
HoldingSpaceTrayBubbleEventHandler(
const HoldingSpaceTrayBubbleEventHandler&) = delete;
HoldingSpaceTrayBubbleEventHandler& operator=(
const HoldingSpaceTrayBubbleEventHandler&) = delete;
~HoldingSpaceTrayBubbleEventHandler() override {
aura::Env::GetInstance()->RemovePreTargetHandler(this);
}
private:
void OnKeyEvent(ui::KeyEvent* event) override {
if (event->type() != ui::EventType::kKeyPressed) {
return;
}
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* bubble_window = bubble_->GetBubbleWidget()->GetNativeView();
if (target && (bubble_window->Contains(target)))
return;
if (delegate_->OnHoldingSpaceTrayBubbleKeyPressed(*event))
event->StopPropagation();
}
const raw_ptr<HoldingSpaceTrayBubble> bubble_;
const raw_ptr<HoldingSpaceViewDelegate> delegate_;
};
class ChildBubbleContainerLayout {
public:
ChildBubbleContainerLayout(views::View* host, int child_spacing)
: host_(host), child_spacing_(child_spacing) {}
void SetMaxHeight(int max_height) { max_height_ = max_height; }
views::ProposedLayout CalculateProposedLayout() const {
views::ProposedLayout layout;
layout.host_size = gfx::Size(kTrayMenuWidth, 0);
int top = 0;
for (views::View* child : host_->children()) {
if (!child->GetVisible()) {
views::ChildLayout child_layout;
child_layout.child_view = child;
child_layout.bounds = gfx::Rect(0, top, layout.host_size.width(), 0);
child_layout.visible = false;
layout.child_layouts.push_back(std::move(child_layout));
continue;
}
if (top != 0) {
top += child_spacing_;
layout.host_size.Enlarge(0, child_spacing_);
}
const int height = child->GetHeightForWidth(layout.host_size.width());
views::ChildLayout child_layout;
child_layout.child_view = child;
child_layout.bounds = gfx::Rect(0, top, layout.host_size.width(), height);
child_layout.visible = true;
layout.child_layouts.push_back(std::move(child_layout));
layout.host_size.Enlarge(0, height);
top += height;
}
if (max_height_ && layout.host_size.height() > max_height_) {
const int height_to_cede =
std::min(layout.child_layouts[0].bounds.height(),
layout.host_size.height() - max_height_);
layout.child_layouts[0].bounds.Inset(
gfx::Insets::TLBR(0, 0, height_to_cede, 0));
for (size_t i = 1; i < layout.child_layouts.size(); ++i)
layout.child_layouts[i].bounds.Offset(0, -height_to_cede);
layout.host_size.Enlarge(0, -height_to_cede);
}
return layout;
}
void ApplyLayout(const views::ProposedLayout& layout) {
for (const auto& child_layout : layout.child_layouts)
child_layout.child_view->SetBoundsRect(child_layout.bounds);
}
const raw_ptr<views::View> host_;
const int child_spacing_;
int max_height_ = 0;
};
}
class HoldingSpaceTrayBubble::ChildBubbleContainer
: public views::View,
public views::AnimationDelegateViews {
METADATA_HEADER(ChildBubbleContainer, views::View)
public:
ChildBubbleContainer()
: views::AnimationDelegateViews(this),
layout_manager_(this, kHoldingSpaceBubbleContainerChildSpacing) {}
void SetMaxHeight(int max_height) {
layout_manager_.SetMaxHeight(max_height);
PreferredSizeChanged();
}
gfx::Size CalculatePreferredSize(
const views::SizeBounds& available_size) const override {
if (current_layout_.host_size.IsEmpty())
current_layout_ = layout_manager_.CalculateProposedLayout();
return current_layout_.host_size;
}
void ChildPreferredSizeChanged(views::View* child) override {
PreferredSizeChanged();
}
void ChildVisibilityChanged(views::View* child) override {
PreferredSizeChanged();
}
void PreferredSizeChanged() override {
if (!GetWidget())
return;
const views::ProposedLayout target_layout(
layout_manager_.CalculateProposedLayout());
if (target_layout == target_layout_) {
views::View::PreferredSizeChanged();
return;
}
if (current_layout_.host_size.IsEmpty()) {
current_layout_ = target_layout_ = target_layout;
views::View::PreferredSizeChanged();
return;
}
start_layout_ = current_layout_;
target_layout_ = target_layout;
layout_animation_ = std::make_unique<gfx::SlideAnimation>(this);
layout_animation_->SetSlideDuration(
gfx::ScopedAnimationDurationScaleMode::duration_multiplier() *
kAnimationDuration);
layout_animation_->SetTweenType(gfx::Tween::Type::FAST_OUT_SLOW_IN);
layout_animation_throughput_tracker_ =
GetWidget()->GetCompositor()->RequestNewCompositorMetricsTracker();
layout_animation_throughput_tracker_->Start(
metrics_util::ForSmoothnessV3(base::BindRepeating(
holding_space_metrics::RecordBubbleResizeAnimationSmoothness)));
layout_animation_->Show();
}
void Layout(PassKey) override {
layout_manager_.ApplyLayout(current_layout_);
}
void AnimationProgressed(const gfx::Animation* animation) override {
current_layout_ = views::ProposedLayoutBetween(
animation->GetCurrentValue(), start_layout_, target_layout_);
PreferredSizeChanged();
}
void AnimationEnded(const gfx::Animation* animation) override {
current_layout_ = target_layout_;
PreferredSizeChanged();
layout_animation_throughput_tracker_->Stop();
layout_animation_throughput_tracker_.reset();
}
private:
ChildBubbleContainerLayout layout_manager_;
mutable views::ProposedLayout start_layout_;
mutable views::ProposedLayout current_layout_;
mutable views::ProposedLayout target_layout_;
std::unique_ptr<gfx::SlideAnimation> layout_animation_;
std::optional<ui::ThroughputTracker> layout_animation_throughput_tracker_;
};
BEGIN_METADATA(HoldingSpaceTrayBubble, ChildBubbleContainer)
END_METADATA
HoldingSpaceTrayBubble::HoldingSpaceTrayBubble(
HoldingSpaceTray* holding_space_tray)
: holding_space_tray_(holding_space_tray) {}
HoldingSpaceTrayBubble::~HoldingSpaceTrayBubble() {
bubble_wrapper_->bubble_view()->ResetDelegate();
for (HoldingSpaceTrayChildBubble* child_bubble : child_bubbles_) {
child_bubble->Reset();
}
}
void HoldingSpaceTrayBubble::Init() {
TrayBubbleView::InitParams init_params = CreateInitParamsForTrayBubble(
holding_space_tray_, true);
init_params.has_shadow = false;
init_params.translucent = false;
init_params.transparent = true;
auto bubble_view = std::make_unique<TrayBubbleView>(init_params);
child_bubble_container_ =
bubble_view->AddChildView(std::make_unique<ChildBubbleContainer>());
child_bubble_container_->SetMaxHeight(
CalculateChildBubbleContainerMaxHeight());
child_bubbles_.push_back(child_bubble_container_->AddChildView(
std::make_unique<PinnedFilesBubble>(&delegate_)));
child_bubbles_.push_back(child_bubble_container_->AddChildView(
std::make_unique<RecentFilesBubble>(&delegate_)));
for (HoldingSpaceTrayChildBubble* child_bubble : child_bubbles_)
child_bubble->Init();
bubble_wrapper_ = std::make_unique<TrayBubbleWrapper>(holding_space_tray_);
bubble_wrapper_->ShowBubble(std::move(bubble_view));
event_handler_ =
std::make_unique<HoldingSpaceTrayBubbleEventHandler>(this, &delegate_);
PrefService* const prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (holding_space_prefs::MarkTimeOfFirstEntry(prefs))
RecordTimeFromFirstAvailabilityToFirstEntry(prefs);
std::vector<const HoldingSpaceItem*> visible_items;
FindVisibleHoldingSpaceItems(bubble_wrapper_->bubble_view(), &visible_items);
holding_space_metrics::RecordVisibleItemCounts(visible_items);
shelf_observation_.Observe(holding_space_tray_->shelf());
}
void HoldingSpaceTrayBubble::AnchorUpdated() {
bubble_wrapper_->bubble_view()->UpdateBubble();
}
TrayBubbleView* HoldingSpaceTrayBubble::GetBubbleView() {
return bubble_wrapper_->bubble_view();
}
views::Widget* HoldingSpaceTrayBubble::GetBubbleWidget() {
return bubble_wrapper_->GetBubbleWidget();
}
std::vector<HoldingSpaceItemView*>
HoldingSpaceTrayBubble::GetHoldingSpaceItemViews() {
std::vector<HoldingSpaceItemView*> views;
for (HoldingSpaceTrayChildBubble* child_bubble : child_bubbles_) {
auto child_bubble_views = child_bubble->GetHoldingSpaceItemViews();
views.insert(views.end(), child_bubble_views.begin(),
child_bubble_views.end());
}
return views;
}
int HoldingSpaceTrayBubble::CalculateTopLevelBubbleMaxHeight() const {
const WorkAreaInsets* work_area = WorkAreaInsets::ForWindow(
holding_space_tray_->shelf()->GetWindow()->GetRootWindow());
const int bottom =
holding_space_tray_->shelf()->IsHorizontalAlignment()
? holding_space_tray_->shelf()->GetShelfBoundsInScreen().y()
: work_area->user_work_area_bounds().bottom();
const int free_space_height_above_anchor =
bottom - work_area->user_work_area_bounds().y();
const gfx::Insets insets =
GetTrayBubbleInsets(holding_space_tray_->GetBubbleWindowContainer());
const int bubble_vertical_margin = insets.top() + insets.bottom();
return free_space_height_above_anchor - bubble_vertical_margin;
}
int HoldingSpaceTrayBubble::CalculateChildBubbleContainerMaxHeight() const {
return CalculateTopLevelBubbleMaxHeight() -
(header_ ? header_->GetHeightForWidth(kTrayMenuWidth) : 0u);
}
void HoldingSpaceTrayBubble::UpdateBubbleBounds() {
child_bubble_container_->SetMaxHeight(
CalculateChildBubbleContainerMaxHeight());
bubble_wrapper_->bubble_view()->ChangeAnchorRect(
holding_space_tray_->shelf()->GetSystemTrayAnchorRect());
}
void HoldingSpaceTrayBubble::OnDidApplyDisplayChanges() {
UpdateBubbleBounds();
}
void HoldingSpaceTrayBubble::OnAutoHideStateChanged(ShelfAutoHideState state) {
UpdateBubbleBounds();
}
void HoldingSpaceTrayBubble::OnDisplayTabletStateChanged(
display::TabletState state) {
if (display::IsTabletStateChanging(state)) {
return;
}
UpdateBubbleBounds();
}
}