| // Copyright 2020 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/system/holding_space/holding_space_tray_bubble.h" |
| |
| #include <vector> |
| |
| #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/system/holding_space/holding_space_item_view.h" |
| #include "ash/system/holding_space/holding_space_tray.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 "ui/aura/env.h" |
| #include "ui/aura/window.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/gfx/animation/slide_animation.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/views/animation/animation_delegate_views.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/proposed_layout.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Animation. |
| constexpr base::TimeDelta kAnimationDuration = |
| base::TimeDelta::FromMilliseconds(167); |
| |
| // Helpers --------------------------------------------------------------------- |
| |
| // Finds all visible `HoldingSpaceItem`s in `parent`'s view hierarchy. |
| 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); |
| } |
| } |
| |
| // Records the time from first availability to first entry into holding space. |
| 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); |
| } |
| |
| // HoldingSpaceEventFilter ----------------------------------------------------- |
| class HoldingSpaceEventFilter : public ui::EventHandler { |
| public: |
| HoldingSpaceEventFilter(HoldingSpaceItemViewDelegate* delegate) |
| : delegate_(delegate) { |
| aura::Env::GetInstance()->AddPreTargetHandler( |
| this, ui::EventTarget::Priority::kSystem); |
| } |
| ~HoldingSpaceEventFilter() override { |
| aura::Env::GetInstance()->RemovePreTargetHandler(this); |
| } |
| HoldingSpaceEventFilter(const HoldingSpaceEventFilter&) = delete; |
| HoldingSpaceEventFilter& operator=(const HoldingSpaceEventFilter&) = delete; |
| |
| private: |
| // ui::EventHandler: |
| void OnKeyEvent(ui::KeyEvent* event) override { |
| if (event->type() == ui::ET_KEY_PRESSED && |
| delegate_->OnHoldingSpaceTrayKeyPressed(*event)) { |
| event->StopPropagation(); |
| } |
| return; |
| } |
| |
| HoldingSpaceItemViewDelegate* delegate_; |
| }; |
| |
| // ChildBubbleContainerLayout -------------------------------------------------- |
| // A class similar to a `views::LayoutManager` which supports calculating and |
| // applying `views::ProposedLayout`s. Views are laid out similar to a vertical |
| // `views::BoxLayout` with the first child flexing to cede layout space if the |
| // layout would otherwise exceed maximum height restrictions. Subsequent child |
| // views will be laid outside of `host` bounds if there is insufficient space. |
| class ChildBubbleContainerLayout { |
| public: |
| ChildBubbleContainerLayout(views::View* host, int child_spacing) |
| : host_(host), child_spacing_(child_spacing) {} |
| |
| // Sets the maximum height restriction for the layout. |
| void SetMaxHeight(int max_height) { max_height_ = max_height; } |
| |
| // Calculates and returns a `views::ProposedLayout` given current maximum |
| // height restrictions and the current state of the view hierarchy. Note that |
| // views are laid out similar to a vertical `views::BoxLayout` with the first |
| // child flexing to cede layout space if the layout would otherwise exceed |
| // maximum height restrictions. |
| views::ProposedLayout CalculateProposedLayout() const { |
| views::ProposedLayout layout; |
| layout.host_size = gfx::Size(kHoldingSpaceBubbleWidth, 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; |
| } |
| |
| // Apply child spacing. |
| 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 maximum height restrictions are present and preferred height exceeds |
| // maximum height, the first child view should cede layout space for others. |
| // Note that subsequent child views will still be given their preferred |
| // height so its possible they will be laid outside of `host_` view bounds. |
| 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(0, 0, 0, height_to_cede); |
| 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; |
| } |
| |
| // Applies the specified `layout` to the view hierarchy. |
| void ApplyLayout(const views::ProposedLayout& layout) { |
| for (const auto& child_layout : layout.child_layouts) |
| child_layout.child_view->SetBoundsRect(child_layout.bounds); |
| } |
| |
| views::View* const host_; |
| const int child_spacing_; |
| |
| // Maximum height restriction for the layout. If zero, it is assumed that |
| // there is no maximum height restriction. |
| int max_height_ = 0; |
| }; |
| |
| } // namespace |
| |
| // HoldingSpaceTrayBubble::ChildBubbleContainer -------------------------------- |
| |
| // The container for `HoldingSpaceTrayBubble` which parents its child bubbles |
| // and animates layout changes. Note that this view uses a pseudo layout manager |
| // to calculate bounds for its children, but animates any layout changes itself. |
| class HoldingSpaceTrayBubble::ChildBubbleContainer |
| : public views::View, |
| public views::AnimationDelegateViews { |
| public: |
| ChildBubbleContainer() |
| : views::AnimationDelegateViews(this), |
| layout_manager_(this, kHoldingSpaceBubbleContainerChildSpacing) {} |
| |
| // Sets the maximum height restriction for the layout. |
| void SetMaxHeight(int max_height) { |
| layout_manager_.SetMaxHeight(max_height); |
| PreferredSizeChanged(); |
| } |
| |
| // views::View: |
| int GetHeightForWidth(int width) const override { |
| DCHECK_EQ(width, kHoldingSpaceBubbleWidth); |
| if (current_layout_.host_size.IsEmpty()) |
| current_layout_ = layout_manager_.CalculateProposedLayout(); |
| return current_layout_.host_size.height(); |
| } |
| |
| 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_` is unchanged then a layout animation is in progress |
| // and the only thing needed is to propagate the event up the tree so that |
| // the widget will be resized and re-anchored. |
| if (target_layout == target_layout_) { |
| views::View::PreferredSizeChanged(); |
| return; |
| } |
| |
| // If `current_layout_` is empty then this is the first layout. Don't |
| // animate the first layout. |
| if (current_layout_.host_size.IsEmpty()) { |
| current_layout_ = target_layout_ = target_layout; |
| views::View::PreferredSizeChanged(); |
| return; |
| } |
| |
| start_layout_ = current_layout_; |
| target_layout_ = target_layout; |
| |
| // Animate changes from the `current_layout_` to the `target_layout_`. |
| layout_animation_ = std::make_unique<gfx::SlideAnimation>(this); |
| layout_animation_->SetSlideDuration( |
| ui::ScopedAnimationDurationScaleMode::duration_multiplier() * |
| kAnimationDuration); |
| layout_animation_->SetTweenType(gfx::Tween::Type::FAST_OUT_SLOW_IN); |
| layout_animation_->Show(); |
| } |
| |
| void Layout() override { layout_manager_.ApplyLayout(current_layout_); } |
| |
| // views::AnimationDelegateViews: |
| 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(); |
| } |
| |
| private: |
| // A pseudo layout manager which supports calculating and applying |
| // `views::ProposedLayouts`. It lays out views similarly to a vertical |
| // `views::BoxLayout` with the first view flexing to cede layout space to |
| // siblings if maximum height restrictions would otherwise be exceeded. |
| ChildBubbleContainerLayout layout_manager_; |
| |
| mutable views::ProposedLayout start_layout_; // Layout being animated from. |
| mutable views::ProposedLayout current_layout_; // Current layout. |
| mutable views::ProposedLayout target_layout_; // Layout being animated to. |
| |
| std::unique_ptr<gfx::SlideAnimation> layout_animation_; |
| }; |
| |
| // HoldingSpaceTrayBubble ------------------------------------------------------ |
| |
| HoldingSpaceTrayBubble::HoldingSpaceTrayBubble( |
| HoldingSpaceTray* holding_space_tray, |
| bool show_by_click) |
| : holding_space_tray_(holding_space_tray) { |
| TrayBubbleView::InitParams init_params; |
| init_params.delegate = holding_space_tray; |
| init_params.parent_window = holding_space_tray->GetBubbleWindowContainer(); |
| init_params.anchor_view = nullptr; |
| init_params.anchor_mode = TrayBubbleView::AnchorMode::kRect; |
| init_params.anchor_rect = |
| holding_space_tray->shelf()->GetSystemTrayAnchorRect(); |
| init_params.insets = GetTrayBubbleInsets(); |
| init_params.shelf_alignment = holding_space_tray->shelf()->alignment(); |
| init_params.preferred_width = kHoldingSpaceBubbleWidth; |
| init_params.close_on_deactivate = true; |
| init_params.show_by_click = show_by_click; |
| init_params.has_shadow = false; |
| |
| // Create and customize bubble view. |
| TrayBubbleView* bubble_view = new TrayBubbleView(init_params); |
| child_bubble_container_ = |
| bubble_view->AddChildView(std::make_unique<ChildBubbleContainer>()); |
| child_bubble_container_->SetMaxHeight(CalculateMaxHeight()); |
| |
| // Add pinned files child bubble. |
| child_bubbles_.push_back(child_bubble_container_->AddChildView( |
| std::make_unique<PinnedFilesBubble>(&delegate_))); |
| |
| // Add recent files child bubble. |
| child_bubbles_.push_back(child_bubble_container_->AddChildView( |
| std::make_unique<RecentFilesBubble>(&delegate_))); |
| |
| // Initialize child bubbles. |
| for (HoldingSpaceTrayChildBubble* child_bubble : child_bubbles_) |
| child_bubble->Init(); |
| |
| // Show the bubble. |
| bubble_wrapper_ = std::make_unique<TrayBubbleWrapper>( |
| holding_space_tray, bubble_view, false /* is_persistent */); |
| |
| // Set bubble frame to be invisible. |
| bubble_wrapper_->GetBubbleWidget() |
| ->non_client_view() |
| ->frame_view() |
| ->SetVisible(false); |
| |
| event_filter_ = std::make_unique<HoldingSpaceEventFilter>(&delegate_); |
| |
| PrefService* const prefs = |
| Shell::Get()->session_controller()->GetLastActiveUserPrefService(); |
| |
| // Mark when holding space was first entered. If this is not the first entry |
| // into holding space, this will no-op. If this is the first entry, record the |
| // amount of time from first availability to first entry into holding space. |
| if (holding_space_prefs::MarkTimeOfFirstEntry(prefs)) |
| RecordTimeFromFirstAvailabilityToFirstEntry(prefs); |
| |
| // Record visible holding space items. |
| std::vector<const HoldingSpaceItem*> visible_items; |
| FindVisibleHoldingSpaceItems(bubble_view, &visible_items); |
| holding_space_metrics::RecordItemCounts(visible_items); |
| |
| shelf_observer_.Add(holding_space_tray_->shelf()); |
| tablet_mode_observer_.Add(Shell::Get()->tablet_mode_controller()); |
| } |
| |
| HoldingSpaceTrayBubble::~HoldingSpaceTrayBubble() { |
| event_filter_.reset(); |
| bubble_wrapper_->bubble_view()->ResetDelegate(); |
| |
| // Explicitly reset child bubbles so that they will stop observing the holding |
| // space controller/model while they are asynchronously destroyed. |
| for (HoldingSpaceTrayChildBubble* child_bubble : child_bubbles_) |
| child_bubble->Reset(); |
| } |
| |
| void HoldingSpaceTrayBubble::AnchorUpdated() { |
| bubble_wrapper_->bubble_view()->UpdateBubble(); |
| } |
| |
| TrayBubbleView* HoldingSpaceTrayBubble::GetBubbleView() { |
| return bubble_wrapper_->bubble_view(); |
| } |
| |
| views::Widget* HoldingSpaceTrayBubble::GetBubbleWidget() { |
| return bubble_wrapper_->GetBubbleWidget(); |
| } |
| |
| int HoldingSpaceTrayBubble::CalculateMaxHeight() 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(); |
| const int bubble_vertical_margin = insets.top() + insets.bottom(); |
| |
| return free_space_height_above_anchor - bubble_vertical_margin; |
| } |
| |
| void HoldingSpaceTrayBubble::UpdateBubbleBounds() { |
| child_bubble_container_->SetMaxHeight(CalculateMaxHeight()); |
| bubble_wrapper_->bubble_view()->ChangeAnchorRect( |
| holding_space_tray_->shelf()->GetSystemTrayAnchorRect()); |
| } |
| |
| void HoldingSpaceTrayBubble::OnDisplayConfigurationChanged() { |
| UpdateBubbleBounds(); |
| } |
| |
| void HoldingSpaceTrayBubble::OnAutoHideStateChanged(ShelfAutoHideState state) { |
| UpdateBubbleBounds(); |
| } |
| |
| void HoldingSpaceTrayBubble::OnTabletModeStarted() { |
| UpdateBubbleBounds(); |
| } |
| |
| void HoldingSpaceTrayBubble::OnTabletModeEnded() { |
| UpdateBubbleBounds(); |
| } |
| |
| } // namespace ash |