| // Copyright 2019 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/shelf/scrollable_shelf_view.h" |
| |
| #include "ash/app_list/app_list_controller_impl.h" |
| #include "ash/public/cpp/presentation_time_recorder.h" |
| #include "ash/public/cpp/shelf_config.h" |
| #include "ash/screen_util.h" |
| #include "ash/shelf/shelf_app_button.h" |
| #include "ash/shelf/shelf_focus_cycler.h" |
| #include "ash/shelf/shelf_navigation_widget.h" |
| #include "ash/shelf/shelf_tooltip_manager.h" |
| #include "ash/shelf/shelf_widget.h" |
| #include "ash/shell.h" |
| #include "ash/system/status_area_widget.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/numerics/ranges.h" |
| #include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/animation_throughput_reporter.h" |
| #include "ui/compositor/paint_recorder.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| #include "ui/gfx/geometry/vector2d_conversions.h" |
| #include "ui/gfx/skia_paint_util.h" |
| #include "ui/gfx/transform_util.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/focus/focus_search.h" |
| #include "ui/views/view_targeter_delegate.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Padding between the arrow button and the end of ScrollableShelf. It is |
| // applied when the arrow button shows. |
| constexpr int kArrowButtonEndPadding = 6; |
| |
| // Padding between the the shelf container view and the arrow button (if any). |
| constexpr int kDistanceToArrowButton = 2; |
| |
| // Size of the arrow button. |
| constexpr int kArrowButtonSize = 20; |
| |
| // The distance between ShelfContainerView and the end of ScrollableShelf when |
| // the arrow button shows. |
| constexpr int kArrowButtonGroupWidth = |
| kArrowButtonSize + kArrowButtonEndPadding + kDistanceToArrowButton; |
| |
| // The gesture fling event with the velocity smaller than the threshold will be |
| // ignored. |
| constexpr int kGestureFlingVelocityThreshold = 1000; |
| |
| // Horizontal size of the tap areafor the overflow arrow button. |
| constexpr int kArrowButtonTapAreaHorizontal = 32; |
| |
| // Length of the fade in/out zone. |
| constexpr int kGradientZoneLength = 26; |
| |
| // Delay to show a new page of shelf icons. |
| constexpr base::TimeDelta kShelfPageFlipDelay = |
| base::TimeDelta::FromMilliseconds(500); |
| |
| // Histogram names for the scrollable shelf dragging metrics. |
| constexpr char kScrollDraggingTabletLauncherVisibleHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.TabletMode.LauncherVisible"; |
| constexpr char kScrollDraggingTabletLauncherVisibleMaxLatencyHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.MaxLatency.TabletMode." |
| "LauncherVisible"; |
| constexpr char kScrollDraggingTabletLauncherHiddenHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.TabletMode.LauncherHidden"; |
| constexpr char kScrollDraggingTabletLauncherHiddenMaxLatencyHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.MaxLatency.TabletMode." |
| "LauncherHidden"; |
| constexpr char kScrollDraggingClamshellLauncherVisibleHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.ClamshellMode.LauncherVisible"; |
| constexpr char kScrollDraggingClamshellLauncherVisibleMaxLatencyHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.MaxLatency.ClamshellMode." |
| "LauncherVisible"; |
| constexpr char kScrollDraggingClamshellLauncherHiddenHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.ClamshellMode.LauncherHidden"; |
| constexpr char kScrollDraggingClamshellLauncherHiddenMaxLatencyHistogram[] = |
| "Apps.ScrollableShelf.Drag.PresentationTime.MaxLatency.ClamshellMode." |
| "LauncherHidden"; |
| |
| // Histogram names for the scrollable shelf animation smoothness metrics. |
| constexpr char kAnimationSmoothnessHistogram[] = |
| "Apps.ScrollableShelf.AnimationSmoothness"; |
| constexpr char kAnimationSmoothnessTabletLauncherVisibleHistogram[] = |
| "Apps.ScrollableShelf.AnimationSmoothness.TabletMode.LauncherVisible"; |
| constexpr char kAnimationSmoothnessTabletLauncherHiddenHistogram[] = |
| "Apps.ScrollableShelf.AnimationSmoothness.TabletMode.LauncherHidden"; |
| constexpr char kAnimationSmoothnessClamshellLauncherVisibleHistogram[] = |
| "Apps.ScrollableShelf.AnimationSmoothness.ClamshellMode.LauncherVisible"; |
| constexpr char kAnimationSmoothnessClamshellLauncherHiddenHistogram[] = |
| "Apps.ScrollableShelf.AnimationSmoothness.ClamshellMode.LauncherHidden"; |
| |
| // Returns the display id for the display that shows the shelf for |view|. |
| int64_t GetDisplayIdForView(const views::View* view) { |
| aura::Window* window = view->GetWidget()->GetNativeWindow(); |
| return display::Screen::GetScreen()->GetDisplayNearestWindow(window).id(); |
| } |
| |
| void ReportSmoothness(bool tablet_mode, bool launcher_visible, int smoothness) { |
| base::UmaHistogramPercentageObsoleteDoNotUse(kAnimationSmoothnessHistogram, |
| smoothness); |
| if (tablet_mode) { |
| if (launcher_visible) { |
| base::UmaHistogramPercentageObsoleteDoNotUse( |
| kAnimationSmoothnessTabletLauncherVisibleHistogram, smoothness); |
| } else { |
| base::UmaHistogramPercentageObsoleteDoNotUse( |
| kAnimationSmoothnessTabletLauncherHiddenHistogram, smoothness); |
| } |
| } else { |
| if (launcher_visible) { |
| base::UmaHistogramPercentageObsoleteDoNotUse( |
| kAnimationSmoothnessClamshellLauncherVisibleHistogram, smoothness); |
| } else { |
| base::UmaHistogramPercentageObsoleteDoNotUse( |
| kAnimationSmoothnessClamshellLauncherHiddenHistogram, smoothness); |
| } |
| } |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // DragIconDropAnimationDelegate |
| |
| class ScrollableShelfView::DragIconDropAnimationDelegate |
| : public ui::ImplicitAnimationObserver { |
| public: |
| DragIconDropAnimationDelegate(views::View* original_view, |
| const gfx::Rect& src_bounds_in_screen, |
| const gfx::Rect& dst_bounds_in_screen, |
| views::UniqueWidgetPtr proxy_view_widget) |
| : original_view_(original_view), |
| src_bounds_in_screen_(src_bounds_in_screen), |
| dst_bounds_in_screen_(dst_bounds_in_screen), |
| proxy_view_widget_(std::move(proxy_view_widget)) {} |
| ~DragIconDropAnimationDelegate() override = default; |
| |
| DragIconDropAnimationDelegate(const DragIconDropAnimationDelegate&) = delete; |
| DragIconDropAnimationDelegate& operator=( |
| const DragIconDropAnimationDelegate&) = delete; |
| |
| void StartAnimation() { |
| ui::ScopedLayerAnimationSettings animation_settings( |
| proxy_view_widget_->GetContentsView()->layer()->GetAnimator()); |
| animation_settings.SetTweenType(gfx::Tween::FAST_OUT_LINEAR_IN); |
| animation_settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET); |
| animation_settings.AddObserver(this); |
| |
| proxy_view_widget_->GetContentsView()->layer()->SetTransform( |
| gfx::TransformBetweenRects(gfx::RectF(src_bounds_in_screen_), |
| gfx::RectF(dst_bounds_in_screen_))); |
| } |
| |
| // ui::ImplicitAnimationObserver: |
| void OnImplicitAnimationsCompleted() override { |
| StopObserving(); |
| |
| // Destructs the proxy image view and shows the original drag view at the |
| // end of animation. |
| original_view_->layer()->SetOpacity(1.0f); |
| proxy_view_widget_.reset(); |
| } |
| |
| private: |
| // Original app icon being dragged in ShelfView. |
| views::View* const original_view_ = nullptr; |
| |
| // Bounds of the DragImageView, aka the DragIconProxy created by the user of |
| // ApplicationDragAndDropHost. |
| gfx::Rect const src_bounds_in_screen_; |
| |
| // Bounds of the ShelfAppButton which DragImageView is imitating. |
| gfx::Rect const dst_bounds_in_screen_; |
| |
| // Placeholder icon representing |original_view_| that moves with the pointer |
| // while being dragged. |
| views::UniqueWidgetPtr proxy_view_widget_; |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // GradientLayerDelegate |
| |
| class ScrollableShelfView::GradientLayerDelegate : public ui::LayerDelegate { |
| public: |
| GradientLayerDelegate() : layer_(ui::LAYER_TEXTURED) { |
| layer_.set_delegate(this); |
| layer_.SetFillsBoundsOpaquely(false); |
| } |
| |
| ~GradientLayerDelegate() override { layer_.set_delegate(nullptr); } |
| |
| bool IsStartFadeZoneVisible() const { |
| return !start_fade_zone_.zone_rect.IsEmpty(); |
| } |
| bool IsEndFadeZoneVisible() const { |
| return !end_fade_zone_.zone_rect.IsEmpty(); |
| } |
| |
| void set_start_fade_zone(const FadeZone& fade_zone) { |
| start_fade_zone_ = fade_zone; |
| } |
| void set_end_fade_zone(const FadeZone& fade_zone) { |
| end_fade_zone_ = fade_zone; |
| } |
| gfx::Rect start_fade_zone_bounds() const { |
| return start_fade_zone_.zone_rect; |
| } |
| gfx::Rect end_fade_zone_bounds() const { return end_fade_zone_.zone_rect; } |
| ui::Layer* layer() { return &layer_; } |
| |
| private: |
| // ui::LayerDelegate: |
| void OnPaintLayer(const ui::PaintContext& context) override { |
| const gfx::Size size = layer()->size(); |
| |
| views::PaintInfo paint_info = |
| views::PaintInfo::CreateRootPaintInfo(context, size); |
| const auto& paint_recording_size = paint_info.paint_recording_size(); |
| |
| // Pass the scale factor when constructing PaintRecorder so the MaskLayer |
| // size is not incorrectly rounded (see https://crbug.com/921274). |
| ui::PaintRecorder recorder( |
| context, paint_info.paint_recording_size(), |
| static_cast<float>(paint_recording_size.width()) / size.width(), |
| static_cast<float>(paint_recording_size.height()) / size.height(), |
| nullptr); |
| |
| recorder.canvas()->DrawColor(SK_ColorBLACK, SkBlendMode::kSrc); |
| |
| if (!start_fade_zone_.zone_rect.IsEmpty()) |
| DrawFadeZone(start_fade_zone_, recorder.canvas()); |
| if (!end_fade_zone_.zone_rect.IsEmpty()) |
| DrawFadeZone(end_fade_zone_, recorder.canvas()); |
| } |
| void OnDeviceScaleFactorChanged(float old_device_scale_factor, |
| float new_device_scale_factor) override {} |
| |
| void DrawFadeZone(const FadeZone& fade_zone, gfx::Canvas* canvas) { |
| gfx::Point start_point; |
| gfx::Point end_point; |
| if (fade_zone.is_horizontal) { |
| start_point = gfx::Point(fade_zone.zone_rect.x(), 0); |
| end_point = gfx::Point(fade_zone.zone_rect.right(), 0); |
| } else { |
| start_point = gfx::Point(0, fade_zone.zone_rect.y()); |
| end_point = gfx::Point(0, fade_zone.zone_rect.bottom()); |
| } |
| |
| cc::PaintFlags flags; |
| flags.setBlendMode(SkBlendMode::kSrc); |
| flags.setAntiAlias(false); |
| |
| flags.setShader(gfx::CreateGradientShader( |
| start_point, end_point, |
| fade_zone.fade_in ? SK_ColorTRANSPARENT : SK_ColorBLACK, |
| fade_zone.fade_in ? SK_ColorBLACK : SK_ColorTRANSPARENT)); |
| |
| canvas->DrawRect(fade_zone.zone_rect, flags); |
| } |
| |
| ui::Layer layer_; |
| |
| FadeZone start_fade_zone_; |
| FadeZone end_fade_zone_; |
| |
| DISALLOW_COPY_AND_ASSIGN(GradientLayerDelegate); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ScrollableShelfArrowView |
| |
| class ScrollableShelfView::ScrollableShelfArrowView |
| : public ScrollArrowView, |
| public views::ViewTargeterDelegate { |
| public: |
| explicit ScrollableShelfArrowView(ArrowType arrow_type, |
| bool is_horizontal_alignment, |
| Shelf* shelf, |
| ShelfButtonDelegate* shelf_button_delegate) |
| : ScrollArrowView(arrow_type, |
| is_horizontal_alignment, |
| shelf, |
| shelf_button_delegate), |
| shelf_(shelf) { |
| SetInkDropMode(InkDropMode::OFF); |
| SetEventTargeter(std::make_unique<views::ViewTargeter>(this)); |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| |
| // When the spoken feedback is enabled, scrollable shelf should ensure that |
| // the hidden icon which receives the accessibility focus shows through |
| // scroll animation. So the arrow button is not useful for the spoken |
| // feedback users. The spoken feedback should ignore the arrow button. |
| GetViewAccessibility().OverrideIsIgnored(/*value=*/true); |
| } |
| ~ScrollableShelfArrowView() override = default; |
| |
| // views::ViewTargeterDelegate: |
| bool DoesIntersectRect(const views::View* target, |
| const gfx::Rect& rect) const override { |
| DCHECK_EQ(target, this); |
| const gfx::Rect bounds = gfx::Rect(size()); |
| |
| // Calculates the tapping area. Note that tapping area is bigger than the |
| // arrow button's bounds. |
| gfx::Rect tap_rect(kArrowButtonTapAreaHorizontal, |
| shelf_->hotseat_widget()->GetHotseatSize()); |
| tap_rect -= gfx::Vector2d((tap_rect.width() - bounds.width()) / 2, |
| (tap_rect.height() - bounds.height()) / 2); |
| DCHECK(tap_rect.Contains(bounds)); |
| |
| return tap_rect.Intersects(rect); |
| } |
| |
| // Make ScrollRectToVisible a no-op because ScrollableShelfArrowView is |
| // always visible/invisible depending on the layout strategy at fixed |
| // locations. So it does not need to be scrolled to show. |
| // TODO (andrewxu): Moves all of functions related with scrolling into |
| // ScrollableShelfContainerView. Then erase this empty function. |
| void ScrollRectToVisible(const gfx::Rect& rect) override {} |
| |
| const char* GetClassName() const override { |
| return "ScrollableShelfArrowView"; |
| } |
| |
| private: |
| Shelf* const shelf_ = nullptr; |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ScopedActiveInkDropCountImpl |
| |
| class ScrollableShelfView::ScopedActiveInkDropCountImpl |
| : public ScrollableShelfView::ScopedActiveInkDropCount { |
| public: |
| explicit ScopedActiveInkDropCountImpl(ScrollableShelfView* owner) |
| : owner_(owner) { |
| owner_->OnActiveInkDropChange(/*increase=*/true); |
| } |
| |
| ~ScopedActiveInkDropCountImpl() override { |
| owner_->OnActiveInkDropChange(/*increase=*/false); |
| } |
| |
| ScopedActiveInkDropCountImpl(const ScopedActiveInkDropCountImpl& rhs) = |
| delete; |
| ScopedActiveInkDropCountImpl& operator=( |
| const ScopedActiveInkDropCountImpl& rhs) = delete; |
| |
| private: |
| ScrollableShelfView* owner_ = nullptr; |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ScrollableShelfContainerView |
| |
| class ScrollableShelfContainerView : public ShelfContainerView, |
| public views::ViewTargeterDelegate { |
| public: |
| explicit ScrollableShelfContainerView( |
| ScrollableShelfView* scrollable_shelf_view) |
| : ShelfContainerView(scrollable_shelf_view->shelf_view()), |
| scrollable_shelf_view_(scrollable_shelf_view) { |
| SetEventTargeter(std::make_unique<views::ViewTargeter>(this)); |
| } |
| ~ScrollableShelfContainerView() override = default; |
| |
| // ShelfContainerView: |
| void TranslateShelfView(const gfx::Vector2dF& offset) override; |
| |
| private: |
| // views::View: |
| void Layout() override; |
| |
| // views::ViewTargeterDelegate: |
| bool DoesIntersectRect(const views::View* target, |
| const gfx::Rect& rect) const override; |
| |
| ScrollableShelfView* scrollable_shelf_view_ = nullptr; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScrollableShelfContainerView); |
| }; |
| |
| void ScrollableShelfContainerView::TranslateShelfView( |
| const gfx::Vector2dF& offset) { |
| ShelfContainerView::TranslateShelfView( |
| scrollable_shelf_view_->ShouldAdaptToRTL() ? -offset : offset); |
| } |
| |
| void ScrollableShelfContainerView::Layout() { |
| // Should not use ShelfView::GetPreferredSize in replace of |
| // CalculateIdealSize. Because ShelfView::CalculatePreferredSize relies on the |
| // bounds of app icon. Meanwhile, the icon's bounds may be updated by |
| // animation. |
| const gfx::Rect ideal_bounds = gfx::Rect(CalculatePreferredSize()); |
| |
| const gfx::Rect local_bounds = GetLocalBounds(); |
| gfx::Rect shelf_view_bounds = |
| local_bounds.Contains(ideal_bounds) ? local_bounds : ideal_bounds; |
| |
| if (shelf_view_->shelf()->IsHorizontalAlignment()) |
| shelf_view_bounds.set_x(ShelfConfig::Get()->GetAppIconEndPadding()); |
| else |
| shelf_view_bounds.set_y(ShelfConfig::Get()->GetAppIconEndPadding()); |
| |
| shelf_view_->SetBoundsRect(shelf_view_bounds); |
| } |
| |
| bool ScrollableShelfContainerView::DoesIntersectRect( |
| const views::View* target, |
| const gfx::Rect& rect) const { |
| // This view's layer is clipped. So the view should only handle the events |
| // within the area after cilp. |
| |
| gfx::RectF bounds = gfx::RectF(scrollable_shelf_view_->visible_space()); |
| views::View::ConvertRectToTarget(scrollable_shelf_view_, this, &bounds); |
| return ToEnclosedRect(bounds).Contains(rect); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ScrollableShelfFocusSearch |
| |
| class ScrollableShelfFocusSearch : public views::FocusSearch { |
| public: |
| explicit ScrollableShelfFocusSearch( |
| ScrollableShelfView* scrollable_shelf_view) |
| : FocusSearch(/*root=*/nullptr, |
| /*cycle=*/true, |
| /*accessibility_mode=*/true), |
| scrollable_shelf_view_(scrollable_shelf_view) {} |
| |
| ~ScrollableShelfFocusSearch() override = default; |
| |
| // views::FocusSearch |
| views::View* FindNextFocusableView( |
| views::View* starting_view, |
| FocusSearch::SearchDirection search_direction, |
| FocusSearch::TraversalDirection traversal_direction, |
| FocusSearch::StartingViewPolicy check_starting_view, |
| FocusSearch::AnchoredDialogPolicy can_go_into_anchored_dialog, |
| views::FocusTraversable** focus_traversable, |
| views::View** focus_traversable_view) override { |
| std::vector<views::View*> focusable_views; |
| ShelfView* shelf_view = scrollable_shelf_view_->shelf_view(); |
| |
| for (int i : shelf_view->visible_views_indices()) |
| focusable_views.push_back(shelf_view->view_model()->view_at(i)); |
| |
| int start_index = 0; |
| for (size_t i = 0; i < focusable_views.size(); ++i) { |
| if (focusable_views[i] == starting_view) { |
| start_index = i; |
| break; |
| } |
| } |
| |
| int new_index = |
| start_index + |
| (search_direction == FocusSearch::SearchDirection::kBackwards ? -1 : 1); |
| |
| // Scrolls to the new page if the focused shelf item is not tappable |
| // on the current page. |
| if (new_index < 0) |
| new_index = focusable_views.size() - 1; |
| else if (new_index >= static_cast<int>(focusable_views.size())) |
| new_index = 0; |
| else if (new_index < scrollable_shelf_view_->first_tappable_app_index()) |
| scrollable_shelf_view_->ScrollToNewPage(/*forward=*/false); |
| else if (new_index > scrollable_shelf_view_->last_tappable_app_index()) |
| scrollable_shelf_view_->ScrollToNewPage(/*forward=*/true); |
| |
| return focusable_views[new_index]; |
| } |
| |
| private: |
| ScrollableShelfView* scrollable_shelf_view_ = nullptr; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScrollableShelfFocusSearch); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ScrollableShelfView |
| |
| ScrollableShelfView::ScrollableShelfView(ShelfModel* model, Shelf* shelf) |
| : shelf_view_(new ShelfView(model, |
| shelf, |
| /*drag_and_drop_host=*/this, |
| /*shelf_button_delegate=*/this)), |
| page_flip_time_threshold_(kShelfPageFlipDelay) { |
| Shell::Get()->AddShellObserver(this); |
| ShelfConfig::Get()->AddObserver(this); |
| set_allow_deactivate_on_esc(true); |
| } |
| |
| ScrollableShelfView::~ScrollableShelfView() { |
| ShelfConfig::Get()->RemoveObserver(this); |
| Shell::Get()->RemoveShellObserver(this); |
| GetShelf()->tooltip()->set_shelf_tooltip_delegate(nullptr); |
| } |
| |
| void ScrollableShelfView::Init() { |
| // Although there is no animation for ScrollableShelfView, a layer is still |
| // needed. Otherwise, the child view without its own layer will be painted on |
| // RootView which is beneath |translucent_background_| in ShelfWidget. |
| // As a result, the child view will not show. |
| SetPaintToLayer(ui::LAYER_NOT_DRAWN); |
| layer()->SetFillsBoundsOpaquely(false); |
| |
| // Initialize the shelf container view. |
| // Note that |shelf_container_view_| should be under the arrow buttons. It |
| // ensures that the arrow button receives the tapping events which happen |
| // within the overlapping zone between the arrow button's tapping area and |
| // the bounds of |shelf_container_view_|. |
| shelf_container_view_ = |
| AddChildView(std::make_unique<ScrollableShelfContainerView>(this)); |
| shelf_container_view_->Initialize(); |
| |
| // Initialize the left arrow button. |
| left_arrow_ = AddChildView(std::make_unique<ScrollableShelfArrowView>( |
| ScrollArrowView::kLeft, GetShelf()->IsHorizontalAlignment(), GetShelf(), |
| this)); |
| |
| // Initialize the right arrow button. |
| right_arrow_ = AddChildView(std::make_unique<ScrollableShelfArrowView>( |
| ScrollArrowView::kRight, GetShelf()->IsHorizontalAlignment(), GetShelf(), |
| this)); |
| |
| gradient_layer_delegate_ = std::make_unique<GradientLayerDelegate>(); |
| layer()->SetMaskLayer(gradient_layer_delegate_->layer()); |
| |
| focus_search_ = std::make_unique<ScrollableShelfFocusSearch>(this); |
| |
| GetShelf()->tooltip()->set_shelf_tooltip_delegate(this); |
| |
| set_context_menu_controller(this); |
| |
| // Initializes |shelf_view_| after scrollable shelf view's children are |
| // initialized. |
| shelf_view_->Init(); |
| } |
| |
| void ScrollableShelfView::OnFocusRingActivationChanged(bool activated) { |
| if (activated) { |
| focus_ring_activated_ = true; |
| SetPaneFocusAndFocusDefault(); |
| force_show_hotseat_resetter_ = |
| GetShelf()->shelf_widget()->ForceShowHotseatInTabletMode(); |
| } else { |
| // Shows the gradient shader when the focus ring is disabled. |
| focus_ring_activated_ = false; |
| if (force_show_hotseat_resetter_) |
| force_show_hotseat_resetter_.RunAndReset(); |
| } |
| |
| MaybeUpdateGradientZone(); |
| } |
| |
| void ScrollableShelfView::ScrollToNewPage(bool forward) { |
| const float offset = CalculatePageScrollingOffset(forward, layout_strategy_); |
| if (GetShelf()->IsHorizontalAlignment()) |
| ScrollByXOffset(offset, /*animating=*/true); |
| else |
| ScrollByYOffset(offset, /*animating=*/true); |
| } |
| |
| views::FocusSearch* ScrollableShelfView::GetFocusSearch() { |
| return focus_search_.get(); |
| } |
| |
| views::FocusTraversable* ScrollableShelfView::GetFocusTraversableParent() { |
| return parent()->GetFocusTraversable(); |
| } |
| |
| views::View* ScrollableShelfView::GetFocusTraversableParentView() { |
| return this; |
| } |
| |
| views::View* ScrollableShelfView::GetDefaultFocusableChild() { |
| // Adapts |scroll_offset_| to show the view properly right after the focus |
| // ring is enabled. |
| |
| if (default_last_focusable_child_) { |
| ScrollToMainOffset(CalculateScrollUpperBound(GetSpaceForIcons()), |
| /*animating=*/true); |
| return FindLastFocusableChild(); |
| } else { |
| ScrollToMainOffset(/*target_offset=*/0.f, /*animating=*/true); |
| return FindFirstFocusableChild(); |
| } |
| } |
| |
| gfx::Rect ScrollableShelfView::GetHotseatBackgroundBounds() const { |
| return available_space_; |
| } |
| |
| bool ScrollableShelfView::ShouldAdaptToRTL() const { |
| return base::i18n::IsRTL() && GetShelf()->IsHorizontalAlignment(); |
| } |
| |
| bool ScrollableShelfView::NeedUpdateToTargetBounds() const { |
| return GetAvailableLocalBounds(/*use_target_bounds=*/true) != |
| GetAvailableLocalBounds(/*use_target_bounds=*/false); |
| } |
| |
| gfx::Rect ScrollableShelfView::GetTargetScreenBoundsOfItemIcon( |
| const ShelfID& id) const { |
| // Calculates the available space for child views based on the target bounds. |
| const gfx::Insets target_edge_padding = |
| CalculateEdgePadding(/*use_target_bounds=*/true); |
| gfx::Rect target_space = GetAvailableLocalBounds(/*use_target_bounds=*/true); |
| target_space.Inset(target_edge_padding); |
| |
| const int target_scroll_offset = |
| CalculateScrollOffsetForTargetAvailableSpace(target_space); |
| |
| gfx::Rect icon_bounds = shelf_view_->view_model()->ideal_bounds( |
| shelf_view_->model()->ItemIndexByID(id)); |
| |
| icon_bounds.Offset(target_edge_padding.left() - edge_padding_insets_.left(), |
| 0); |
| |
| // Transforms |icon_bounds| from shelf view's coordinates to scrollable shelf |
| // view's coordinates manually. |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| const int shelf_view_offset = ShelfConfig::Get()->GetAppIconEndPadding(); |
| const int shelf_view_container_offset = |
| is_horizontal_alignment ? shelf_container_view_->bounds().x() |
| : shelf_container_view_->bounds().y(); |
| const int delta = |
| -target_scroll_offset + shelf_view_container_offset + shelf_view_offset; |
| const gfx::Vector2d bounds_offset = is_horizontal_alignment |
| ? gfx::Vector2d(delta, 0) |
| : gfx::Vector2d(0, delta); |
| icon_bounds.Offset(bounds_offset); |
| |
| // If the icon is invisible under the target view bounds, replaces the actual |
| // icon's bounds with the rectangle centering on the edge of |target_space|. |
| const gfx::Point icon_bounds_center = icon_bounds.CenterPoint(); |
| if (icon_bounds_center.x() > target_space.right()) { |
| icon_bounds.Offset(target_space.right_center().OffsetFromOrigin() - |
| icon_bounds_center.OffsetFromOrigin()); |
| } else if (icon_bounds_center.x() < target_space.x()) { |
| icon_bounds.Offset(target_space.left_center().OffsetFromOrigin() - |
| icon_bounds_center.OffsetFromOrigin()); |
| } |
| |
| // Hotseat's target bounds may differ from the actual bounds. So it has to |
| // transform the bounds manually from view's local coordinates to screen. |
| // Notes that the target bounds stored in shelf layout manager are adapted to |
| // RTL already while |icon_bounds| are not adjusted to RTL yet. |
| gfx::Rect hotseat_bounds_in_screen = |
| GetShelf()->hotseat_widget()->GetTargetBounds(); |
| if (ShouldAdaptToRTL()) { |
| // One simple way for transformation under RTL is: (1) Transforms hotseat |
| // target bounds from RTL to LTR. (2) Calculates the icon's bounds in screen |
| // under LTR. (3) Transforms the icon's bounds to RTL. |
| gfx::Rect display_bounds = |
| display::Screen::GetScreen() |
| ->GetDisplayNearestWindow(GetWidget()->GetNativeView()) |
| .bounds(); |
| hotseat_bounds_in_screen.set_x(display_bounds.right() - |
| hotseat_bounds_in_screen.right()); |
| icon_bounds.Offset(hotseat_bounds_in_screen.OffsetFromOrigin()); |
| icon_bounds.set_x(display_bounds.right() - icon_bounds.right()); |
| } else { |
| icon_bounds.Offset(hotseat_bounds_in_screen.OffsetFromOrigin()); |
| } |
| |
| return icon_bounds; |
| } |
| |
| bool ScrollableShelfView::RequiresScrollingForItemSize( |
| const gfx::Size& target_size, |
| int button_size) const { |
| const gfx::Size icons_preferred_size = |
| shelf_container_view_->CalculateIdealSize(button_size); |
| return !CanFitAllAppsWithoutScrolling(target_size, icons_preferred_size); |
| } |
| |
| void ScrollableShelfView::SetEdgePaddingInsets( |
| const gfx::Insets& padding_insets) { |
| edge_padding_insets_ = padding_insets; |
| shelf_view_->LayoutIfAppIconsOffsetUpdates(); |
| } |
| |
| gfx::Insets ScrollableShelfView::CalculateEdgePadding( |
| bool use_target_bounds) const { |
| // Tries display centering strategy. |
| const gfx::Insets display_centering_edge_padding = |
| CalculatePaddingForDisplayCentering(use_target_bounds); |
| if (!display_centering_edge_padding.IsEmpty()) { |
| // Returns early if the value is legal. |
| return display_centering_edge_padding; |
| } |
| |
| const int icons_size = |
| shelf_view_->GetSizeOfAppButtons(shelf_view_->number_of_visible_apps(), |
| shelf_view_->GetButtonSize()) + |
| 2 * ShelfConfig::Get()->GetAppIconEndPadding(); |
| |
| const gfx::Rect available_local_bounds = |
| GetAvailableLocalBounds(use_target_bounds); |
| const int available_size_for_app_icons = GetShelf()->PrimaryAxisValue( |
| available_local_bounds.width(), available_local_bounds.height()); |
| |
| int gap = CanFitAllAppsWithoutScrolling(available_local_bounds.size(), |
| CalculatePreferredSize()) |
| ? available_size_for_app_icons - icons_size |
| : 0; // overflow |
| |
| // Calculates the paddings before/after the visible area of scrollable shelf. |
| // |after_padding| being zero ensures that the available space after the |
| // visible area is filled first. |
| const int before_padding = gap; |
| const int after_padding = 0; |
| |
| gfx::Insets padding_insets; |
| if (GetShelf()->IsHorizontalAlignment()) { |
| padding_insets = |
| gfx::Insets(/*top=*/0, before_padding, /*bottom=*/0, after_padding); |
| } else { |
| padding_insets = |
| gfx::Insets(before_padding, /*left=*/0, after_padding, /*right=*/0); |
| } |
| |
| return padding_insets; |
| } |
| |
| views::View* ScrollableShelfView::GetShelfContainerViewForTest() { |
| return shelf_container_view_; |
| } |
| |
| bool ScrollableShelfView::ShouldAdjustForTest() const { |
| return CalculateAdjustmentOffset(CalculateMainAxisScrollDistance(), |
| layout_strategy_, GetSpaceForIcons()); |
| } |
| |
| void ScrollableShelfView::SetTestObserver(TestObserver* test_observer) { |
| DCHECK(!(test_observer && test_observer_)); |
| |
| test_observer_ = test_observer; |
| } |
| |
| bool ScrollableShelfView::IsAnyCornerButtonInkDropActivatedForTest() const { |
| return activated_corner_buttons_ > 0; |
| } |
| |
| float ScrollableShelfView::GetScrollUpperBoundForTest() const { |
| return CalculateScrollUpperBound(GetSpaceForIcons()); |
| } |
| |
| int ScrollableShelfView::GetSumOfButtonSizeAndSpacing() const { |
| return shelf_view_->GetButtonSize() + ShelfConfig::Get()->button_spacing(); |
| } |
| |
| int ScrollableShelfView::GetGestureDragThreshold() const { |
| return shelf_view_->GetButtonSize() / 2; |
| } |
| |
| float ScrollableShelfView::CalculateScrollUpperBound( |
| int available_space_for_icons) const { |
| if (layout_strategy_ == kNotShowArrowButtons) |
| return 0.f; |
| |
| return std::max( |
| 0, CalculateShelfIconsPreferredLength() - available_space_for_icons); |
| } |
| |
| float ScrollableShelfView::CalculateClampedScrollOffset( |
| float scroll, |
| int available_space_for_icons) const { |
| const float scroll_upper_bound = |
| CalculateScrollUpperBound(available_space_for_icons); |
| scroll = base::ClampToRange(scroll, 0.0f, scroll_upper_bound); |
| return scroll; |
| } |
| |
| void ScrollableShelfView::StartShelfScrollAnimation(float scroll_distance) { |
| const gfx::Vector2dF scroll_offset_before_update = scroll_offset_; |
| UpdateScrollOffset(scroll_distance); |
| |
| if (scroll_offset_before_update == scroll_offset_) |
| return; |
| |
| StopObservingImplicitAnimations(); |
| |
| during_scroll_animation_ = true; |
| MaybeUpdateGradientZone(); |
| |
| // In tablet mode, if the target layout only has one arrow button, enable the |
| // rounded corners of the shelf container layer in order to cut off the icons |
| // outside of the hotseat background. |
| const bool one_arrow_in_target_state = |
| (layout_strategy_ == LayoutStrategy::kShowLeftArrowButton || |
| layout_strategy_ == LayoutStrategy::kShowRightArrowButton); |
| if (one_arrow_in_target_state) |
| EnableShelfRoundedCorners(/*enable=*/true); |
| |
| ui::ScopedLayerAnimationSettings animation_settings( |
| shelf_view_->layer()->GetAnimator()); |
| animation_settings.SetTweenType(gfx::Tween::EASE_OUT); |
| animation_settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET); |
| animation_settings.AddObserver(this); |
| |
| ui::AnimationThroughputReporter reporter( |
| animation_settings.GetAnimator(), |
| metrics_util::ForSmoothness( |
| base::BindRepeating(&ReportSmoothness, Shell::Get()->IsInTabletMode(), |
| Shell::Get()->app_list_controller()->IsVisible( |
| GetDisplayIdForView(this))))); |
| |
| shelf_container_view_->TranslateShelfView(scroll_offset_); |
| } |
| |
| ScrollableShelfView::LayoutStrategy |
| ScrollableShelfView::CalculateLayoutStrategy(float scroll_distance_on_main_axis, |
| int available_length) const { |
| if (available_length >= CalculateShelfIconsPreferredLength()) { |
| return kNotShowArrowButtons; |
| } |
| |
| if (scroll_distance_on_main_axis == 0.f) { |
| // No invisible shelf buttons at the left side. So hide the left button. |
| return kShowRightArrowButton; |
| } |
| |
| if (scroll_distance_on_main_axis == |
| CalculateScrollUpperBound(available_length)) { |
| // If there is no invisible shelf button at the right side, hide the right |
| // button. |
| return kShowLeftArrowButton; |
| } |
| |
| // There are invisible shelf buttons at both sides. So show two buttons. |
| return kShowButtons; |
| } |
| |
| Shelf* ScrollableShelfView::GetShelf() { |
| return const_cast<Shelf*>( |
| const_cast<const ScrollableShelfView*>(this)->GetShelf()); |
| } |
| |
| const Shelf* ScrollableShelfView::GetShelf() const { |
| return shelf_view_->shelf(); |
| } |
| |
| gfx::Size ScrollableShelfView::CalculatePreferredSize() const { |
| return shelf_container_view_->GetPreferredSize(); |
| } |
| |
| void ScrollableShelfView::Layout() { |
| gfx::Rect shelf_container_bounds = gfx::Rect(size()); |
| |
| // Transpose and layout as if it is horizontal. |
| const bool is_horizontal = GetShelf()->IsHorizontalAlignment(); |
| if (!is_horizontal) |
| shelf_container_bounds.Transpose(); |
| |
| gfx::Size arrow_button_size(kArrowButtonSize, |
| shelf_container_bounds.height()); |
| gfx::Size arrow_button_group_size(kArrowButtonGroupWidth, |
| shelf_container_bounds.height()); |
| |
| // The bounds of |left_arrow_| and |right_arrow_| are in the |
| // ScrollableShelfView's local coordinates. |
| gfx::Rect left_arrow_bounds; |
| gfx::Rect right_arrow_bounds; |
| |
| const int before_padding = |
| is_horizontal ? edge_padding_insets_.left() : edge_padding_insets_.top(); |
| const int after_padding = is_horizontal ? edge_padding_insets_.right() |
| : edge_padding_insets_.bottom(); |
| |
| // Calculates the bounds of the left arrow button. If the left arrow button |
| // should not show, |left_arrow_bounds| should be empty. |
| if (layout_strategy_ == kShowLeftArrowButton || |
| layout_strategy_ == kShowButtons) { |
| gfx::Point left_arrow_start_point(shelf_container_bounds.x(), 0); |
| left_arrow_bounds = |
| gfx::Rect(left_arrow_start_point, arrow_button_group_size); |
| left_arrow_bounds.Offset(before_padding, 0); |
| left_arrow_bounds.Inset(kArrowButtonEndPadding, 0, kDistanceToArrowButton, |
| 0); |
| left_arrow_bounds.ClampToCenteredSize(arrow_button_size); |
| } |
| |
| if (layout_strategy_ == kShowRightArrowButton || |
| layout_strategy_ == kShowButtons) { |
| gfx::Point right_arrow_start_point( |
| shelf_container_bounds.right() - after_padding - kArrowButtonGroupWidth, |
| 0); |
| right_arrow_bounds = |
| gfx::Rect(right_arrow_start_point, arrow_button_group_size); |
| right_arrow_bounds.Inset(kDistanceToArrowButton, 0, kArrowButtonEndPadding, |
| 0); |
| right_arrow_bounds.ClampToCenteredSize(arrow_button_size); |
| } |
| |
| // Adjust the bounds when not showing in the horizontal |
| // alignment.tShelf()->IsHorizontalAlignment()) { |
| if (!is_horizontal) { |
| left_arrow_bounds.Transpose(); |
| right_arrow_bounds.Transpose(); |
| shelf_container_bounds.Transpose(); |
| } |
| |
| // Layout |left_arrow_| if it should show. |
| left_arrow_->SetVisible(!left_arrow_bounds.IsEmpty()); |
| left_arrow_->SetBoundsRect(left_arrow_bounds); |
| |
| // Layout |right_arrow_| if it should show. |
| right_arrow_->SetVisible(!right_arrow_bounds.IsEmpty()); |
| right_arrow_->SetBoundsRect(right_arrow_bounds); |
| |
| // Layer::Clone(), which may be triggered by screen rotation, does not copy |
| // the mask layer. So we may need to reset the mask layer. |
| if (ShouldApplyMaskLayerGradientZone() && !layer()->layer_mask_layer()) { |
| DCHECK(!gradient_layer_delegate_->layer()->layer_mask_back_link()); |
| layer()->SetMaskLayer(gradient_layer_delegate_->layer()); |
| } |
| |
| MaybeUpdateGradientZone(); |
| |
| // Layout |shelf_container_view_|. |
| shelf_container_view_->SetBoundsRect(shelf_container_bounds); |
| |
| EnableLayerClipOnShelfContainerView(ShouldEnableLayerClip()); |
| } |
| |
| void ScrollableShelfView::ChildPreferredSizeChanged(views::View* child) { |
| // Add/remove a shelf icon may change the layout strategy. |
| UpdateAvailableSpaceAndScroll(); |
| Layout(); |
| } |
| |
| void ScrollableShelfView::OnScrollEvent(ui::ScrollEvent* event) { |
| if (event->finger_count() != 2) |
| return; |
| if (ShouldDelegateScrollToShelf(*event)) { |
| ui::MouseWheelEvent wheel(*event); |
| GetShelf()->ProcessMouseWheelEvent(&wheel, /*from_touchpad=*/true); |
| event->StopPropagation(); |
| } |
| } |
| |
| void ScrollableShelfView::OnMouseEvent(ui::MouseEvent* event) { |
| if (event->IsMouseWheelEvent()) { |
| HandleMouseWheelEvent(event->AsMouseWheelEvent()); |
| return; |
| } |
| |
| // The mouse event's location may be outside of ShelfView but within the |
| // bounds of the ScrollableShelfView. Meanwhile, ScrollableShelfView should |
| // handle the mouse event consistently with ShelfView. To achieve this, |
| // we simply redirect |event| to ShelfView. |
| gfx::Point location_in_shelf_view = event->location(); |
| View::ConvertPointToTarget(this, shelf_view_, &location_in_shelf_view); |
| event->set_location(location_in_shelf_view); |
| shelf_view_->OnMouseEvent(event); |
| } |
| |
| void ScrollableShelfView::OnGestureEvent(ui::GestureEvent* event) { |
| if (ShouldHandleGestures(*event) && ProcessGestureEvent(*event)) { |
| // |event| is consumed by ScrollableShelfView. |
| event->SetHandled(); |
| } else if (shelf_view_->HandleGestureEvent(event)) { |
| // |event| is consumed by ShelfView. |
| event->StopPropagation(); |
| } else if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN) { |
| // |event| is consumed by neither ScrollableShelfView nor ShelfView. So the |
| // gesture end event will not be propagated to this view. Then we need to |
| // reset the class members related with scroll status explicitly. |
| ResetScrollStatus(); |
| } |
| } |
| |
| void ScrollableShelfView::GetAccessibleNodeData(ui::AXNodeData* node_data) { |
| GetViewAccessibility().OverrideNextFocus(GetShelf()->GetStatusAreaWidget()); |
| GetViewAccessibility().OverridePreviousFocus( |
| GetShelf()->shelf_widget()->navigation_widget()); |
| } |
| |
| void ScrollableShelfView::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| if (ShouldApplyMaskLayerGradientZone() && |
| gradient_layer_delegate_->layer()->bounds() != layer()->bounds()) { |
| gradient_layer_delegate_->layer()->SetBounds(layer()->bounds()); |
| } |
| |
| const gfx::Insets old_edge_padding_insets = edge_padding_insets_; |
| const gfx::Vector2dF old_scroll_offset = scroll_offset_; |
| |
| // The changed view bounds may lead to update on the available space. |
| UpdateAvailableSpaceAndScroll(); |
| |
| // Relayout shelf items if the preferred padding changed. |
| if (old_edge_padding_insets != edge_padding_insets_) |
| shelf_view_->OnBoundsChanged(shelf_view_->GetBoundsInScreen()); |
| |
| // Avoids calling AdjustOffset() when the scrollable shelf view is |
| // under scroll along the main axis. Otherwise, animation will conflict with |
| // scroll gesture. Meanwhile, translates the shelf view |
| // if AdjustOffset() returns false since when AdjustOffset() returns true, |
| // shelf view is scrolled by animation. |
| const bool should_translate_shelf_view = |
| scroll_status_ == kAlongMainAxisScroll || !AdjustOffset(); |
| |
| if (should_translate_shelf_view && old_scroll_offset != scroll_offset_) |
| shelf_container_view_->TranslateShelfView(scroll_offset_); |
| } |
| |
| void ScrollableShelfView::ViewHierarchyChanged( |
| const views::ViewHierarchyChangedDetails& details) { |
| if (details.parent != shelf_view_) |
| return; |
| |
| shelf_view_->UpdateShelfItemViewsVisibility(); |
| |
| // When app scaling state needs update, hotseat bounds should change. Then |
| // it is not meaningful to do further work in the current view bounds. So |
| // returns early. |
| if (GetShelf()->hotseat_widget()->UpdateTargetHotseatDensityIfNeeded()) |
| return; |
| |
| const gfx::Vector2dF old_scroll_offset = scroll_offset_; |
| |
| // Adding/removing an icon may change the padding then affect the available |
| // space. |
| UpdateAvailableSpaceAndScroll(); |
| |
| if (old_scroll_offset != scroll_offset_) |
| shelf_container_view_->TranslateShelfView(scroll_offset_); |
| } |
| |
| void ScrollableShelfView::ScrollRectToVisible(const gfx::Rect& rect) { |
| // Transform |rect| to local view coordinates taking |scroll_offset_| into |
| // consideration. |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| gfx::Rect rect_after_adjustment = rect; |
| if (is_horizontal_alignment) |
| rect_after_adjustment.Offset(-scroll_offset_.x(), 0); |
| else |
| rect_after_adjustment.Offset(0, -scroll_offset_.y()); |
| |
| // Notes that |rect| is not mirrored under RTL while |visible_space_| has been |
| // mirrored. It is easier for coding if we mirror |visible_space_| back and |
| // then do the calculation. |
| const gfx::Rect visible_space_without_RTL = GetMirroredRect(visible_space_); |
| |
| // |rect_after_adjustment| is already shown completely. So scroll is not |
| // needed. |
| if (visible_space_without_RTL.Contains(rect_after_adjustment)) { |
| AdjustOffset(); |
| return; |
| } |
| |
| const float original_offset = CalculateMainAxisScrollDistance(); |
| |
| // |forward| indicates the scroll direction. |
| const bool forward = |
| is_horizontal_alignment |
| ? rect_after_adjustment.right() > visible_space_without_RTL.right() |
| : rect_after_adjustment.bottom() > visible_space_without_RTL.bottom(); |
| |
| // Scrolling |shelf_view_| has the following side-effects: |
| // (1) May change the layout strategy. |
| // (2) May change the visible space. |
| // (3) Must change the scrolling offset. |
| // (4) Must change |rect_after_adjustment|'s coordinates after adjusting the |
| // scroll. |
| LayoutStrategy layout_strategy_after_scroll = layout_strategy_; |
| float main_axis_offset_after_scroll = original_offset; |
| gfx::Rect visible_space_after_scroll = visible_space_without_RTL; |
| gfx::Rect rect_after_scroll = rect_after_adjustment; |
| |
| // In each iteration, it scrolls |shelf_view_| to the neighboring page. |
| // Terminating the loop iteration if: |
| // (1) Find the suitable page which shows |rect| completely. |
| // (2) Cannot scroll |shelf_view_| anymore (it may happen with ChromeVox |
| // enabled). |
| while (!visible_space_after_scroll.Contains(rect_after_scroll)) { |
| int page_scroll_distance = |
| CalculatePageScrollingOffset(forward, layout_strategy_after_scroll); |
| |
| // Breaking the while loop if it cannot scroll anymore. |
| if (!page_scroll_distance) |
| break; |
| |
| main_axis_offset_after_scroll = CalculateTargetOffsetAfterScroll( |
| main_axis_offset_after_scroll, page_scroll_distance); |
| layout_strategy_after_scroll = CalculateLayoutStrategy( |
| main_axis_offset_after_scroll, GetSpaceForIcons()); |
| visible_space_after_scroll = |
| GetMirroredRect(CalculateVisibleSpace(layout_strategy_after_scroll)); |
| rect_after_scroll = rect_after_adjustment; |
| const int offset_delta = main_axis_offset_after_scroll - original_offset; |
| if (is_horizontal_alignment) |
| rect_after_scroll.Offset(-offset_delta, 0); |
| else |
| rect_after_scroll.Offset(0, -offset_delta); |
| } |
| |
| if (!visible_space_after_scroll.Contains(rect_after_scroll)) |
| return; |
| |
| ScrollToMainOffset(main_axis_offset_after_scroll, /*animating=*/true); |
| } |
| |
| std::unique_ptr<ui::Layer> ScrollableShelfView::RecreateLayer() { |
| layer()->SetMaskLayer(nullptr); |
| return views::View::RecreateLayer(); |
| } |
| |
| const char* ScrollableShelfView::GetClassName() const { |
| return "ScrollableShelfView"; |
| } |
| |
| void ScrollableShelfView::OnShelfButtonAboutToRequestFocusFromTabTraversal( |
| ShelfButton* button, |
| bool reverse) { |
| if ((button == left_arrow_) || (button == right_arrow_)) |
| return; |
| |
| shelf_view_->OnShelfButtonAboutToRequestFocusFromTabTraversal(button, |
| reverse); |
| ShelfWidget* shelf_widget = GetShelf()->shelf_widget(); |
| // In tablet mode, when the hotseat is not extended but one of the buttons |
| // gets focused, it should update the visibility of the hotseat. |
| if (Shell::Get()->IsInTabletMode() && |
| !shelf_widget->hotseat_widget()->IsExtended()) { |
| shelf_widget->shelf_layout_manager()->UpdateVisibilityState(); |
| } |
| } |
| |
| void ScrollableShelfView::ButtonPressed(views::Button* sender, |
| const ui::Event& event, |
| views::InkDrop* ink_drop) { |
| if ((sender == left_arrow_) || (sender == right_arrow_)) { |
| ScrollToNewPage(sender == right_arrow_); |
| return; |
| } |
| |
| shelf_view_->ButtonPressed(sender, event, ink_drop); |
| } |
| |
| void ScrollableShelfView::HandleAccessibleActionScrollToMakeVisible( |
| ShelfButton* button) { |
| // Scrollable shelf can only be hidden in tablet mode. |
| GetShelf()->hotseat_widget()->set_manually_extended(true); |
| GetShelf()->shelf_widget()->shelf_layout_manager()->UpdateVisibilityState(); |
| } |
| |
| std::unique_ptr<ScrollableShelfView::ScopedActiveInkDropCount> |
| ScrollableShelfView::CreateScopedActiveInkDropCount(const ShelfButton* sender) { |
| if (!ShouldCountActivatedInkDrop(sender)) |
| return nullptr; |
| |
| return std::make_unique<ScopedActiveInkDropCountImpl>(this); |
| } |
| |
| void ScrollableShelfView::ShowContextMenuForViewImpl( |
| views::View* source, |
| const gfx::Point& point, |
| ui::MenuSourceType source_type) { |
| // |point| is in screen coordinates. So it does not need to transform. |
| shelf_view_->ShowContextMenuForViewImpl(shelf_view_, point, source_type); |
| } |
| |
| void ScrollableShelfView::OnShelfAlignmentChanged( |
| aura::Window* root_window, |
| ShelfAlignment old_alignment) { |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| left_arrow_->set_is_horizontal_alignment(is_horizontal_alignment); |
| right_arrow_->set_is_horizontal_alignment(is_horizontal_alignment); |
| scroll_offset_ = gfx::Vector2dF(); |
| ScrollToMainOffset(CalculateMainAxisScrollDistance(), /*animating=*/false); |
| Layout(); |
| } |
| |
| void ScrollableShelfView::OnShelfConfigUpdated() { |
| UpdateAvailableSpaceAndScroll(); |
| shelf_view_->OnShelfConfigUpdated(); |
| } |
| |
| bool ScrollableShelfView::ShouldShowTooltipForView( |
| const views::View* view) const { |
| if (!view || !view->parent()) |
| return false; |
| |
| if (view == left_arrow_ || view == right_arrow_) |
| return true; |
| |
| if (view->parent() != shelf_view_) |
| return false; |
| |
| // The shelf item corresponding to |view| may have been removed from the |
| // model. |
| if (!shelf_view_->ShouldShowTooltipForChildView(view)) |
| return false; |
| |
| const gfx::Rect screen_bounds = view->GetBoundsInScreen(); |
| gfx::Rect visible_bounds_in_screen = visible_space_; |
| views::View::ConvertRectToScreen(this, &visible_bounds_in_screen); |
| |
| return visible_bounds_in_screen.Contains(screen_bounds); |
| } |
| |
| bool ScrollableShelfView::ShouldHideTooltip( |
| const gfx::Point& cursor_location) const { |
| if ((ShouldShowLeftArrow() && |
| left_arrow_->GetMirroredBounds().Contains(cursor_location)) || |
| (ShouldShowRightArrow() && |
| right_arrow_->GetMirroredBounds().Contains(cursor_location))) { |
| return false; |
| } |
| |
| // Should hide the tooltip if |cursor_location| is not in |visible_space_|. |
| if (!visible_space_.Contains(cursor_location)) |
| return true; |
| |
| gfx::Point location_in_shelf_view = cursor_location; |
| views::View::ConvertPointToTarget(this, shelf_view_, &location_in_shelf_view); |
| return shelf_view_->ShouldHideTooltip(location_in_shelf_view); |
| } |
| |
| const std::vector<aura::Window*> ScrollableShelfView::GetOpenWindowsForView( |
| views::View* view) { |
| if (!view || view->parent() != shelf_view_) |
| return std::vector<aura::Window*>(); |
| |
| return shelf_view_->GetOpenWindowsForView(view); |
| } |
| |
| base::string16 ScrollableShelfView::GetTitleForView( |
| const views::View* view) const { |
| if (!view || !view->parent()) |
| return base::string16(); |
| |
| if (view->parent() == shelf_view_) |
| return shelf_view_->GetTitleForView(view); |
| |
| if (view == left_arrow_) |
| return l10n_util::GetStringUTF16(IDS_SHELF_PREVIOUS); |
| |
| if (view == right_arrow_) |
| return l10n_util::GetStringUTF16(IDS_SHELF_NEXT); |
| |
| return base::string16(); |
| } |
| |
| views::View* ScrollableShelfView::GetViewForEvent(const ui::Event& event) { |
| if (event.target() == GetWidget()->GetNativeWindow()) |
| return this; |
| |
| return nullptr; |
| } |
| |
| bool ScrollableShelfView::ShouldStartDrag( |
| const std::string& app_id, |
| const gfx::Point& location_in_screen_coordinates) const { |
| return false; |
| } |
| |
| void ScrollableShelfView::CreateDragIconProxyByLocationWithNoAnimation( |
| const gfx::Point& origin_in_screen_coordinates, |
| const gfx::ImageSkia& icon, |
| views::View* replaced_view, |
| float scale_factor, |
| int blur_radius) { |
| drag_icon_widget_ = |
| DragImageView::Create(GetWidget()->GetNativeWindow()->GetRootWindow(), |
| ui::mojom::DragEventSource::kMouse); |
| DragImageView* drag_icon = |
| static_cast<DragImageView*>(drag_icon_widget_->GetContentsView()); |
| drag_icon->SetImage(icon); |
| const gfx::Rect replaced_view_screen_bounds = |
| replaced_view->GetBoundsInScreen(); |
| drag_icon->SetBoundsInScreen(replaced_view_screen_bounds); |
| drag_icon_widget_->SetVisibilityAnimationTransition( |
| views::Widget::ANIMATE_NONE); |
| drag_icon->SetWidgetVisible(true); |
| drag_icon->SetPaintToLayer(); |
| drag_icon->layer()->SetFillsBoundsOpaquely(false); |
| } |
| |
| void ScrollableShelfView::UpdateDragIconProxy( |
| const gfx::Point& location_in_screen_coordinates) { |
| static_cast<DragImageView*>(drag_icon_widget_->GetContentsView()) |
| ->SetScreenPosition(location_in_screen_coordinates); |
| |
| if (IsDragIconWithinVisibleSpace()) { |
| page_flip_timer_.AbandonAndStop(); |
| return; |
| } |
| |
| if (!page_flip_timer_.IsRunning()) { |
| page_flip_timer_.Start(FROM_HERE, page_flip_time_threshold_, this, |
| &ScrollableShelfView::OnPageFlipTimer); |
| } |
| } |
| |
| void ScrollableShelfView::DestroyDragIconProxy() { |
| if (page_flip_timer_.IsRunning()) |
| page_flip_timer_.AbandonAndStop(); |
| |
| ShelfAppButton* drag_view = shelf_view_->drag_view(); |
| |
| const bool should_start_animation = |
| drag_view && !shelf_view_->dragged_off_shelf() && drag_icon_widget_; |
| if (!should_start_animation) { |
| drag_icon_widget_.reset(); |
| return; |
| } |
| |
| // The ideal bounds stored in view model are in |shelf_view_|'s coordinates. |
| views::ViewModel* shelf_view_model = shelf_view_->view_model(); |
| const gfx::Rect target_bounds = shelf_view_model->ideal_bounds( |
| shelf_view_model->GetIndexOfView(drag_view)); |
| const gfx::Rect mirrored_target_bounds_in_shelf_view = |
| shelf_view_->GetMirroredRect(target_bounds); |
| |
| // No animation is created if the target slot for the drag icon is not on the |
| // current page. This edge case may be triggered by trying to move the icon of |
| // a running app to the area exclusively for pinned apps. |
| gfx::RectF target_bounds_in_local(mirrored_target_bounds_in_shelf_view); |
| ConvertRectToTarget(shelf_view_, this, &target_bounds_in_local); |
| if (!visible_space_.Contains(gfx::ToEnclosedRect(target_bounds_in_local))) { |
| drag_icon_widget_.reset(); |
| drag_view->layer()->SetOpacity(1.0f); |
| return; |
| } |
| |
| const bool drag_originated_from_app_list = |
| shelf_view_->IsShelfViewHandlingDragAndDrop(); |
| |
| // The drag proxy is the DragImageView created for the DragAndDropHost which |
| // could be either the ShelfView or ScrollableShelfView depending on where the |
| // drag originated from. |
| views::UniqueWidgetPtr drag_proxy; |
| if (drag_originated_from_app_list) { |
| drag_proxy = shelf_view_->RetrieveDragIconProxyAndClearDragProxyState(); |
| |
| // Handles the edge case that an app icon is dragged from AppListView to |
| // ShelfView then it is pinned to another page. |
| drag_icon_widget_.reset(); |
| } else { |
| drag_proxy = std::move(drag_icon_widget_); |
| } |
| |
| gfx::Rect start_in_screen(drag_proxy->GetContentsView()->GetBoundsInScreen()); |
| |
| gfx::Rect end_in_screen = mirrored_target_bounds_in_shelf_view; |
| ConvertRectToScreen(shelf_view_, &end_in_screen); |
| |
| if (start_in_screen.IsEmpty() || end_in_screen.IsEmpty()) { |
| drag_proxy.reset(); |
| return; |
| } |
| |
| drag_icon_drop_animation_delegate_ = |
| std::make_unique<DragIconDropAnimationDelegate>( |
| drag_view, start_in_screen, end_in_screen, std::move(drag_proxy)); |
| drag_icon_drop_animation_delegate_->StartAnimation(); |
| } |
| |
| bool ScrollableShelfView::StartDrag( |
| const std::string& app_id, |
| const gfx::Point& location_in_screen_coordinates) { |
| return false; |
| } |
| |
| bool ScrollableShelfView::Drag( |
| const gfx::Point& location_in_screen_coordinates) { |
| return false; |
| } |
| |
| void ScrollableShelfView::OnImplicitAnimationsCompleted() { |
| during_scroll_animation_ = false; |
| Layout(); |
| |
| EnableShelfRoundedCorners(/*enable=*/false); |
| |
| if (scroll_status_ != kAlongMainAxisScroll) |
| UpdateTappableIconIndices(); |
| |
| // Notifies ChromeVox of the changed location at the end of animation. |
| shelf_view_->NotifyAccessibilityEvent(ax::mojom::Event::kLocationChanged, |
| /*send_native_event=*/true); |
| |
| if (!drag_icon_widget_) |
| return; |
| |
| if (IsDragIconWithinVisibleSpace()) |
| return; |
| |
| page_flip_timer_.Start(FROM_HERE, page_flip_time_threshold_, this, |
| &ScrollableShelfView::OnPageFlipTimer); |
| } |
| |
| bool ScrollableShelfView::ShouldShowLeftArrow() const { |
| return (layout_strategy_ == kShowLeftArrowButton) || |
| (layout_strategy_ == kShowButtons); |
| } |
| |
| bool ScrollableShelfView::ShouldShowRightArrow() const { |
| return (layout_strategy_ == kShowRightArrowButton) || |
| (layout_strategy_ == kShowButtons); |
| } |
| |
| int ScrollableShelfView::GetStatusWidgetSizeOnPrimaryAxis( |
| bool use_target_bounds) const { |
| const gfx::Size status_widget_size = |
| use_target_bounds |
| ? GetShelf()->status_area_widget()->GetTargetBounds().size() |
| : GetShelf() |
| ->shelf_widget() |
| ->status_area_widget() |
| ->GetWindowBoundsInScreen() |
| .size(); |
| return GetShelf()->PrimaryAxisValue(status_widget_size.width(), |
| status_widget_size.height()); |
| } |
| |
| gfx::Rect ScrollableShelfView::GetAvailableLocalBounds( |
| bool use_target_bounds) const { |
| return use_target_bounds |
| ? gfx::Rect(GetShelf()->hotseat_widget()->GetTargetBounds().size()) |
| : GetLocalBounds(); |
| } |
| |
| gfx::Insets ScrollableShelfView::CalculatePaddingForDisplayCentering( |
| bool use_target_bounds) const { |
| const int icons_size = |
| shelf_view_->GetSizeOfAppButtons(shelf_view_->number_of_visible_apps(), |
| shelf_view_->GetButtonSize()) + |
| 2 * ShelfConfig::Get()->GetAppIconEndPadding(); |
| const gfx::Rect display_bounds = |
| screen_util::GetDisplayBoundsWithShelf(GetWidget()->GetNativeWindow()); |
| const int display_size_primary = GetShelf()->PrimaryAxisValue( |
| display_bounds.width(), display_bounds.height()); |
| const int gap = (display_size_primary - icons_size) / 2; |
| |
| // Calculates paddings in view coordinates. |
| const gfx::Rect screen_bounds = |
| use_target_bounds ? GetShelf()->hotseat_widget()->GetTargetBounds() |
| : GetBoundsInScreen(); |
| int before_padding = |
| gap - GetShelf()->PrimaryAxisValue( |
| ShouldAdaptToRTL() |
| ? display_bounds.right() - screen_bounds.right() |
| : screen_bounds.x() - display_bounds.x(), |
| screen_bounds.y() - display_bounds.y()); |
| int after_padding = |
| gap - GetShelf()->PrimaryAxisValue( |
| ShouldAdaptToRTL() |
| ? screen_bounds.x() - display_bounds.x() |
| : display_bounds.right() - screen_bounds.right(), |
| display_bounds.bottom() - screen_bounds.bottom()); |
| |
| // Checks whether there is enough space to ensure |base_padding_|. Returns |
| // empty insets if not. |
| if (before_padding < 0 || after_padding < 0) |
| return gfx::Insets(); |
| |
| gfx::Insets padding_insets; |
| if (GetShelf()->IsHorizontalAlignment()) { |
| padding_insets = |
| gfx::Insets(/*top=*/0, before_padding, /*bottom=*/0, after_padding); |
| } else { |
| padding_insets = |
| gfx::Insets(before_padding, /*left=*/0, after_padding, /*right=*/0); |
| } |
| |
| return padding_insets; |
| } |
| |
| bool ScrollableShelfView::ShouldHandleGestures(const ui::GestureEvent& event) { |
| // ScrollableShelfView only handles the gesture scrolling along the main axis. |
| // For other gesture events, including the scrolling across the main axis, |
| // they are handled by ShelfView. |
| |
| if (scroll_status_ == kNotInScroll && !event.IsScrollGestureEvent()) |
| return false; |
| |
| if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN) { |
| CHECK_EQ(scroll_status_, kNotInScroll); |
| |
| float main_offset = event.details().scroll_x_hint(); |
| float cross_offset = event.details().scroll_y_hint(); |
| if (!GetShelf()->IsHorizontalAlignment()) |
| std::swap(main_offset, cross_offset); |
| |
| scroll_status_ = std::abs(main_offset) < std::abs(cross_offset) |
| ? kAcrossMainAxisScroll |
| : kAlongMainAxisScroll; |
| } |
| |
| bool should_handle_gestures = scroll_status_ == kAlongMainAxisScroll; |
| |
| if (scroll_status_ == kAlongMainAxisScroll && |
| event.type() == ui::ET_GESTURE_SCROLL_BEGIN) { |
| scroll_offset_before_main_axis_scrolling_ = scroll_offset_; |
| layout_strategy_before_main_axis_scrolling_ = layout_strategy_; |
| |
| // The change in |scroll_status_| may lead to update on the gradient zone. |
| MaybeUpdateGradientZone(); |
| } |
| |
| if (event.type() == ui::ET_GESTURE_END) |
| ResetScrollStatus(); |
| |
| return should_handle_gestures; |
| } |
| |
| void ScrollableShelfView::ResetScrollStatus() { |
| scroll_status_ = kNotInScroll; |
| scroll_offset_before_main_axis_scrolling_ = gfx::Vector2dF(); |
| layout_strategy_before_main_axis_scrolling_ = kNotShowArrowButtons; |
| |
| // The change in |scroll_status_| may lead to update on the gradient zone. |
| MaybeUpdateGradientZone(); |
| } |
| |
| bool ScrollableShelfView::ProcessGestureEvent(const ui::GestureEvent& event) { |
| if (layout_strategy_ == kNotShowArrowButtons) |
| return true; |
| |
| // Handle scroll-related events, but don't do anything special for begin and |
| // end. |
| if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN) { |
| DCHECK(!presentation_time_recorder_); |
| if (Shell::Get()->IsInTabletMode()) { |
| if (Shell::Get()->app_list_controller()->IsVisible( |
| GetDisplayIdForView(this))) { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| GetWidget()->GetCompositor(), |
| kScrollDraggingTabletLauncherVisibleHistogram, |
| kScrollDraggingTabletLauncherVisibleMaxLatencyHistogram); |
| } else { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| GetWidget()->GetCompositor(), |
| kScrollDraggingTabletLauncherHiddenHistogram, |
| kScrollDraggingTabletLauncherHiddenMaxLatencyHistogram); |
| } |
| } else { |
| if (Shell::Get()->app_list_controller()->IsVisible( |
| GetDisplayIdForView(this))) { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| GetWidget()->GetCompositor(), |
| kScrollDraggingClamshellLauncherVisibleHistogram, |
| kScrollDraggingClamshellLauncherVisibleMaxLatencyHistogram); |
| } else { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| GetWidget()->GetCompositor(), |
| kScrollDraggingClamshellLauncherHiddenHistogram, |
| kScrollDraggingClamshellLauncherHiddenMaxLatencyHistogram); |
| } |
| } |
| return true; |
| } |
| |
| if (event.type() == ui::ET_GESTURE_END) { |
| // Do not reset |presentation_time_recorder_| in ui::ET_GESTURE_SCROLL_END |
| // event because it may not exist due to gesture fling. |
| presentation_time_recorder_.reset(); |
| |
| // The type of scrolling offset is float to ensure that ScrollableShelfView |
| // is responsive to slow gesture scrolling. However, after offset |
| // adjustment, the scrolling offset should be floored. |
| scroll_offset_ = gfx::ToFlooredVector2d(scroll_offset_); |
| |
| // If the scroll animation is created, tappable icon indices are updated |
| // at the end of animation. |
| if (!AdjustOffset() && !during_scroll_animation_) |
| UpdateTappableIconIndices(); |
| return true; |
| } |
| |
| if (event.type() == ui::ET_SCROLL_FLING_START) { |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| if (!ShouldHandleScroll(gfx::Vector2dF(event.details().velocity_x(), |
| event.details().velocity_y()), |
| /*is_gesture_fling=*/true)) { |
| return false; |
| } |
| |
| int scroll_velocity = is_horizontal_alignment |
| ? event.details().velocity_x() |
| : event.details().velocity_y(); |
| if (ShouldAdaptToRTL()) |
| scroll_velocity = -scroll_velocity; |
| float page_scrolling_offset = CalculatePageScrollingOffset( |
| scroll_velocity < 0, layout_strategy_before_main_axis_scrolling_); |
| |
| // Only starts animation when scroll distance is greater than zero. |
| if (std::fabs(page_scrolling_offset) > 0.f) { |
| ScrollToMainOffset((is_horizontal_alignment |
| ? scroll_offset_before_main_axis_scrolling_.x() |
| : scroll_offset_before_main_axis_scrolling_.y()) + |
| page_scrolling_offset, |
| /*animating=*/true); |
| } |
| |
| return true; |
| } |
| |
| if (event.type() != ui::ET_GESTURE_SCROLL_UPDATE) |
| return false; |
| |
| float scroll_delta = 0.f; |
| const bool is_horizontal = GetShelf()->IsHorizontalAlignment(); |
| if (is_horizontal) { |
| scroll_delta = -event.details().scroll_x(); |
| scroll_delta = ShouldAdaptToRTL() ? -scroll_delta : scroll_delta; |
| } else { |
| scroll_delta = -event.details().scroll_y(); |
| } |
| |
| // Return early if scrollable shelf cannot be scrolled anymore because it has |
| // reached to the end. |
| const float current_scroll_offset = CalculateMainAxisScrollDistance(); |
| if ((current_scroll_offset == 0.f && scroll_delta <= 0.f) || |
| (current_scroll_offset == CalculateScrollUpperBound(GetSpaceForIcons()) && |
| scroll_delta >= 0.f)) { |
| return true; |
| } |
| |
| DCHECK(presentation_time_recorder_); |
| presentation_time_recorder_->RequestNext(); |
| |
| if (is_horizontal) |
| ScrollByXOffset(scroll_delta, /*animate=*/false); |
| else |
| ScrollByYOffset(scroll_delta, /*animate=*/false); |
| |
| return true; |
| } |
| |
| void ScrollableShelfView::HandleMouseWheelEvent(ui::MouseWheelEvent* event) { |
| // Note that the scrolling from touchpad is propagated as mouse wheel event. |
| // Let the shelf handle mouse wheel events over the empty area of the shelf |
| // view, as these events would be ignored by the scrollable shelf view. |
| gfx::Point location_in_shelf_view = event->location(); |
| View::ConvertPointToTarget(this, shelf_view_, &location_in_shelf_view); |
| if (!shelf_view_->LocationInsideVisibleShelfItemBounds( |
| location_in_shelf_view)) { |
| GetShelf()->ProcessMouseWheelEvent(event, false); |
| return; |
| } |
| |
| if (!ShouldHandleScroll(gfx::Vector2dF(event->x_offset(), event->y_offset()), |
| /*is_gesture_fling=*/false)) { |
| return; |
| } |
| |
| event->SetHandled(); |
| |
| // Scrolling the mouse wheel may create multiple mouse wheel events at the |
| // same time. If the scrollable shelf view is during scrolling animation at |
| // this moment, do not handle the mouse wheel event. |
| if (shelf_view_->layer()->GetAnimator()->is_animating()) |
| return; |
| |
| if (GetShelf()->IsHorizontalAlignment()) { |
| const float x_offset = event->x_offset(); |
| const float y_offset = event->y_offset(); |
| // If the shelf is bottom aligned, we can scroll over the shelf contents if |
| // the scroll is horizontal or vertical (in the case of a mousewheel |
| // scroll). We take the biggest offset difference of the vertical and |
| // horizontal components to determine the offset to scroll over the |
| // contents. |
| float max_absolute_offset = |
| abs(x_offset) > abs(y_offset) ? x_offset : y_offset; |
| ScrollByXOffset( |
| CalculatePageScrollingOffset(max_absolute_offset < 0, layout_strategy_), |
| /*animating=*/true); |
| } else { |
| ScrollByYOffset( |
| CalculatePageScrollingOffset(event->y_offset() < 0, layout_strategy_), |
| /*animating=*/true); |
| } |
| } |
| |
| void ScrollableShelfView::ScrollByXOffset(float x_offset, bool animating) { |
| ScrollToMainOffset(scroll_offset_.x() + x_offset, animating); |
| } |
| |
| void ScrollableShelfView::ScrollByYOffset(float y_offset, bool animating) { |
| ScrollToMainOffset(scroll_offset_.y() + y_offset, animating); |
| } |
| |
| void ScrollableShelfView::ScrollToMainOffset(float target_offset, |
| bool animating) { |
| if (animating) { |
| StartShelfScrollAnimation(target_offset); |
| } else { |
| UpdateScrollOffset(target_offset); |
| shelf_container_view_->TranslateShelfView(scroll_offset_); |
| } |
| } |
| |
| float ScrollableShelfView::CalculatePageScrollingOffset( |
| bool forward, |
| LayoutStrategy layout_strategy) const { |
| // Returns zero if inputs are invalid. |
| const bool invalid = (layout_strategy == kNotShowArrowButtons) || |
| (layout_strategy == kShowLeftArrowButton && forward) || |
| (layout_strategy == kShowRightArrowButton && !forward); |
| if (invalid) |
| return 0; |
| |
| float offset = CalculatePageScrollingOffsetInAbs(layout_strategy); |
| |
| if (!forward) |
| offset = -offset; |
| |
| return offset; |
| } |
| |
| float ScrollableShelfView::CalculatePageScrollingOffsetInAbs( |
| LayoutStrategy layout_strategy) const { |
| // Implement the arrow button handler in the same way with the gesture |
| // scrolling. The key is to calculate the suitable scroll distance. |
| |
| float offset = 0.f; |
| |
| // The available space for icons excluding the area taken by arrow button(s). |
| int space_excluding_arrow; |
| |
| const int space_needed_for_button = GetSumOfButtonSizeAndSpacing(); |
| |
| if (layout_strategy == kShowRightArrowButton) { |
| space_excluding_arrow = GetSpaceForIcons() - kArrowButtonGroupWidth; |
| |
| // After scrolling, the left arrow button will show. Adapts the offset |
| // to the extra arrow button. |
| const int offset_for_extra_arrow = |
| kArrowButtonGroupWidth - ShelfConfig::Get()->GetAppIconEndPadding(); |
| |
| const int mod = space_excluding_arrow % space_needed_for_button; |
| offset = space_excluding_arrow - mod - offset_for_extra_arrow; |
| } else if (layout_strategy == kShowButtons || |
| layout_strategy == kShowLeftArrowButton) { |
| space_excluding_arrow = GetSpaceForIcons() - 2 * kArrowButtonGroupWidth; |
| const int mod = space_excluding_arrow % space_needed_for_button; |
| offset = space_excluding_arrow - mod; |
| |
| // Layout of kShowLeftArrowButton can be regarded as the layout of |
| // kShowButtons with extra offset. |
| if (layout_strategy == kShowLeftArrowButton) { |
| const int extra_offset = -ShelfConfig::Get()->button_spacing() - |
| (GetSpaceForIcons() - kArrowButtonGroupWidth) % |
| space_needed_for_button + |
| ShelfConfig::Get()->GetAppIconEndPadding(); |
| offset += extra_offset; |
| } |
| } |
| |
| DCHECK_GE(offset, 0); |
| |
| return offset; |
| } |
| |
| float ScrollableShelfView::CalculateTargetOffsetAfterScroll( |
| float start_offset, |
| float scroll_distance) const { |
| float target_offset = start_offset; |
| |
| target_offset += scroll_distance; |
| target_offset = |
| CalculateClampedScrollOffset(target_offset, GetSpaceForIcons()); |
| LayoutStrategy layout_strategy_after_scroll = |
| CalculateLayoutStrategy(target_offset, GetSpaceForIcons()); |
| target_offset = CalculateScrollDistanceAfterAdjustment( |
| target_offset, layout_strategy_after_scroll); |
| |
| return target_offset; |
| } |
| |
| ScrollableShelfView::FadeZone ScrollableShelfView::CalculateStartGradientZone() |
| const { |
| if (!should_show_start_gradient_zone_) |
| return FadeZone(); |
| |
| gfx::Rect zone_rect; |
| bool fade_in = false; |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| |
| if (is_horizontal_alignment) { |
| int gradient_start; |
| int gradient_end; |
| |
| // Calculates the bounds of the gradient zone. Enlarge the gradient zone by |
| // one-pixel to offset the potential rounding error during rendering (we |
| // also do it in CalculateEndGradientZone()). |
| if (ShouldAdaptToRTL()) { |
| const int border = visible_space_.right(); |
| gradient_start = border - kGradientZoneLength; |
| gradient_end = border + 1; |
| } else { |
| const int border = visible_space_.x(); |
| gradient_start = border - 1; |
| gradient_end = border + kGradientZoneLength; |
| } |
| zone_rect = |
| gfx::Rect(gradient_start, 0, gradient_end - gradient_start, height()); |
| } else { |
| zone_rect = |
| gfx::Rect(0, visible_space_.y() - 1, width(), kGradientZoneLength + 1); |
| } |
| |
| fade_in = !ShouldAdaptToRTL(); |
| |
| return {zone_rect, fade_in, is_horizontal_alignment}; |
| } |
| |
| ScrollableShelfView::FadeZone ScrollableShelfView::CalculateEndGradientZone() |
| const { |
| if (!should_show_end_gradient_zone_) |
| return FadeZone(); |
| |
| gfx::Rect zone_rect; |
| bool fade_in = false; |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| |
| if (is_horizontal_alignment) { |
| int gradient_start; |
| int gradient_end; |
| |
| if (ShouldAdaptToRTL()) { |
| const int border = visible_space_.x(); |
| gradient_start = border - 1; |
| gradient_end = border + kGradientZoneLength; |
| } else { |
| const int border = visible_space_.right(); |
| gradient_start = border - kGradientZoneLength; |
| gradient_end = border + 1; |
| } |
| zone_rect = |
| gfx::Rect(gradient_start, 0, gradient_end - gradient_start, height()); |
| } else { |
| zone_rect = gfx::Rect(0, visible_space_.bottom() - kGradientZoneLength, |
| width(), kGradientZoneLength + 1); |
| } |
| |
| fade_in = ShouldAdaptToRTL(); |
| |
| return {zone_rect, fade_in, is_horizontal_alignment}; |
| } |
| |
| void ScrollableShelfView::UpdateGradientZoneState() { |
| // The gradient zone is not painted when the focus ring shows in order to |
| // display the focus ring correctly. |
| if (focus_ring_activated_) { |
| should_show_start_gradient_zone_ = false; |
| should_show_end_gradient_zone_ = false; |
| return; |
| } |
| |
| if (during_scroll_animation_) { |
| should_show_start_gradient_zone_ = true; |
| should_show_end_gradient_zone_ = true; |
| return; |
| } |
| |
| should_show_start_gradient_zone_ = layout_strategy_ == kShowLeftArrowButton || |
| (layout_strategy_ == kShowButtons && |
| scroll_status_ == kAlongMainAxisScroll); |
| should_show_end_gradient_zone_ = ShouldShowRightArrow(); |
| } |
| |
| void ScrollableShelfView::MaybeUpdateGradientZone() { |
| if (!ShouldApplyMaskLayerGradientZone()) |
| return; |
| |
| // Fade zones should be updated if: |
| // (1) Fade zone's visibility changes. |
| // (2) Fade zone should show and the arrow button's location changes. |
| UpdateGradientZoneState(); |
| |
| const FadeZone target_start_fade_zone = CalculateStartGradientZone(); |
| const FadeZone target_end_fade_zone = CalculateEndGradientZone(); |
| |
| const bool should_update_start_fade_zone = |
| target_start_fade_zone.zone_rect != |
| gradient_layer_delegate_->start_fade_zone_bounds(); |
| const bool should_update_end_fade_zone = |
| target_end_fade_zone.zone_rect != |
| gradient_layer_delegate_->end_fade_zone_bounds(); |
| |
| if (!should_update_start_fade_zone && !should_update_end_fade_zone) |
| return; |
| |
| PaintGradientZone(CalculateStartGradientZone(), CalculateEndGradientZone()); |
| } |
| |
| void ScrollableShelfView::PaintGradientZone(const FadeZone& start_rect, |
| const FadeZone& end_rect) { |
| gradient_layer_delegate_->set_start_fade_zone(start_rect); |
| gradient_layer_delegate_->set_end_fade_zone(end_rect); |
| SchedulePaint(); |
| } |
| |
| bool ScrollableShelfView::ShouldApplyMaskLayerGradientZone() const { |
| return layout_strategy_ != LayoutStrategy::kNotShowArrowButtons; |
| } |
| |
| float ScrollableShelfView::GetActualScrollOffset( |
| float main_axis_scroll_distance, |
| LayoutStrategy layout_strategy) const { |
| return (layout_strategy == kShowButtons || |
| layout_strategy == kShowLeftArrowButton) |
| ? (main_axis_scroll_distance + kArrowButtonGroupWidth - |
| ShelfConfig::Get()->GetAppIconEndPadding()) |
| : main_axis_scroll_distance; |
| } |
| |
| void ScrollableShelfView::UpdateTappableIconIndices() { |
| // Scrollable shelf should be not under the scroll along the main axis, which |
| // means that the decimal part of the main scroll offset should be zero. |
| DCHECK(scroll_status_ != kAlongMainAxisScroll); |
| |
| // The value returned by CalculateMainAxisScrollDistance() can be casted into |
| // an integer without losing precision since the decimal part is zero. |
| const std::pair<int, int> tappable_indices = CalculateTappableIconIndices( |
| layout_strategy_, CalculateMainAxisScrollDistance()); |
| first_tappable_app_index_ = tappable_indices.first; |
| last_tappable_app_index_ = tappable_indices.second; |
| } |
| |
| std::pair<int, int> ScrollableShelfView::CalculateTappableIconIndices( |
| ScrollableShelfView::LayoutStrategy layout_strategy, |
| int scroll_distance_on_main_axis) const { |
| const auto& visible_views_indices = shelf_view_->visible_views_indices(); |
| |
| if (visible_views_indices.empty() || visible_space_.IsEmpty()) |
| return std::pair<int, int>(-1, -1); |
| |
| if (layout_strategy == ScrollableShelfView::kNotShowArrowButtons) { |
| return std::pair<int, int>(visible_views_indices.front(), |
| visible_views_indices.back()); |
| } |
| |
| const int visible_size = GetShelf()->IsHorizontalAlignment() |
| ? visible_space_.width() |
| : visible_space_.height(); |
| |
| const int space_needed_for_button = GetSumOfButtonSizeAndSpacing(); |
| |
| // Note that some apps may have their |ShelfAppButton| views hidden, when they |
| // are on an inactive desk. Therefore, the indices of tappable apps may not be |
| // contiguous, so we need to map from a visible view index back to an app |
| // index. The below are indices into the |visible_views_indices| vector. |
| int first_visible_view_index = -1; |
| int last_visible_view_index = -1; |
| if (layout_strategy == kShowRightArrowButton || |
| layout_strategy == kShowButtons) { |
| first_visible_view_index = |
| scroll_distance_on_main_axis / space_needed_for_button + |
| (layout_strategy == kShowButtons ? 1 : 0); |
| last_visible_view_index = |
| first_visible_view_index + visible_size / space_needed_for_button; |
| |
| const int end_of_last_visible_view = |
| last_visible_view_index * space_needed_for_button + |
| shelf_view_->GetButtonSize() - scroll_distance_on_main_axis; |
| |
| // It is very rare but |visible_size| may be smaller than |
| // |space_needed_for_button| as reported in https://crbug.com/1094363. |
| if (end_of_last_visible_view > visible_size && |
| last_visible_view_index > first_visible_view_index) { |
| last_visible_view_index--; |
| } |
| } else { |
| DCHECK_EQ(layout_strategy, kShowLeftArrowButton); |
| last_visible_view_index = visible_views_indices.size() - 1; |
| first_visible_view_index = |
| last_visible_view_index - visible_size / space_needed_for_button + 1; |
| } |
| |
| DCHECK_GE(first_visible_view_index, 0); |
| DCHECK_LT(first_visible_view_index, |
| static_cast<int>(visible_views_indices.size())); |
| DCHECK_GE(last_visible_view_index, 0); |
| DCHECK_LT(last_visible_view_index, |
| static_cast<int>(visible_views_indices.size())); |
| |
| return std::pair<int, int>(visible_views_indices[first_visible_view_index], |
| visible_views_indices[last_visible_view_index]); |
| } |
| |
| views::View* ScrollableShelfView::FindFirstFocusableChild() { |
| return shelf_view_->FindFirstFocusableChild(); |
| } |
| |
| views::View* ScrollableShelfView::FindLastFocusableChild() { |
| return shelf_view_->FindLastFocusableChild(); |
| } |
| |
| int ScrollableShelfView::GetSpaceForIcons() const { |
| return GetShelf()->IsHorizontalAlignment() ? available_space_.width() |
| : available_space_.height(); |
| } |
| |
| bool ScrollableShelfView::CanFitAllAppsWithoutScrolling( |
| const gfx::Size& available_size, |
| const gfx::Size& icons_preferred_size) const { |
| const int available_length = |
| (GetShelf()->IsHorizontalAlignment() ? available_size.width() |
| : available_size.height()); |
| |
| int preferred_length = GetShelf()->IsHorizontalAlignment() |
| ? icons_preferred_size.width() |
| : icons_preferred_size.height(); |
| preferred_length += 2 * ShelfConfig::Get()->GetAppIconEndPadding(); |
| |
| return available_length >= preferred_length; |
| } |
| |
| bool ScrollableShelfView::ShouldHandleScroll(const gfx::Vector2dF& offset, |
| bool is_gesture_scrolling) const { |
| // When the shelf is aligned at the bottom, a horizontal mousewheel scroll may |
| // also be handled by the ScrollableShelf if the offset along the main axis is |
| // 0. This case is mainly triggered by an event generated in the MouseWheel, |
| // but not in the touchpad, as touchpads events are caught on ScrollEvent. |
| // If there is an x component to the scroll, consider this instead of the y |
| // axis because the horizontal scroll could move the scrollable shelf. |
| const float main_axis_offset = |
| GetShelf()->IsHorizontalAlignment() && offset.x() != 0 ? offset.x() |
| : offset.y(); |
| |
| const int threshold = is_gesture_scrolling ? kGestureFlingVelocityThreshold |
| : KScrollOffsetThreshold; |
| return abs(main_axis_offset) > threshold; |
| } |
| |
| bool ScrollableShelfView::AdjustOffset() { |
| const float offset = CalculateAdjustmentOffset( |
| CalculateMainAxisScrollDistance(), layout_strategy_, GetSpaceForIcons()); |
| |
| // Returns early when it does not need to adjust the shelf view's location. |
| if (!offset) |
| return false; |
| |
| if (GetShelf()->IsHorizontalAlignment()) |
| ScrollByXOffset(offset, /*animate=*/true); |
| else |
| ScrollByYOffset(offset, /*animate=*/true); |
| |
| return true; |
| } |
| |
| float ScrollableShelfView::CalculateAdjustmentOffset( |
| int main_axis_scroll_distance, |
| LayoutStrategy layout_strategy, |
| int available_space_for_icons) const { |
| // Scrollable shelf should be not under the scroll along the main axis, which |
| // means that the decimal part of the main scroll offset should be zero. |
| DCHECK(scroll_status_ != kAlongMainAxisScroll); |
| |
| // Returns early when it does not need to adjust the shelf view's location. |
| if (layout_strategy == kNotShowArrowButtons || |
| main_axis_scroll_distance >= |
| CalculateScrollUpperBound(available_space_for_icons)) { |
| return 0; |
| } |
| |
| // Because the decimal part of the scroll offset is zero, it is meaningful |
| // to use modulo operation here. |
| const int remainder = static_cast<int>(GetActualScrollOffset( |
| main_axis_scroll_distance, layout_strategy)) % |
| GetSumOfButtonSizeAndSpacing(); |
| int offset = remainder > GetGestureDragThreshold() |
| ? GetSumOfButtonSizeAndSpacing() - remainder |
| : -remainder; |
| |
| return offset; |
| } |
| |
| int ScrollableShelfView::CalculateScrollDistanceAfterAdjustment( |
| int main_axis_scroll_distance, |
| LayoutStrategy layout_strategy) const { |
| return main_axis_scroll_distance + |
| CalculateAdjustmentOffset(main_axis_scroll_distance, layout_strategy, |
| GetSpaceForIcons()); |
| } |
| |
| void ScrollableShelfView::UpdateAvailableSpace() { |
| if (!is_padding_configured_externally_) { |
| edge_padding_insets_ = CalculateEdgePadding(/*use_target_bounds=*/false); |
| } |
| |
| available_space_ = GetLocalBounds(); |
| available_space_.Inset(edge_padding_insets_); |
| |
| // The hotseat uses |available_space_| to determine where to show its |
| // background, so notify it when it is recalculated. |
| if (HotseatWidget::ShouldShowHotseatBackground()) |
| GetShelf()->hotseat_widget()->UpdateTranslucentBackground(); |
| } |
| |
| gfx::Rect ScrollableShelfView::CalculateVisibleSpace( |
| LayoutStrategy layout_strategy) const { |
| const bool in_tablet_mode = Shell::Get()->IsInTabletMode(); |
| if (layout_strategy == kNotShowArrowButtons && !in_tablet_mode) |
| return GetAvailableLocalBounds(/*use_target_bounds=*/false); |
| |
| const bool should_show_left_arrow = |
| (layout_strategy == kShowLeftArrowButton) || |
| (layout_strategy == kShowButtons); |
| const bool should_show_right_arrow = |
| (layout_strategy == kShowRightArrowButton) || |
| (layout_strategy == kShowButtons); |
| |
| const int before_padding = |
| (should_show_left_arrow ? kArrowButtonGroupWidth : 0); |
| const int after_padding = |
| (should_show_right_arrow ? kArrowButtonGroupWidth : 0); |
| |
| gfx::Insets visible_space_insets; |
| if (ShouldAdaptToRTL()) { |
| visible_space_insets = gfx::Insets(0, after_padding, 0, before_padding); |
| } else { |
| visible_space_insets = |
| GetShelf()->IsHorizontalAlignment() |
| ? gfx::Insets(0, before_padding, 0, after_padding) |
| : gfx::Insets(before_padding, 0, after_padding, 0); |
| } |
| visible_space_insets -= CalculateRipplePaddingInsets(); |
| |
| gfx::Rect visible_space = available_space_; |
| visible_space.Inset(visible_space_insets); |
| |
| return visible_space; |
| } |
| |
| gfx::Insets ScrollableShelfView::CalculateRipplePaddingInsets() const { |
| // Indicates whether it is in tablet mode with hotseat enabled. |
| const bool in_tablet_mode = Shell::Get()->IsInTabletMode(); |
| |
| const int ripple_padding = |
| ShelfConfig::Get()->scrollable_shelf_ripple_padding(); |
| const int before_padding = |
| (in_tablet_mode && !ShouldShowLeftArrow()) ? 0 : ripple_padding; |
| const int after_padding = |
| (in_tablet_mode && !ShouldShowRightArrow()) ? 0 : ripple_padding; |
| |
| if (ShouldAdaptToRTL()) |
| return gfx::Insets(0, after_padding, 0, before_padding); |
| |
| return GetShelf()->IsHorizontalAlignment() |
| ? gfx::Insets(0, before_padding, 0, after_padding) |
| : gfx::Insets(before_padding, 0, after_padding, 0); |
| } |
| |
| gfx::RoundedCornersF |
| ScrollableShelfView::CalculateShelfContainerRoundedCorners() const { |
| // This function may access TabletModeController during destruction of |
| // Hotseat. However, TabletModeController is destructed before Hotseat. So |
| // check the pointer explicitly here. |
| // TODO(https://crbug.com/1067490): reorder the destruction order in |
| // Shell::~Shell then remove the explicit check. |
| const bool is_in_tablet_mode = |
| Shell::Get()->tablet_mode_controller() && Shell::Get()->IsInTabletMode(); |
| |
| if (!is_in_tablet_mode) |
| return gfx::RoundedCornersF(); |
| |
| const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment(); |
| const float radius = (is_horizontal_alignment ? height() : width()) / 2.f; |
| |
| int upper_left = ShouldShowLeftArrow() ? 0 : radius; |
| |
| int upper_right; |
| if (is_horizontal_alignment) |
| upper_right = ShouldShowRightArrow() ? 0 : radius; |
| else |
| upper_right = ShouldShowLeftArrow() ? 0 : radius; |
| |
| int lower_right = ShouldShowRightArrow() ? 0 : radius; |
| |
| int lower_left; |
| if (is_horizontal_alignment) |
| lower_left = ShouldShowLeftArrow() ? 0 : radius; |
| else |
| lower_left = ShouldShowRightArrow() ? 0 : radius; |
| |
| if (ShouldAdaptToRTL()) { |
| std::swap(upper_left, upper_right); |
| std::swap(lower_left, lower_right); |
| } |
| |
| return gfx::RoundedCornersF(upper_left, upper_right, lower_right, lower_left); |
| } |
| |
| void ScrollableShelfView::OnPageFlipTimer() { |
| gfx::Rect visible_space_in_screen = visible_space_; |
| views::View::ConvertRectToScreen(this, &visible_space_in_screen); |
| |
| const gfx::Rect drag_icon_screen_bounds = |
| drag_icon_widget_->GetWindowBoundsInScreen(); |
| |
| DCHECK(!visible_space_in_screen.Contains(drag_icon_screen_bounds)); |
| |
| // Calculates the page scrolling direction based on the |drag_icon_widget_|'s |
| // location and the bounds of the visible space. |
| bool should_scroll_to_next; |
| if (ShouldAdaptToRTL()) { |
| should_scroll_to_next = |
| drag_icon_screen_bounds.x() < visible_space_in_screen.x(); |
| } else { |
| should_scroll_to_next = |
| GetShelf()->IsHorizontalAlignment() |
| ? drag_icon_screen_bounds.right() > visible_space_in_screen.right() |
| : drag_icon_screen_bounds.bottom() > |
| visible_space_in_screen.bottom(); |
| } |
| |
| ScrollToNewPage(/*forward=*/should_scroll_to_next); |
| |
| if (test_observer_) |
| test_observer_->OnPageFlipTimerFired(); |
| } |
| |
| bool ScrollableShelfView::IsDragIconWithinVisibleSpace() const { |
| gfx::Rect visible_space_in_screen = visible_space_; |
| views::View::ConvertRectToScreen(this, &visible_space_in_screen); |
| |
| const gfx::Rect drag_icon_screen_bounds = |
| drag_icon_widget_->GetContentsView()->GetBoundsInScreen(); |
| |
| if (GetShelf()->IsHorizontalAlignment()) { |
| return drag_icon_screen_bounds.x() >= visible_space_in_screen.x() && |
| drag_icon_screen_bounds.right() <= visible_space_in_screen.right(); |
| } |
| |
| return drag_icon_screen_bounds.y() >= visible_space_in_screen.y() && |
| drag_icon_screen_bounds.bottom() <= visible_space_in_screen.bottom(); |
| } |
| |
| bool ScrollableShelfView::ShouldDelegateScrollToShelf( |
| const ui::ScrollEvent& event) const { |
| // When the shelf is not aligned in the bottom, the events should be |
| // propagated and handled as MouseWheel events. |
| if (!GetShelf()->IsHorizontalAlignment()) |
| return false; |
| |
| if (event.type() != ui::ET_SCROLL) |
| return false; |
| |
| const float main_offset = event.x_offset(); |
| const float cross_offset = event.y_offset(); |
| // We only delegate to the shelf scroll events across the main axis, |
| // otherwise, let them propagate and be handled as MouseWheel Events. |
| return std::abs(main_offset) < std::abs(cross_offset); |
| } |
| |
| float ScrollableShelfView::CalculateMainAxisScrollDistance() const { |
| return GetShelf()->IsHorizontalAlignment() ? scroll_offset_.x() |
| : scroll_offset_.y(); |
| } |
| |
| void ScrollableShelfView::UpdateScrollOffset(float target_offset) { |
| target_offset = |
| CalculateClampedScrollOffset(target_offset, GetSpaceForIcons()); |
| |
| if (GetShelf()->IsHorizontalAlignment()) |
| scroll_offset_.set_x(target_offset); |
| else |
| scroll_offset_.set_y(target_offset); |
| |
| // Calculating the layout strategy relies on |scroll_offset_|. |
| LayoutStrategy new_strategy = CalculateLayoutStrategy( |
| CalculateMainAxisScrollDistance(), GetSpaceForIcons()); |
| |
| const bool strategy_needs_update = (layout_strategy_ != new_strategy); |
| if (strategy_needs_update) { |
| layout_strategy_ = new_strategy; |
| const bool has_gradient_zone = layer()->layer_mask_layer(); |
| const bool should_have_gradient_zone = ShouldApplyMaskLayerGradientZone(); |
| if (has_gradient_zone && !should_have_gradient_zone) { |
| PaintGradientZone(FadeZone(), FadeZone()); |
| layer()->SetMaskLayer(nullptr); |
| } else if (!has_gradient_zone && should_have_gradient_zone) { |
| gradient_layer_delegate_->layer()->SetBounds(layer()->bounds()); |
| layer()->SetMaskLayer(gradient_layer_delegate_->layer()); |
| } |
| InvalidateLayout(); |
| } |
| |
| visible_space_ = CalculateVisibleSpace(layout_strategy_); |
| |
| if (scroll_status_ != kAlongMainAxisScroll) |
| UpdateTappableIconIndices(); |
| } |
| |
| void ScrollableShelfView::UpdateAvailableSpaceAndScroll() { |
| UpdateAvailableSpace(); |
| UpdateScrollOffset(CalculateMainAxisScrollDistance()); |
| } |
| |
| int ScrollableShelfView::CalculateScrollOffsetForTargetAvailableSpace( |
| const gfx::Rect& target_space) const { |
| // Ensures that the scroll offset is legal under the updated available space. |
| const int available_space_for_icons = |
| GetShelf()->PrimaryAxisValue(target_space.width(), target_space.height()); |
| int target_scroll_offset = CalculateClampedScrollOffset( |
| CalculateMainAxisScrollDistance(), available_space_for_icons); |
| |
| // Calculates the layout strategy based on the new scroll offset. |
| LayoutStrategy new_strategy = |
| CalculateLayoutStrategy(target_scroll_offset, available_space_for_icons); |
| |
| // Adjusts the scroll offset with the new strategy. |
| target_scroll_offset += CalculateAdjustmentOffset( |
| target_scroll_offset, new_strategy, available_space_for_icons); |
| |
| return target_scroll_offset; |
| } |
| |
| bool ScrollableShelfView::ShouldCountActivatedInkDrop( |
| const views::View* sender) const { |
| bool should_count = false; |
| |
| // When scrolling shelf by gestures, the shelf icon's ink drop ripple may be |
| // activated accidentally. So ignore the ink drop activity during animation. |
| if (during_scroll_animation_) |
| return should_count; |
| |
| // The ink drop needs to be clipped only if |sender| is the app at one of the |
| // corners of the shelf. This happens if it is either the first or the last |
| // tappable app and no arrow is showing on its side. |
| if (shelf_view_->view_model()->view_at(first_tappable_app_index_) == sender) { |
| should_count = !(layout_strategy_ == kShowButtons || |
| layout_strategy_ == kShowLeftArrowButton); |
| } else if (shelf_view_->view_model()->view_at(last_tappable_app_index_) == |
| sender) { |
| should_count = !(layout_strategy_ == kShowButtons || |
| layout_strategy_ == kShowRightArrowButton); |
| } |
| |
| return should_count; |
| } |
| |
| void ScrollableShelfView::EnableShelfRoundedCorners(bool enable) { |
| // Only enable shelf rounded corners in tablet mode. Note that we allow |
| // disabling rounded corners in clamshell. Because when switching to clamshell |
| // from tablet, this method may be called after tablet mode ends. |
| // TODO(https://crbug.com/1067490): reorder the destruction order in |
| // Shell::~Shell then remove the explicit check. |
| const bool is_in_tablet_mode = |
| Shell::Get()->tablet_mode_controller() && Shell::Get()->IsInTabletMode(); |
| if (enable && !is_in_tablet_mode) |
| return; |
| |
| ui::Layer* layer = shelf_container_view_->layer(); |
| |
| const bool has_rounded_corners = !(layer->rounded_corner_radii().IsEmpty()); |
| if (enable == has_rounded_corners) |
| return; |
| |
| // In non-overflow mode, only apply layer clip on |shelf_container_view_| |
| // when the ripple ring of the first/last shelf icon shows. |
| // Note that |layout_strategy_| may update while EnableShelfRoundedCorners() |
| // is not called. For example, the first icon's context menu shows, then the |
| // system notification makes shelf view enter overflow mode. So |
| // |layer_clip_in_non_overflow_| updates regardless of |layout_strategy_|. |
| layer_clip_in_non_overflow_ = enable; |
| |
| if (layout_strategy_ == kNotShowArrowButtons) |
| EnableLayerClipOnShelfContainerView(layer_clip_in_non_overflow_); |
| |
| layer->SetRoundedCornerRadius(enable ? CalculateShelfContainerRoundedCorners() |
| : gfx::RoundedCornersF()); |
| |
| if (!layer->is_fast_rounded_corner()) |
| layer->SetIsFastRoundedCorner(/*enable=*/true); |
| } |
| |
| void ScrollableShelfView::OnActiveInkDropChange(bool increase) { |
| if (increase) |
| ++activated_corner_buttons_; |
| else |
| --activated_corner_buttons_; |
| |
| // When long pressing icons, sometimes there are more ripple animations |
| // pending over others buttons. Only activate rounded corners when at least |
| // one button needs them. |
| CHECK_GE(activated_corner_buttons_, 0); |
| CHECK_LE(activated_corner_buttons_, 2); |
| EnableShelfRoundedCorners(activated_corner_buttons_ > 0); |
| } |
| |
| bool ScrollableShelfView::ShouldEnableLayerClip() const { |
| // Always use layer clip in overflow mode. |
| if (layout_strategy_ != LayoutStrategy::kNotShowArrowButtons) |
| return true; |
| |
| // TODO(https://crbug.com/1067490): reorder the destruction order in |
| // Shell::~Shell then remove the explicit check. |
| const bool is_in_tablet_mode = |
| Shell::Get()->tablet_mode_controller() && Shell::Get()->IsInTabletMode(); |
| |
| // In clamshell, only use layer clip in overflow mode. |
| if (!is_in_tablet_mode) |
| return false; |
| |
| // In tablet mode, whether using layer clip in non-overflow mode depends on |
| // |layer_clip_in_non_overflow_|. |
| return layer_clip_in_non_overflow_; |
| } |
| |
| void ScrollableShelfView::EnableLayerClipOnShelfContainerView(bool enable) { |
| if (!enable) { |
| shelf_container_view_->layer()->SetClipRect(gfx::Rect()); |
| return; |
| } |
| |
| // |visible_space_| is in local coordinates. It should be transformed into |
| // |shelf_container_view_|'s coordinates for layer clip. |
| gfx::RectF visible_space_in_shelf_container_coordinates(visible_space_); |
| views::View::ConvertRectToTarget( |
| this, shelf_container_view_, |
| &visible_space_in_shelf_container_coordinates); |
| shelf_container_view_->layer()->SetClipRect( |
| gfx::ToEnclosedRect(visible_space_in_shelf_container_coordinates)); |
| } |
| |
| int ScrollableShelfView::CalculateShelfIconsPreferredLength() const { |
| const gfx::Size shelf_preferred_size( |
| shelf_container_view_->GetPreferredSize()); |
| const int preferred_length = |
| (GetShelf()->IsHorizontalAlignment() ? shelf_preferred_size.width() |
| : shelf_preferred_size.height()); |
| return preferred_length + 2 * ShelfConfig::Get()->GetAppIconEndPadding(); |
| } |
| |
| } // namespace ash |