| // Copyright 2013 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/app_list/views/apps_container_view.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/app_list/app_list_model_provider.h" |
| #include "ash/app_list/app_list_util.h" |
| #include "ash/app_list/views/app_list_a11y_announcer.h" |
| #include "ash/app_list/views/app_list_folder_view.h" |
| #include "ash/app_list/views/app_list_item_view.h" |
| #include "ash/app_list/views/app_list_main_view.h" |
| #include "ash/app_list/views/app_list_view.h" |
| #include "ash/app_list/views/contents_view.h" |
| #include "ash/app_list/views/continue_section_view.h" |
| #include "ash/app_list/views/folder_background_view.h" |
| #include "ash/app_list/views/page_switcher.h" |
| #include "ash/app_list/views/search_box_view.h" |
| #include "ash/app_list/views/suggestion_chip_container_view.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/keyboard/ui/keyboard_ui_controller.h" |
| #include "ash/public/cpp/app_list/app_list_config.h" |
| #include "ash/public/cpp/app_list/app_list_model_delegate.h" |
| #include "ash/public/cpp/app_list/app_list_switches.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/search_box/search_box_constants.h" |
| #include "ash/shelf/gradient_layer_delegate.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "ash/style/style_util.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/cxx17_backports.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animation_element.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/controls/highlight_path_generator.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/view_utils.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // The number of rows for portrait mode with mode productivity launcher |
| // enabled. |
| constexpr int kPreferredGridRowsInPortraitProductivityLauncher = 5; |
| |
| // The number of columns for portrait mode with mode productivity launcher |
| // enabled. |
| constexpr int kPreferredGridColumnsInPortraitProductivityLauncher = 5; |
| |
| // The long apps grid dimension when productivity launcher is not enabled: |
| // * number of columns in landscape mode |
| // * number of rows in portrait mode |
| constexpr int kPreferredGridColumns = 5; |
| |
| // The short apps grid dimension when productivity launcher is not enabled: |
| // * number of rows in landscape mode |
| // * number of columns in portrait mode |
| constexpr int kPreferredGridRows = 4; |
| |
| // The range of app list transition progress in which the suggestion chips' |
| // opacity changes from 0 to 1. |
| constexpr float kSuggestionChipOpacityStartProgress = 0.66; |
| constexpr float kSuggestionChipOpacityEndProgress = 1; |
| |
| // Range of the height of centerline above screen bottom that all apps should |
| // change opacity. NOTE: this is used to change page switcher's opacity as |
| // well. |
| constexpr float kAppsOpacityChangeStart = 8.0f; |
| constexpr float kAppsOpacityChangeEnd = 144.0f; |
| |
| // The app list transition progress value for fullscreen state. |
| constexpr float kAppListFullscreenProgressValue = 2.0; |
| |
| // The amount by which the apps container UI should be offset downwards when |
| // shown on non apps page UI. |
| constexpr int kNonAppsStateVerticalOffset = 24; |
| |
| // The opacity the apps container UI should have when shown on non apps page UI. |
| constexpr float kNonAppsStateOpacity = 0.1; |
| |
| // The ratio of allowed bounds for apps grid view to its maximum margin. |
| constexpr int kAppsGridMarginRatio = 16; |
| constexpr int kAppsGridMarginRatioForSmallWidth = 12; |
| |
| // The margins within the apps container for app list folder view. |
| constexpr int kFolderMargin = 8; |
| |
| // The suggestion chip container height. |
| constexpr int kSuggestionChipContainerHeight = 32; |
| |
| // The suggestion chip container top margin. |
| constexpr int kSuggestionChipContainerTopMargin = 16; |
| |
| // The horizontal margin between the apps grid view and the page switcher. |
| constexpr int kGridToPageSwitcherMargin = 8; |
| |
| // Minimal horizontal distance from the page switcher to apps container bounds. |
| constexpr int kPageSwitcherEndMargin = 16; |
| |
| // The vertical margin for the `AppsGridView` contents. |
| constexpr int kGridVerticalMargin = 24; |
| |
| // The space between sort ui controls (including sort button and redo button). |
| constexpr int kSortUiControlSpacing = 10; |
| |
| // The preferred size of sort ui controls (like sort button and redo button). |
| constexpr int kSortUiControlPreferredSize = 20; |
| |
| // The number of columns available for the ContinueSectionView. |
| constexpr int kContinueColumnCount = 4; |
| |
| // The vertical spacing between recent apps and continue section view. |
| constexpr int kRecentAppsTopMargin = 16; |
| |
| // The vertical spacing above and below the separator when using kRegular/kDense |
| // AppListConfigType. |
| constexpr int kRegularSeparatorVerticalInset = 16; |
| constexpr int kDenseSeparatorVerticalInset = 8; |
| |
| // The width of the separator. |
| constexpr int kSeparatorWidth = 240; |
| |
| // The actual height of the fadeout gradient mask at the top and bottom of the |
| // `scrollable_container_`. |
| constexpr int kDefaultFadeoutMaskHeight = 16; |
| |
| // SortUiControl --------------------------------------------------------------- |
| |
| class SortUiControl : public views::ImageButton { |
| public: |
| SortUiControl(AppListViewDelegate* delegate, |
| views::Button::PressedCallback pressed_callback) |
| : views::ImageButton(pressed_callback), delegate_(delegate) { |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON); |
| SetPreferredSize( |
| gfx::Size(kSortUiControlPreferredSize, kSortUiControlPreferredSize)); |
| |
| // This view is used behind the feature flag and is immature. Therefore |
| // ignore it in a11y for now. |
| GetViewAccessibility().OverrideIsIgnored(true); |
| } |
| SortUiControl(const SortUiControl&) = delete; |
| SortUiControl& operator=(const SortUiControl&) = delete; |
| ~SortUiControl() override = default; |
| |
| // views::View: |
| void OnThemeChanged() override { |
| views::View::OnThemeChanged(); |
| StyleUtil::ConfigureInkDropAttributes( |
| this, StyleUtil::kBaseColor | StyleUtil::kInkDropOpacity | |
| StyleUtil::kHighlightOpacity); |
| views::InstallFixedSizeCircleHighlightPathGenerator( |
| this, kSortUiControlPreferredSize / 2); |
| views::InkDrop::UseInkDropForFloodFillRipple(views::InkDrop::Get(this), |
| /*highlight_on_hover=*/true, |
| /*highlight_on_focus=*/true); |
| } |
| |
| protected: |
| AppListViewDelegate* const delegate_; |
| }; |
| |
| // RedoButton ------------------------------------------------------------------ |
| |
| // A button for reverting the temporary sort order if any. |
| // TODO(https://crbug.com/1263999): remove `RedoButton` when the app list sort |
| // is enabled as default. |
| class RedoButton : public SortUiControl { |
| public: |
| METADATA_HEADER(RedoButton); |
| |
| explicit RedoButton(AppListViewDelegate* delegate) |
| : SortUiControl(delegate, |
| base::BindRepeating(&RedoButton::RevertAppListSort, |
| base::Unretained(this))) {} |
| RedoButton(const RedoButton&) = delete; |
| RedoButton& operator=(const RedoButton&) = delete; |
| ~RedoButton() override = default; |
| |
| private: |
| // views::View: |
| void OnThemeChanged() override { |
| SortUiControl::OnThemeChanged(); |
| const SkColor icon_color = AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kButtonIconColor); |
| SetImage(views::Button::STATE_NORMAL, |
| gfx::CreateVectorIcon(kSmallCloseButtonIcon, |
| GetPreferredSize().width(), icon_color)); |
| } |
| |
| void RevertAppListSort() { |
| views::InkDrop::Get(this)->GetInkDrop()->AnimateToState( |
| views::InkDropState::ACTION_TRIGGERED); |
| AppListModelProvider::Get() |
| ->model() |
| ->delegate() |
| ->RequestAppListSortRevert(); |
| } |
| }; |
| |
| BEGIN_METADATA(RedoButton, views::View) |
| END_METADATA |
| |
| // SortUiControlContainer ------------------------------------------------------ |
| |
| class SortUiControlContainer : public views::View { |
| public: |
| METADATA_HEADER(SortUiControlContainer); |
| |
| explicit SortUiControlContainer(AppListViewDelegate* delegate) { |
| // The layer is required in animation. |
| SetPaintToLayer(ui::LayerType::LAYER_NOT_DRAWN); |
| |
| // Configure the layout. |
| auto box_layout = std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kHorizontal, |
| /*inside_border_insets=*/gfx::Insets(), |
| /*between_child_spacing=*/kSortUiControlSpacing); |
| box_layout->set_main_axis_alignment( |
| views::BoxLayout::MainAxisAlignment::kCenter); |
| box_layout->set_cross_axis_alignment( |
| views::BoxLayout::CrossAxisAlignment::kCenter); |
| SetLayoutManager(std::move(box_layout)); |
| |
| // Add children. |
| AddChildView(std::make_unique<RedoButton>(delegate)); |
| |
| GetViewAccessibility().OverrideIsIgnored(true); |
| } |
| SortUiControlContainer(const SortUiControlContainer&) = delete; |
| SortUiControlContainer& operator=(const SortUiControlContainer&) = delete; |
| ~SortUiControlContainer() override = default; |
| }; |
| |
| BEGIN_METADATA(SortUiControlContainer, views::View) |
| END_METADATA |
| |
| } // namespace |
| |
| // A view that contains continue section, recent apps and a separator view, |
| // which is shown when any of other views is shown. |
| // The view is intended to be a wrapper around suggested content views that |
| // makes applying identical transforms to suggested content views easier. |
| class AppsContainerView::ContinueContainer : public views::View { |
| public: |
| ContinueContainer(AppsContainerView* apps_container, |
| AppListViewDelegate* view_delegate) { |
| SetPaintToLayer(ui::LAYER_NOT_DRAWN); |
| |
| SetLayoutManager(std::make_unique<views::FlexLayout>()) |
| ->SetOrientation(views::LayoutOrientation::kVertical); |
| |
| continue_section_ = AddChildView(std::make_unique<ContinueSectionView>( |
| view_delegate, kContinueColumnCount, /*tablet_mode=*/true)); |
| continue_section_->SetPaintToLayer(); |
| continue_section_->layer()->SetFillsBoundsOpaquely(false); |
| continue_section_->UpdateSuggestionTasks(); |
| |
| recent_apps_ = AddChildView( |
| std::make_unique<RecentAppsView>(apps_container, view_delegate)); |
| recent_apps_->SetPaintToLayer(); |
| recent_apps_->layer()->SetFillsBoundsOpaquely(false); |
| |
| separator_ = AddChildView(std::make_unique<views::Separator>()); |
| DCHECK(ColorProvider::Get()); |
| separator_->SetColor(ColorProvider::Get()->GetContentLayerColor( |
| ColorProvider::ContentLayerType::kSeparatorColor)); |
| separator_->SetPreferredSize( |
| gfx::Size(kSeparatorWidth, views::Separator::kThickness)); |
| // Initially set the vertical inset to kRegularSeparatorVerticalInset. The |
| // value will be updated in `AppsContainerView::UpdateAppListConfig()` |
| separator_->SetProperty(views::kMarginsKey, |
| gfx::Insets(kRegularSeparatorVerticalInset, 0)); |
| separator_->SetPaintToLayer(); |
| separator_->layer()->SetFillsBoundsOpaquely(false); |
| separator_->SetProperty(views::kCrossAxisAlignmentKey, |
| views::LayoutAlignment::kCenter); |
| |
| UpdateRecentAppsMargins(); |
| UpdateSeparatorVisibility(); |
| } |
| |
| // views::View: |
| void ChildVisibilityChanged(views::View* child) override { |
| if (child == recent_apps_ || child == continue_section_) |
| UpdateSeparatorVisibility(); |
| |
| if (child == continue_section_) |
| UpdateRecentAppsMargins(); |
| } |
| |
| void OnThemeChanged() override { |
| views::View::OnThemeChanged(); |
| separator_->SetColor(ColorProvider::Get()->GetContentLayerColor( |
| ColorProvider::ContentLayerType::kSeparatorColor)); |
| } |
| |
| bool HasRecentApps() const { |
| return recent_apps_ && recent_apps_->GetVisible(); |
| } |
| |
| void UpdateAppListConfig(AppListConfig* config) { |
| if (recent_apps_) |
| recent_apps_->UpdateAppListConfig(config); |
| |
| const int separator_vertical_inset = |
| config->type() == AppListConfigType::kRegular |
| ? kRegularSeparatorVerticalInset |
| : kDenseSeparatorVerticalInset; |
| separator_->SetProperty(views::kMarginsKey, |
| gfx::Insets(separator_vertical_inset, 0)); |
| } |
| |
| ContinueSectionView* continue_section() { return continue_section_; } |
| RecentAppsView* recent_apps() { return recent_apps_; } |
| views::View* separator() { return separator_; } |
| |
| private: |
| void UpdateRecentAppsMargins() { |
| if (!recent_apps_ || !continue_section_) |
| return; |
| // Remove recent apps top margin if continue section is hidden. |
| recent_apps_->SetProperty( |
| views::kMarginsKey, |
| gfx::Insets(continue_section_->GetVisible() ? kRecentAppsTopMargin : 0, |
| 0, 0, 0)); |
| } |
| |
| void UpdateSeparatorVisibility() { |
| if (!separator_ || !recent_apps_ || !continue_section_) |
| return; |
| separator_->SetVisible(recent_apps_->GetVisible() || |
| continue_section_->GetVisible()); |
| } |
| |
| ContinueSectionView* continue_section_ = nullptr; |
| RecentAppsView* recent_apps_ = nullptr; |
| views::Separator* separator_ = nullptr; |
| }; |
| |
| AppsContainerView::AppsContainerView(ContentsView* contents_view) |
| : contents_view_(contents_view) { |
| AppListModelProvider::Get()->AddObserver(this); |
| |
| SetPaintToLayer(ui::LAYER_NOT_DRAWN); |
| |
| scrollable_container_ = AddChildView(std::make_unique<views::View>()); |
| scrollable_container_->SetPaintToLayer(ui::LAYER_NOT_DRAWN); |
| AppListViewDelegate* view_delegate = |
| contents_view_->GetAppListMainView()->view_delegate(); |
| |
| // The bounds of the |scrollable_container_| will visually clip the |
| // |continue_container_| and |apps_grid_view_| layers. |
| scrollable_container_->layer()->SetMasksToBounds(true); |
| |
| if (features::IsProductivityLauncherEnabled()) { |
| continue_container_ = scrollable_container_->AddChildView( |
| std::make_unique<ContinueContainer>(this, view_delegate)); |
| } else { |
| // Add child view at index 0 so focus traversal goes to suggestion chips |
| // before the views in the scrollable_container. |
| suggestion_chip_container_view_ = AddChildViewAt( |
| std::make_unique<SuggestionChipContainerView>(contents_view), 0); |
| } |
| |
| AppListA11yAnnouncer* a11y_announcer = |
| contents_view->app_list_view()->a11y_announcer(); |
| // Add `apps_grid_view_` at index 0 to put it at the back and ensure other |
| // views in the `scrollable_container` get events first, since the grid |
| // overlaps in bounds with these other views. |
| apps_grid_view_ = scrollable_container_->AddChildViewAt( |
| std::make_unique<PagedAppsGridView>(contents_view, a11y_announcer, |
| /*folder_delegate=*/nullptr, |
| /*folder_controller=*/this, |
| /*container_delegate=*/this), |
| 0); |
| apps_grid_view_->Init(); |
| apps_grid_view_->pagination_model()->AddObserver(this); |
| if (features::IsProductivityLauncherEnabled()) |
| apps_grid_view_->set_margin_for_gradient_mask(kDefaultFadeoutMaskHeight); |
| |
| // Page switcher should be initialized after AppsGridView. |
| auto page_switcher = std::make_unique<PageSwitcher>( |
| apps_grid_view_->pagination_model(), true /* vertical */, |
| contents_view->app_list_view()->is_tablet_mode()); |
| page_switcher_ = AddChildView(std::move(page_switcher)); |
| |
| auto app_list_folder_view = std::make_unique<AppListFolderView>( |
| this, apps_grid_view_, contents_view_, a11y_announcer, view_delegate); |
| folder_background_view_ = AddChildView( |
| std::make_unique<FolderBackgroundView>(app_list_folder_view.get())); |
| |
| app_list_folder_view_ = AddChildView(std::move(app_list_folder_view)); |
| // The folder view is initially hidden. |
| app_list_folder_view_->SetVisible(false); |
| |
| if (features::IsLauncherAppSortEnabled()) { |
| sort_button_container_ = |
| AddChildView(std::make_unique<SortUiControlContainer>(view_delegate)); |
| } |
| |
| // NOTE: At this point, the apps grid folder and recent apps grids are not |
| // fully initialized - they require an `app_list_config_` instance (because |
| // they contain AppListItemView), which in turn requires widget, and the |
| // view's contents bounds to be correctly calculated. The initialization |
| // will be completed in `OnBoundsChanged()` when the apps container bounds are |
| // first set. |
| } |
| |
| AppsContainerView::~AppsContainerView() { |
| AppListModelProvider::Get()->RemoveObserver(this); |
| apps_grid_view_->pagination_model()->RemoveObserver(this); |
| |
| // Make sure |page_switcher_| is deleted before |apps_grid_view_| because |
| // |page_switcher_| uses the PaginationModel owned by |apps_grid_view_|. |
| delete page_switcher_; |
| |
| // App list folder view, if shown, may reference/observe a root apps grid view |
| // item (associated with the item for which the folder is shown). Delete |
| // `app_list_folder_view_` explicitly to ensure it's deleted before |
| // `apps_grid_view_`. |
| delete app_list_folder_view_; |
| } |
| |
| void AppsContainerView::UpdateTopLevelGridDimensions() { |
| const GridLayout grid_layout = CalculateGridLayout(); |
| if (features::IsProductivityLauncherEnabled()) { |
| apps_grid_view_->SetMaxColumnsAndRows( |
| /*max_columns=*/grid_layout.columns, |
| /*max_rows_on_first_page=*/grid_layout.first_page_rows, |
| /*max_rows=*/grid_layout.rows); |
| } else { |
| apps_grid_view_->SetMaxColumnsAndRows( |
| /*max_columns=*/grid_layout.columns, |
| /*max_rows_on_first_page=*/grid_layout.first_page_rows, |
| /*max_rows=*/grid_layout.rows); |
| } |
| } |
| |
| gfx::Rect AppsContainerView::CalculateAvailableBoundsForAppsGrid( |
| const gfx::Rect& contents_bounds) const { |
| gfx::Rect available_bounds = contents_bounds; |
| // Reserve horizontal margins to accommodate page switcher. |
| available_bounds.Inset(GetMinHorizontalMarginForAppsGrid(), 0); |
| // Reserve vertical space for search box and suggestion chips. |
| available_bounds.Inset( |
| 0, |
| GetMinTopMarginForAppsGrid( |
| contents_view_->GetSearchBoxSize(AppListState::kStateApps)), |
| 0, 0); |
| // Subtracts apps grid view insets from space available for apps grid. |
| available_bounds.Inset(0, kGridVerticalMargin); |
| |
| return available_bounds; |
| } |
| |
| void AppsContainerView::UpdateAppListConfig(const gfx::Rect& contents_bounds) { |
| // For productivity launcher, the rows for this grid layout will be ignored |
| // during creation of a new config. |
| GridLayout grid_layout = CalculateGridLayout(); |
| |
| const gfx::Rect available_bounds = |
| CalculateAvailableBoundsForAppsGrid(contents_bounds); |
| |
| std::unique_ptr<AppListConfig> new_config = |
| AppListConfigProvider::Get().CreateForFullscreenAppList( |
| display::Screen::GetScreen() |
| ->GetDisplayNearestView(GetWidget()->GetNativeView()) |
| .work_area() |
| .size(), |
| grid_layout.rows, grid_layout.columns, available_bounds.size(), |
| app_list_config_.get()); |
| |
| // `CreateForFullscreenAppList()` will create a new config only if it differs |
| // from the current `app_list_config_`. Nothing to do if the old |
| // `AppListConfig` can be used for the updated apps container bounds. |
| if (!new_config) |
| return; |
| |
| // Keep old config around until child views have been updated to use the new |
| // config. |
| auto old_config = std::move(app_list_config_); |
| app_list_config_ = std::move(new_config); |
| |
| // Invalidate the cached container margins - app list config change generally |
| // changes preferred apps grid margins, which can influence the container |
| // margins. |
| cached_container_margins_ = CachedContainerMargins(); |
| |
| apps_grid_view()->UpdateAppListConfig(app_list_config_.get()); |
| app_list_folder_view()->UpdateAppListConfig(app_list_config_.get()); |
| if (continue_container_) |
| continue_container_->UpdateAppListConfig(app_list_config_.get()); |
| } |
| |
| void AppsContainerView::OnActiveAppListModelsChanged( |
| AppListModel* model, |
| SearchModel* search_model) { |
| // Nothing to do if the apps grid views have not yet been initialized. |
| if (!app_list_config_) |
| return; |
| |
| UpdateForActiveAppListModel(); |
| } |
| |
| void AppsContainerView::ShowFolderForItemView( |
| AppListItemView* folder_item_view) { |
| // Prevent new animations from starting if there are currently animations |
| // pending. This fixes crbug.com/357099. |
| if (app_list_folder_view_->IsAnimationRunning()) |
| return; |
| |
| DCHECK(folder_item_view->is_folder()); |
| |
| UMA_HISTOGRAM_ENUMERATION("Apps.AppListFolderOpened", |
| kFullscreenAppListFolders, kMaxFolderOpened); |
| |
| app_list_folder_view_->ConfigureForFolderItemView(folder_item_view); |
| SetShowState(SHOW_ACTIVE_FOLDER, false); |
| |
| // If there is no selected view in the root grid when a folder is opened, |
| // silently focus the first item in the folder to avoid showing the selection |
| // highlight or announcing to A11y, but still ensuring the arrow keys navigate |
| // from the first item. |
| const bool silently = !apps_grid_view()->has_selected_view(); |
| app_list_folder_view_->FocusFirstItem(silently); |
| |
| // Disable all the items behind the folder so that they will not be reached |
| // during focus traversal. |
| DisableFocusForShowingActiveFolder(true); |
| } |
| |
| void AppsContainerView::ShowApps(AppListItemView* folder_item_view, |
| bool select_folder) { |
| DVLOG(1) << __FUNCTION__; |
| if (app_list_folder_view_->IsAnimationRunning()) |
| return; |
| |
| const bool animate = !!folder_item_view; |
| SetShowState(SHOW_APPS, animate); |
| DisableFocusForShowingActiveFolder(false); |
| if (folder_item_view) { |
| // Focus `folder_item_view` but only show the selection highlight if there |
| // was already one showing. |
| if (select_folder) |
| folder_item_view->RequestFocus(); |
| else |
| folder_item_view->SilentlyRequestFocus(); |
| } |
| } |
| |
| void AppsContainerView::ResetForShowApps() { |
| DVLOG(1) << __FUNCTION__; |
| UpdateSuggestionChips(); |
| UpdateRecentApps(); |
| SetShowState(SHOW_APPS, false); |
| DisableFocusForShowingActiveFolder(false); |
| } |
| |
| void AppsContainerView::SetDragAndDropHostOfCurrentAppList( |
| ApplicationDragAndDropHost* drag_and_drop_host) { |
| apps_grid_view()->SetDragAndDropHostOfCurrentAppList(drag_and_drop_host); |
| app_list_folder_view()->items_grid_view()->SetDragAndDropHostOfCurrentAppList( |
| drag_and_drop_host); |
| } |
| |
| void AppsContainerView::ReparentFolderItemTransit( |
| AppListFolderItem* folder_item) { |
| if (app_list_folder_view_->IsAnimationRunning()) |
| return; |
| SetShowState(SHOW_ITEM_REPARENT, false); |
| DisableFocusForShowingActiveFolder(false); |
| } |
| |
| bool AppsContainerView::IsInFolderView() const { |
| return show_state_ == SHOW_ACTIVE_FOLDER; |
| } |
| |
| void AppsContainerView::ReparentDragEnded() { |
| DVLOG(1) << __FUNCTION__; |
| // The container will be showing apps if the folder was deleted mid-drag. |
| if (show_state_ == SHOW_APPS) |
| return; |
| DCHECK_EQ(SHOW_ITEM_REPARENT, show_state_); |
| show_state_ = AppsContainerView::SHOW_APPS; |
| } |
| |
| // PaginationModelObserver: |
| void AppsContainerView::SelectedPageChanged(int old_selected, |
| int new_selected) { |
| // There is no |continue_container_| to translate when productivity launcher |
| // is not enabled, so return early. |
| if (!features::IsProductivityLauncherEnabled()) |
| return; |
| |
| // |continue_container_| is hidden above the grid when not on the first page. |
| gfx::Transform transform; |
| gfx::Vector2dF translate; |
| translate.set_y(-scrollable_container_->bounds().height() * new_selected); |
| transform.Translate(translate); |
| continue_container_->layer()->SetTransform(transform); |
| } |
| |
| void AppsContainerView::TransitionChanged() { |
| // There is no |continue_container_| to translate when productivity launcher |
| // is not enabled, so return early. |
| if (!features::IsProductivityLauncherEnabled()) |
| return; |
| |
| auto* pagination_model = apps_grid_view_->pagination_model(); |
| const PaginationModel::Transition& transition = |
| pagination_model->transition(); |
| if (!pagination_model->is_valid_page(transition.target_page)) |
| return; |
| |
| // Because |continue_container_| only shows on the first page, only update its |
| // transform if its page is involved in the transition. Otherwise, there is |
| // no need to transform the |continue_container_| because it will remain |
| // hidden throughout the transition. |
| if (transition.target_page == 0 || pagination_model->selected_page() == 0) { |
| const int page_height = scrollable_container_->bounds().height(); |
| gfx::Vector2dF translate; |
| |
| if (transition.target_page == 0) { |
| // Scroll the continue section down from above. |
| translate.set_y(-page_height + page_height * transition.progress); |
| } else { |
| // Scroll the continue section upwards |
| translate.set_y(-page_height * transition.progress); |
| } |
| gfx::Transform transform; |
| transform.Translate(translate); |
| continue_container_->layer()->SetTransform(transform); |
| } |
| } |
| |
| void AppsContainerView::TransitionStarted() { |
| MaybeCreateGradientMask(); |
| } |
| |
| void AppsContainerView::TransitionEnded() { |
| // TODO(crbug.com/1285184): Sometimes gradient mask is not removed because |
| // this function does not get called in some cases. |
| |
| // Gradient mask is no longer necessary once transition is finished. |
| MaybeRemoveGradientMask(); |
| } |
| |
| void AppsContainerView::ScrollStarted() { |
| MaybeCreateGradientMask(); |
| } |
| |
| void AppsContainerView::ScrollEnded() { |
| // Need to reset the mask because transition will not happen in some |
| // cases. (See https://crbug.com/1049275) |
| MaybeRemoveGradientMask(); |
| } |
| |
| // PagedAppsGridView::ContainerDelegate: |
| bool AppsContainerView::IsPointWithinPageFlipBuffer( |
| const gfx::Point& point_in_apps_grid) const { |
| // The page flip buffer is the work area bounds excluding shelf bounds, which |
| // is the same as AppsContainerView's bounds. |
| gfx::Point point = point_in_apps_grid; |
| ConvertPointToTarget(apps_grid_view_, this, &point); |
| return this->GetContentsBounds().Contains(point); |
| } |
| |
| bool AppsContainerView::IsPointWithinBottomDragBuffer( |
| const gfx::Point& point, |
| int page_flip_zone_size) const { |
| // The bottom drag buffer is between the bottom of apps grid and top of shelf. |
| gfx::Point point_in_parent = point; |
| ConvertPointToTarget(apps_grid_view_, this, &point_in_parent); |
| gfx::Rect parent_rect = this->GetContentsBounds(); |
| const int kBottomDragBufferMax = parent_rect.bottom(); |
| const int kBottomDragBufferMin = scrollable_container_->bounds().bottom() - |
| apps_grid_view_->GetInsets().bottom() - |
| page_flip_zone_size; |
| // TODO(crbug.com/1234064): In ProductivityLauncher, with a variable row size, |
| // the size of the bottom drag buffer can visually change. Figure out how we |
| // want to handle this and update this code to reflect that. |
| return point_in_parent.y() > kBottomDragBufferMin && |
| point_in_parent.y() < kBottomDragBufferMax; |
| } |
| |
| void AppsContainerView::MaybeCreateGradientMask() { |
| if (features::IsBackgroundBlurEnabled()) { |
| if (!layer()->layer_mask_layer() && !gradient_layer_delegate_) { |
| gradient_layer_delegate_ = std::make_unique<GradientLayerDelegate>(); |
| UpdateGradientMaskBounds(); |
| } |
| if (gradient_layer_delegate_) { |
| scrollable_container_->layer()->SetMaskLayer( |
| gradient_layer_delegate_->layer()); |
| } |
| } |
| } |
| |
| void AppsContainerView::MaybeRemoveGradientMask() { |
| if (scrollable_container_->layer()->layer_mask_layer() && |
| !keep_gradient_mask_for_cardified_state_) { |
| scrollable_container_->layer()->SetMaskLayer(nullptr); |
| } |
| } |
| |
| void AppsContainerView::OnCardifiedStateStarted() { |
| keep_gradient_mask_for_cardified_state_ = true; |
| MaybeCreateGradientMask(); |
| } |
| |
| void AppsContainerView::OnCardifiedStateEnded() { |
| keep_gradient_mask_for_cardified_state_ = false; |
| MaybeRemoveGradientMask(); |
| } |
| |
| // RecentAppsView::Delegate: |
| void AppsContainerView::MoveFocusUpFromRecents() { |
| DCHECK(!GetRecentApps()->children().empty()); |
| views::View* first_recent = GetRecentApps()->children()[0]; |
| DCHECK(views::IsViewClass<AppListItemView>(first_recent)); |
| // Find the view one step in reverse from the first recent app. |
| views::View* previous_view = GetFocusManager()->GetNextFocusableView( |
| first_recent, GetWidget(), /*reverse=*/true, /*dont_loop=*/false); |
| DCHECK(previous_view); |
| previous_view->RequestFocus(); |
| } |
| |
| void AppsContainerView::MoveFocusDownFromRecents(int column) { |
| int top_level_item_count = apps_grid_view_->view_model()->view_size(); |
| if (top_level_item_count <= 0) |
| return; |
| // Attempt to focus the item at `column` in the first row, or the last item if |
| // there aren't enough items. This could happen if the user's apps are in a |
| // small number of folders. |
| int index = std::min(column, top_level_item_count - 1); |
| AppListItemView* item = apps_grid_view_->GetItemViewAt(index); |
| DCHECK(item); |
| item->RequestFocus(); |
| } |
| |
| ContinueSectionView* AppsContainerView::GetContinueSection() { |
| if (!continue_container_) |
| return nullptr; |
| return continue_container_->continue_section(); |
| } |
| |
| RecentAppsView* AppsContainerView::GetRecentApps() { |
| if (!continue_container_) |
| return nullptr; |
| return continue_container_->recent_apps(); |
| } |
| |
| views::View* AppsContainerView::GetSeparatorView() { |
| if (!continue_container_) |
| return nullptr; |
| return continue_container_->separator(); |
| } |
| |
| void AppsContainerView::UpdateControlVisibility(AppListViewState app_list_state, |
| bool is_in_drag) { |
| if (app_list_state == AppListViewState::kClosed) |
| return; |
| |
| SetCanProcessEventsWithinSubtree( |
| app_list_state == AppListViewState::kFullscreenAllApps || |
| app_list_state == AppListViewState::kPeeking); |
| |
| apps_grid_view_->UpdateControlVisibility(app_list_state, is_in_drag); |
| page_switcher_->SetVisible( |
| is_in_drag || app_list_state == AppListViewState::kFullscreenAllApps || |
| app_list_state == AppListViewState::kFullscreenSearch); |
| |
| // Ignore button press during dragging to avoid app list item views' opacity |
| // being set to wrong value. |
| page_switcher_->set_ignore_button_press(is_in_drag); |
| |
| if (suggestion_chip_container_view_) { |
| suggestion_chip_container_view_->SetVisible( |
| app_list_state == AppListViewState::kFullscreenAllApps || |
| app_list_state == AppListViewState::kPeeking || is_in_drag); |
| } |
| } |
| |
| void AppsContainerView::AnimateOpacity(float current_progress, |
| AppListViewState target_view_state, |
| const OpacityAnimator& animator) { |
| if (suggestion_chip_container_view_) { |
| const bool target_suggestion_chip_visibility = |
| target_view_state == AppListViewState::kFullscreenAllApps || |
| target_view_state == AppListViewState::kPeeking; |
| animator.Run(suggestion_chip_container_view_, |
| target_suggestion_chip_visibility); |
| } |
| |
| if (!apps_grid_view_->layer()->GetAnimator()->IsAnimatingProperty( |
| ui::LayerAnimationElement::OPACITY)) { |
| apps_grid_view_->UpdateOpacity(true /*restore_opacity*/, |
| kAppsOpacityChangeStart, |
| kAppsOpacityChangeEnd); |
| apps_grid_view_->layer()->SetOpacity(current_progress > 1.0f ? 1.0f : 0.0f); |
| } |
| |
| const bool target_grid_visibility = |
| target_view_state == AppListViewState::kFullscreenAllApps || |
| target_view_state == AppListViewState::kFullscreenSearch; |
| animator.Run(apps_grid_view_, target_grid_visibility); |
| animator.Run(page_switcher_, target_grid_visibility); |
| } |
| |
| void AppsContainerView::AnimateYPosition(AppListViewState target_view_state, |
| const TransformAnimator& animator, |
| float default_offset) { |
| // Apps container position is calculated for app list progress relative to |
| // peeking state, which may not match the progress value used to calculate |
| // |default_offset| - when showing search results page, the transform offset |
| // is calculated using progress relative to AppListViewState::kHalf. |
| const float progress = |
| contents_view_->app_list_view()->GetAppListTransitionProgress( |
| AppListView::kProgressFlagNone | |
| AppListView::kProgressFlagWithTransform); |
| const int current_suggestion_chip_y = GetExpectedSuggestionChipY(progress); |
| const int target_suggestion_chip_y = GetExpectedSuggestionChipY( |
| AppListView::GetTransitionProgressForState(target_view_state)); |
| const int offset = current_suggestion_chip_y - target_suggestion_chip_y; |
| |
| if (suggestion_chip_container_view_) { |
| suggestion_chip_container_view_->SetY(target_suggestion_chip_y); |
| animator.Run(offset, suggestion_chip_container_view_->layer()); |
| } |
| |
| scrollable_container_->SetY(target_suggestion_chip_y + chip_grid_y_distance_); |
| animator.Run(offset, scrollable_container_->layer()); |
| page_switcher_->SetY(target_suggestion_chip_y + chip_grid_y_distance_); |
| animator.Run(offset, page_switcher_->layer()); |
| |
| if (features::IsLauncherAppSortEnabled()) { |
| sort_button_container_->SetY(target_suggestion_chip_y); |
| animator.Run(offset, sort_button_container_->layer()); |
| } |
| } |
| |
| void AppsContainerView::OnTabletModeChanged(bool started) { |
| if (suggestion_chip_container_view_) |
| suggestion_chip_container_view_->OnTabletModeChanged(started); |
| apps_grid_view_->OnTabletModeChanged(started); |
| app_list_folder_view_->OnTabletModeChanged(started); |
| page_switcher_->set_is_tablet_mode(started); |
| } |
| |
| void AppsContainerView::Layout() { |
| gfx::Rect rect(GetContentsBounds()); |
| if (rect.IsEmpty()) |
| return; |
| |
| // Layout suggestion chips. |
| gfx::Rect chip_container_rect = rect; |
| chip_container_rect.set_y(GetExpectedSuggestionChipY( |
| contents_view_->app_list_view()->GetAppListTransitionProgress( |
| AppListView::kProgressFlagNone))); |
| |
| if (suggestion_chip_container_view_) { |
| chip_container_rect.set_height(kSuggestionChipContainerHeight); |
| chip_container_rect.Inset(GetIdealHorizontalMargin(), 0); |
| suggestion_chip_container_view_->SetBoundsRect(chip_container_rect); |
| } else { |
| chip_container_rect.set_height(0); |
| } |
| |
| // Set bounding box for the folder view - the folder may overlap with |
| // suggestion chips, but not the search box. |
| gfx::Rect folder_bounding_box = rect; |
| folder_bounding_box.Inset(kFolderMargin, chip_container_rect.y(), |
| kFolderMargin, kFolderMargin); |
| app_list_folder_view_->SetBoundingBox(folder_bounding_box); |
| |
| // Leave the same available bounds for the apps grid view in both |
| // fullscreen and peeking state to avoid resizing the view during |
| // animation and dragging, which is an expensive operation. |
| rect.set_y(chip_container_rect.bottom()); |
| rect.set_height(rect.height() - |
| GetExpectedSuggestionChipY(kAppListFullscreenProgressValue) - |
| chip_container_rect.height()); |
| |
| // Layout apps grid. |
| const gfx::Insets grid_insets = apps_grid_view_->GetInsets(); |
| const gfx::Insets margins = CalculateMarginsForAvailableBounds( |
| GetContentsBounds(), |
| contents_view_->GetSearchBoxSize(AppListState::kStateApps)); |
| gfx::Rect grid_rect = rect; |
| grid_rect.Inset(margins.left(), kGridVerticalMargin, margins.right(), |
| margins.bottom()); |
| // The grid rect insets are added to calculated margins. Given that the |
| // grid bounds rect should include insets, they have to be removed from |
| // added margins. |
| grid_rect.Inset(-grid_insets); |
| |
| gfx::Rect scrollable_bounds = grid_rect; |
| // With productivity launcher enabled, add space to the top of the |
| // `scrollable_container_` bounds to make room for the gradient mask to be |
| // placed above the continue section. |
| if (features::IsProductivityLauncherEnabled()) |
| scrollable_bounds.Inset(0, -kDefaultFadeoutMaskHeight, 0, 0); |
| scrollable_container_->SetBoundsRect(scrollable_bounds); |
| |
| if (gradient_layer_delegate_) |
| UpdateGradientMaskBounds(); |
| |
| bool first_page_config_changed = false; |
| if (features::IsProductivityLauncherEnabled()) { |
| const int continue_container_height = |
| continue_container_->GetPreferredSize().height(); |
| continue_container_->SetBoundsRect(gfx::Rect(0, kDefaultFadeoutMaskHeight, |
| grid_rect.width(), |
| continue_container_height)); |
| // Setting this offset prevents the app items in the grid from overlapping |
| // with the continue section. |
| first_page_config_changed = apps_grid_view_->ConfigureFirstPagePadding( |
| continue_container_height, continue_container_->HasRecentApps()); |
| } |
| |
| // Make sure that UpdateTopLevelGridDimensions() happens after setting the |
| // apps grid's first page offset, because it can change the number of rows |
| // shown in the grid. |
| UpdateTopLevelGridDimensions(); |
| |
| gfx::Rect apps_grid_bounds(grid_rect.size()); |
| // Set the apps grid bounds y to make room for the top gradient mask. |
| if (features::IsProductivityLauncherEnabled()) |
| apps_grid_bounds.set_y(kDefaultFadeoutMaskHeight); |
| |
| if (apps_grid_view_->bounds() != apps_grid_bounds) { |
| apps_grid_view_->SetBoundsRect(apps_grid_bounds); |
| } else if (first_page_config_changed) { |
| // Apps grid layout depends on the continue container bounds, so explicitly |
| // call layout to ensure apps grid view gets laid out even if its bounds do |
| // not change. |
| apps_grid_view_->Layout(); |
| } |
| |
| // Record the distance of y position between suggestion chip container |
| // and apps grid view to avoid duplicate calculation of apps grid view's |
| // y position during dragging. |
| chip_grid_y_distance_ = scrollable_container_->y() - chip_container_rect.y(); |
| |
| // Layout page switcher. |
| const int page_switcher_width = page_switcher_->GetPreferredSize().width(); |
| const gfx::Rect page_switcher_bounds( |
| grid_rect.right() + kGridToPageSwitcherMargin, grid_rect.y(), |
| page_switcher_width, grid_rect.height()); |
| page_switcher_->SetBoundsRect(page_switcher_bounds); |
| |
| if (features::IsLauncherAppSortEnabled()) { |
| // Align `sort_button_container_` with the bottom of the |
| // `suggestion_chip_container_view_` horizontally; align |
| // `sort_button_container_` with `page_switcher_bounds` vertically on the |
| // right edge. |
| const int sort_button_container_width = |
| sort_button_container_->GetPreferredSize().width(); |
| gfx::Rect sort_button_container_rect( |
| page_switcher_bounds.right() - sort_button_container_width, |
| chip_container_rect.bottom(), sort_button_container_width, 20); |
| sort_button_container_->SetBoundsRect(sort_button_container_rect); |
| } |
| |
| switch (show_state_) { |
| case SHOW_APPS: |
| break; |
| case SHOW_ACTIVE_FOLDER: { |
| app_list_folder_view_->UpdatePreferredBounds(); |
| folder_background_view_->SetBoundsRect(rect); |
| app_list_folder_view_->SetBoundsRect( |
| app_list_folder_view_->preferred_bounds()); |
| break; |
| } |
| case SHOW_ITEM_REPARENT: |
| case SHOW_NONE: |
| break; |
| } |
| } |
| |
| bool AppsContainerView::OnKeyPressed(const ui::KeyEvent& event) { |
| if (show_state_ == SHOW_APPS) |
| return apps_grid_view_->OnKeyPressed(event); |
| else |
| return app_list_folder_view_->OnKeyPressed(event); |
| } |
| |
| const char* AppsContainerView::GetClassName() const { |
| return "AppsContainerView"; |
| } |
| |
| void AppsContainerView::OnBoundsChanged(const gfx::Rect& old_bounds) { |
| const bool creating_initial_config = !app_list_config_; |
| |
| // The size and layout of apps grid items depend on the dimensions of the |
| // display on which the apps container is shown. Given that the apps container |
| // is shown in fullscreen app list view (and covers complete app list view |
| // bounds), changes in the `AppsContainerView` bounds can be used as a proxy |
| // to detect display size changes. |
| UpdateAppListConfig(GetContentsBounds()); |
| DCHECK(app_list_config_); |
| UpdateTopLevelGridDimensions(); |
| |
| // Finish initialization of views that require app list config. |
| if (creating_initial_config) |
| UpdateForActiveAppListModel(); |
| } |
| |
| void AppsContainerView::OnGestureEvent(ui::GestureEvent* event) { |
| // Ignore tap/long-press, allow those to pass to the ancestor view. |
| if (event->type() == ui::ET_GESTURE_TAP || |
| event->type() == ui::ET_GESTURE_LONG_PRESS) { |
| return; |
| } |
| |
| // Will forward events to |apps_grid_view_| if they occur in the same y-region |
| if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN && |
| event->location().y() <= apps_grid_view_->bounds().y()) { |
| return; |
| } |
| |
| // If a folder is currently opening or closing, we should ignore the event. |
| // This is here until the animation for pagination while closing folders is |
| // fixed: https://crbug.com/875133 |
| if (app_list_folder_view_->IsAnimationRunning()) { |
| event->SetHandled(); |
| return; |
| } |
| |
| // Temporary event for use by |apps_grid_view_| |
| ui::GestureEvent grid_event(*event); |
| ConvertEventToTarget(apps_grid_view_, &grid_event); |
| apps_grid_view_->OnGestureEvent(&grid_event); |
| |
| // If the temporary event was handled, we don't want to handle it again. |
| if (grid_event.handled()) |
| event->SetHandled(); |
| } |
| |
| void AppsContainerView::OnShown() { |
| DVLOG(1) << __FUNCTION__; |
| // Explicitly hide the virtual keyboard before showing the apps container |
| // view. This prevents the virtual keyboard's "transient blur" feature from |
| // kicking in - if a text input loses focus, and a text input gains it within |
| // seconds, the virtual keyboard gets reshown. This is undesirable behavior |
| // for the app list (where search box gets focus by default). |
| if (keyboard::KeyboardUIController::HasInstance()) |
| keyboard::KeyboardUIController::Get()->HideKeyboardExplicitlyBySystem(); |
| |
| GetViewAccessibility().OverrideIsLeaf(false); |
| } |
| |
| void AppsContainerView::OnWillBeHidden() { |
| DVLOG(1) << __FUNCTION__; |
| if (show_state_ == SHOW_APPS || show_state_ == SHOW_ITEM_REPARENT) |
| apps_grid_view_->EndDrag(true); |
| else if (show_state_ == SHOW_ACTIVE_FOLDER) |
| app_list_folder_view_->CloseFolderPage(); |
| } |
| |
| void AppsContainerView::OnHidden() { |
| // Apps container view is shown faded behind the search results UI - hide its |
| // contents from the screen reader as the apps grid is not normally |
| // actionable in this state. |
| GetViewAccessibility().OverrideIsLeaf(true); |
| } |
| |
| void AppsContainerView::OnAnimationStarted(AppListState from_state, |
| AppListState to_state) { |
| gfx::Rect contents_bounds = GetDefaultContentsBounds(); |
| |
| const gfx::Rect from_rect = |
| GetPageBoundsForState(from_state, contents_bounds, gfx::Rect()); |
| const gfx::Rect to_rect = |
| GetPageBoundsForState(to_state, contents_bounds, gfx::Rect()); |
| if (from_rect != to_rect) { |
| DCHECK_EQ(from_rect.size(), to_rect.size()); |
| DCHECK_EQ(from_rect.x(), to_rect.x()); |
| |
| SetBoundsRect(to_rect); |
| |
| gfx::Transform initial_transform; |
| initial_transform.Translate(0, from_rect.y() - to_rect.y()); |
| layer()->SetTransform(initial_transform); |
| |
| auto settings = contents_view_->CreateTransitionAnimationSettings(layer()); |
| layer()->SetTransform(gfx::Transform()); |
| } |
| |
| // Set the page opacity. |
| auto settings = contents_view_->CreateTransitionAnimationSettings(layer()); |
| UpdateContainerOpacityForState(to_state); |
| } |
| |
| void AppsContainerView::UpdatePageOpacityForState(AppListState state, |
| float search_box_opacity, |
| bool restore_opacity) { |
| UpdateContainerOpacityForState(state); |
| |
| const float progress = |
| contents_view_->app_list_view()->GetAppListTransitionProgress( |
| AppListView::kProgressFlagNone); |
| UpdateContentsOpacity(progress, restore_opacity); |
| } |
| |
| void AppsContainerView::UpdatePageBoundsForState( |
| AppListState state, |
| const gfx::Rect& contents_bounds, |
| const gfx::Rect& search_box_bounds) { |
| AppListPage::UpdatePageBoundsForState(state, contents_bounds, |
| search_box_bounds); |
| |
| const float progress = |
| contents_view_->app_list_view()->GetAppListTransitionProgress( |
| AppListView::kProgressFlagNone); |
| UpdateContentsYPosition(progress); |
| } |
| |
| gfx::Rect AppsContainerView::GetPageBoundsForState( |
| AppListState state, |
| const gfx::Rect& contents_bounds, |
| const gfx::Rect& search_box_bounds) const { |
| if (state == AppListState::kStateApps) |
| return contents_bounds; |
| |
| gfx::Rect bounds = contents_bounds; |
| bounds.Offset(0, kNonAppsStateVerticalOffset); |
| return bounds; |
| } |
| |
| int AppsContainerView::GetMinHorizontalMarginForAppsGrid() const { |
| return kPageSwitcherEndMargin + kGridToPageSwitcherMargin + |
| page_switcher_->GetPreferredSize().width(); |
| } |
| |
| int AppsContainerView::GetMinTopMarginForAppsGrid( |
| const gfx::Size& search_box_size) const { |
| const int suggestion_chip_container_size = |
| features::IsProductivityLauncherEnabled() |
| ? 0 |
| : kSuggestionChipContainerHeight + kSuggestionChipContainerTopMargin; |
| |
| return search_box_size.height() + kGridVerticalMargin + |
| suggestion_chip_container_size; |
| } |
| |
| int AppsContainerView::GetIdealHorizontalMargin() const { |
| const int available_width = GetContentsBounds().width(); |
| if (available_width >= |
| kAppsGridMarginRatio * GetMinHorizontalMarginForAppsGrid()) { |
| return available_width / kAppsGridMarginRatio; |
| } |
| return available_width / kAppsGridMarginRatioForSmallWidth; |
| } |
| |
| int AppsContainerView::GetIdealVerticalMargin() const { |
| return GetContentsBounds().height() / kAppsGridMarginRatio; |
| } |
| |
| const gfx::Insets& AppsContainerView::CalculateMarginsForAvailableBounds( |
| const gfx::Rect& available_bounds, |
| const gfx::Size& search_box_size) { |
| if (cached_container_margins_.bounds_size == available_bounds.size() && |
| cached_container_margins_.search_box_size == search_box_size) { |
| return cached_container_margins_.margins; |
| } |
| |
| // For productivity launcher, the `grid_layout`'s rows will be ignored because |
| // the vertical margin will be constant. |
| const GridLayout grid_layout = CalculateGridLayout(); |
| const gfx::Size min_grid_size = apps_grid_view()->GetMinimumTileGridSize( |
| grid_layout.columns, grid_layout.rows); |
| const gfx::Size max_grid_size = apps_grid_view()->GetMaximumTileGridSize( |
| grid_layout.columns, grid_layout.rows); |
| |
| int available_height = available_bounds.height(); |
| // Add search box, and suggestion chips container height (with its margins to |
| // search box and apps grid) to non apps grid size. |
| // NOTE: Not removing bottom apps grid inset because they are included into |
| // the total margin values. |
| available_height -= GetMinTopMarginForAppsGrid(search_box_size); |
| |
| // Calculates margin value to ensure the apps grid size is within required |
| // bounds. |
| // |ideal_margin|: The value the margin would have with no restrictions on |
| // grid size. |
| // |available_size|: The available size for apps grid in the dimension where |
| // margin is applied. |
| // |min_size|: The min allowed size for apps grid in the dimension where |
| // margin is applied. |
| // |max_size|: The max allowed size for apps grid in the dimension where |
| // margin is applied. |
| const auto calculate_margin = [](int ideal_margin, int available_size, |
| int min_size, int max_size) -> int { |
| const int ideal_size = available_size - 2 * ideal_margin; |
| if (ideal_size < min_size) |
| return ideal_margin - (min_size - ideal_size + 1) / 2; |
| if (ideal_size > max_size) |
| return ideal_margin + (ideal_size - max_size) / 2; |
| return ideal_margin; |
| }; |
| |
| int vertical_margin = 0; |
| if (features::IsProductivityLauncherEnabled()) { |
| // Productivity launcher does not have a preset number of rows per page. |
| // Instead of adjusting the margins to fit a set number of rows, the grid |
| // will change the number of rows to fit within the provided space. |
| vertical_margin = kGridVerticalMargin; |
| } else { |
| vertical_margin = |
| calculate_margin(GetIdealVerticalMargin(), available_height, |
| min_grid_size.height(), max_grid_size.height()); |
| } |
| |
| const int horizontal_margin = |
| calculate_margin(GetIdealHorizontalMargin(), available_bounds.width(), |
| min_grid_size.width(), max_grid_size.width()); |
| |
| const int min_horizontal_margin = GetMinHorizontalMarginForAppsGrid(); |
| |
| cached_container_margins_.margins = |
| gfx::Insets(std::max(vertical_margin, kGridVerticalMargin), |
| std::max(horizontal_margin, min_horizontal_margin), |
| std::max(vertical_margin, kGridVerticalMargin), |
| std::max(horizontal_margin, min_horizontal_margin)); |
| cached_container_margins_.bounds_size = available_bounds.size(); |
| cached_container_margins_.search_box_size = search_box_size; |
| |
| return cached_container_margins_.margins; |
| } |
| |
| void AppsContainerView::UpdateRecentApps() { |
| if (!GetRecentApps() || !app_list_config_) |
| return; |
| |
| AppListModelProvider* const model_provider = AppListModelProvider::Get(); |
| GetRecentApps()->ShowResults(model_provider->search_model(), |
| model_provider->model()); |
| } |
| |
| void AppsContainerView::UpdateSuggestionChips() { |
| if (!suggestion_chip_container_view_) |
| return; |
| |
| suggestion_chip_container_view_->SetResults( |
| AppListModelProvider::Get()->search_model()->results()); |
| } |
| |
| base::ScopedClosureRunner AppsContainerView::DisableSuggestionChipsBlur() { |
| if (!suggestion_chip_container_view_) |
| return base::ScopedClosureRunner(base::DoNothing()); |
| |
| ++suggestion_chips_blur_disabler_count_; |
| |
| if (suggestion_chips_blur_disabler_count_ == 1) |
| suggestion_chip_container_view_->SetBlurDisabled(true); |
| |
| return base::ScopedClosureRunner( |
| base::BindOnce(&AppsContainerView::OnSuggestionChipsBlurDisablerReleased, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void AppsContainerView::SetShowState(ShowState show_state, |
| bool show_apps_with_animation) { |
| if (show_state_ == show_state) |
| return; |
| |
| show_state_ = show_state; |
| |
| // Layout before showing animation because the animation's target bounds are |
| // calculated based on the layout. |
| Layout(); |
| |
| switch (show_state_) { |
| case SHOW_APPS: |
| page_switcher_->SetCanProcessEventsWithinSubtree(true); |
| folder_background_view_->SetVisible(false); |
| apps_grid_view_->ResetForShowApps(); |
| app_list_folder_view_->ResetItemsGridForClose(); |
| if (show_apps_with_animation) { |
| app_list_folder_view_->ScheduleShowHideAnimation(false, false); |
| } else { |
| app_list_folder_view_->HideViewImmediately(); |
| } |
| break; |
| case SHOW_ACTIVE_FOLDER: |
| page_switcher_->SetCanProcessEventsWithinSubtree(false); |
| folder_background_view_->SetVisible(true); |
| app_list_folder_view_->ScheduleShowHideAnimation(true, false); |
| break; |
| case SHOW_ITEM_REPARENT: |
| page_switcher_->SetCanProcessEventsWithinSubtree(true); |
| folder_background_view_->SetVisible(false); |
| app_list_folder_view_->ScheduleShowHideAnimation(false, true); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void AppsContainerView::UpdateContainerOpacityForState(AppListState state) { |
| const float target_opacity = |
| state == AppListState::kStateApps ? 1.0f : kNonAppsStateOpacity; |
| if (layer()->GetTargetOpacity() != target_opacity) |
| layer()->SetOpacity(target_opacity); |
| } |
| |
| void AppsContainerView::UpdateContentsOpacity(float progress, |
| bool restore_opacity) { |
| apps_grid_view_->UpdateOpacity(restore_opacity, kAppsOpacityChangeStart, |
| kAppsOpacityChangeEnd); |
| |
| // Updates the opacity of page switcher buttons. The same rule as all apps in |
| // AppsGridView. |
| AppListView* app_list_view = contents_view_->app_list_view(); |
| int screen_bottom = app_list_view->GetScreenBottom(); |
| gfx::Rect switcher_bounds = page_switcher_->GetBoundsInScreen(); |
| float centerline_above_work_area = |
| std::max<float>(screen_bottom - switcher_bounds.CenterPoint().y(), 0.f); |
| float opacity = |
| std::min(std::max((centerline_above_work_area - kAppsOpacityChangeStart) / |
| (kAppsOpacityChangeEnd - kAppsOpacityChangeStart), |
| 0.f), |
| 1.0f); |
| page_switcher_->layer()->SetOpacity(restore_opacity ? 1.0f : opacity); |
| |
| if (suggestion_chip_container_view_) { |
| // Changes the opacity of suggestion chips between 0 and 1 when app list |
| // transition progress changes between |kSuggestionChipOpacityStartProgress| |
| // and |kSuggestionChipOpacityEndProgress|. |
| float chips_opacity = |
| base::clamp((progress - kSuggestionChipOpacityStartProgress) / |
| (kSuggestionChipOpacityEndProgress - |
| kSuggestionChipOpacityStartProgress), |
| 0.0f, 1.0f); |
| suggestion_chip_container_view_->layer()->SetOpacity( |
| restore_opacity ? 1.0 : chips_opacity); |
| } |
| } |
| |
| void AppsContainerView::UpdateContentsYPosition(float progress) { |
| const int current_suggestion_chip_y = GetExpectedSuggestionChipY(progress); |
| if (suggestion_chip_container_view_) |
| suggestion_chip_container_view_->SetY(current_suggestion_chip_y); |
| scrollable_container_->SetY(current_suggestion_chip_y + |
| chip_grid_y_distance_); |
| page_switcher_->SetY(current_suggestion_chip_y + chip_grid_y_distance_); |
| |
| // If app list is in drag, reset transforms that might started animating in |
| // AnimateYPosition(). |
| if (contents_view_->app_list_view()->is_in_drag()) { |
| if (suggestion_chip_container_view_) |
| suggestion_chip_container_view_->layer()->SetTransform(gfx::Transform()); |
| scrollable_container_->layer()->SetTransform(gfx::Transform()); |
| page_switcher_->layer()->SetTransform(gfx::Transform()); |
| } |
| } |
| |
| void AppsContainerView::DisableFocusForShowingActiveFolder(bool disabled) { |
| if (suggestion_chip_container_view_) { |
| suggestion_chip_container_view_->DisableFocusForShowingActiveFolder( |
| disabled); |
| } |
| if (auto* recent_apps = GetRecentApps(); recent_apps) { |
| recent_apps->DisableFocusForShowingActiveFolder(disabled); |
| } |
| if (auto* continue_section = GetContinueSection(); continue_section) { |
| continue_section->DisableFocusForShowingActiveFolder(disabled); |
| } |
| apps_grid_view_->DisableFocusForShowingActiveFolder(disabled); |
| |
| // Ignore the page switcher in accessibility tree so that buttons inside it |
| // will not be accessed by ChromeVox. |
| SetViewIgnoredForAccessibility(page_switcher_, disabled); |
| } |
| |
| int AppsContainerView::GetExpectedSuggestionChipY(float progress) { |
| const gfx::Rect search_box_bounds = |
| contents_view_->GetSearchBoxExpectedBoundsForProgress( |
| AppListState::kStateApps, progress); |
| |
| if (!suggestion_chip_container_view_) |
| return search_box_bounds.bottom(); |
| |
| return search_box_bounds.bottom() + kSuggestionChipContainerTopMargin; |
| } |
| |
| AppsContainerView::GridLayout AppsContainerView::CalculateGridLayout() const { |
| DCHECK(GetWidget()); |
| |
| // Adapt columns and rows based on the display/root window size. |
| const gfx::Size size = |
| display::Screen::GetScreen() |
| ->GetDisplayNearestView(GetWidget()->GetNativeView()) |
| .work_area() |
| .size(); |
| const bool is_portrait_mode = size.height() > size.width(); |
| const int available_height = |
| CalculateAvailableBoundsForAppsGrid(GetContentsBounds()).height(); |
| |
| int preferred_columns = 0; |
| int preferred_rows = 0; |
| |
| if (is_portrait_mode) { |
| preferred_rows = features::IsProductivityLauncherEnabled() |
| ? kPreferredGridRowsInPortraitProductivityLauncher |
| : kPreferredGridColumns; |
| preferred_columns = |
| features::IsProductivityLauncherEnabled() |
| ? kPreferredGridColumnsInPortraitProductivityLauncher |
| : kPreferredGridRows; |
| } else { |
| preferred_rows = kPreferredGridRows; |
| preferred_columns = kPreferredGridColumns; |
| } |
| |
| GridLayout result; |
| result.columns = preferred_columns; |
| result.rows = |
| apps_grid_view_->CalculateMaxRows(available_height, preferred_rows); |
| result.first_page_rows = apps_grid_view_->CalculateFirstPageMaxRows( |
| available_height, preferred_rows); |
| return result; |
| } |
| |
| void AppsContainerView::UpdateForActiveAppListModel() { |
| AppListModel* const model = AppListModelProvider::Get()->model(); |
| apps_grid_view_->SetModel(model); |
| apps_grid_view_->SetItemList(model->top_level_item_list()); |
| UpdateRecentApps(); |
| UpdateSuggestionChips(); |
| |
| // If model changes, close the folder view if it's open, as the associated |
| // item list is about to go away. |
| SetShowState(SHOW_APPS, false); |
| } |
| |
| void AppsContainerView::OnSuggestionChipsBlurDisablerReleased() { |
| DCHECK_GT(suggestion_chips_blur_disabler_count_, 0u); |
| --suggestion_chips_blur_disabler_count_; |
| |
| if (suggestion_chips_blur_disabler_count_ == 0) |
| suggestion_chip_container_view_->SetBlurDisabled(false); |
| } |
| |
| void AppsContainerView::UpdateGradientMaskBounds() { |
| const gfx::Rect container_bounds = scrollable_container_->bounds(); |
| const gfx::Rect top_gradient_bounds(0, 0, container_bounds.width(), |
| kDefaultFadeoutMaskHeight); |
| const gfx::Rect bottom_gradient_bounds( |
| 0, container_bounds.height() - kDefaultFadeoutMaskHeight, |
| container_bounds.width(), kDefaultFadeoutMaskHeight); |
| |
| gradient_layer_delegate_->set_start_fade_zone({top_gradient_bounds, |
| /*fade_in=*/true, |
| /*is_horizontal=*/false}); |
| gradient_layer_delegate_->set_end_fade_zone({bottom_gradient_bounds, |
| /*fade_in=*/false, |
| /*is_horizonal=*/false}); |
| gradient_layer_delegate_->layer()->SetBounds(container_bounds); |
| } |
| |
| } // namespace ash |