| // Copyright 2021 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/app_list_bubble_presenter.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/app_list/app_list_bubble_event_filter.h" |
| #include "ash/app_list/app_list_controller_impl.h" |
| #include "ash/app_list/app_list_event_targeter.h" |
| #include "ash/app_list/views/app_list_bubble_apps_page.h" |
| #include "ash/app_list/views/app_list_bubble_view.h" |
| #include "ash/app_list/views/app_list_drag_and_drop_host.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/app_list/app_list_client.h" |
| #include "ash/public/cpp/app_list/app_list_types.h" |
| #include "ash/public/cpp/assistant/controller/assistant_ui_controller.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/shelf/home_button.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shelf/shelf_navigation_widget.h" |
| #include "ash/shell.h" |
| #include "ash/system/tray/tray_background_view.h" |
| #include "ash/wm/container_finder.h" |
| #include "base/bind.h" |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/i18n/rtl.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/time/time.h" |
| #include "chromeos/services/assistant/public/cpp/assistant_enums.h" |
| #include "ui/aura/client/focus_client.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| namespace { |
| |
| using chromeos::assistant::AssistantExitPoint; |
| |
| // Maximum amount of time to spend refreshing zero state search results before |
| // opening the launcher. |
| constexpr base::TimeDelta kZeroStateSearchTimeout = base::Milliseconds(16); |
| |
| // Space between the edge of the bubble and the edge of the work area. |
| constexpr int kWorkAreaPadding = 8; |
| |
| // Space between the AppListBubbleView and the top of the screen should be at |
| // least this value plus the shelf height. |
| constexpr int kExtraTopOfScreenSpacing = 16; |
| |
| gfx::Rect GetWorkAreaForBubble(aura::Window* root_window) { |
| display::Display display = |
| display::Screen::GetScreen()->GetDisplayNearestWindow(root_window); |
| gfx::Rect work_area = display.work_area(); |
| |
| // Subtract the shelf's bounds from the work area, since the shelf should |
| // always be shown with the app list bubble. This is done because the work |
| // area includes the area under the shelf when the shelf is set to auto-hide. |
| work_area.Subtract(Shelf::ForWindow(root_window)->GetIdealBounds()); |
| |
| return work_area; |
| } |
| |
| // Returns the preferred size of the bubble widget in DIPs. |
| gfx::Size ComputeBubbleSize(aura::Window* root_window, |
| AppListBubbleView* bubble_view) { |
| const int default_height = 688; |
| // As of August 2021 the assistant cards require a minimum width of 640. If |
| // the cards become narrower then this could be reduced. |
| const int default_width = 640; |
| const int shelf_size = ShelfConfig::Get()->shelf_size(); |
| const gfx::Rect work_area = GetWorkAreaForBubble(root_window); |
| int height = default_height; |
| |
| // If the work area height is too small to fit the default size bubble, then |
| // calculate a smaller height to fit in the work area. Otherwise, if the work |
| // area height is tall enough to fit at least two default sized bubbles, then |
| // calculate a taller bubble with height taking no more than half the work |
| // area. |
| if (work_area.height() < |
| default_height + shelf_size + kExtraTopOfScreenSpacing) { |
| height = work_area.height() - shelf_size - kExtraTopOfScreenSpacing; |
| } else if (work_area.height() > |
| default_height * 2 + shelf_size + kExtraTopOfScreenSpacing) { |
| // Calculate the height required to fit the contents of the AppListBubble |
| // with no scrolling. |
| int height_to_fit_all_apps = bubble_view->GetHeightToFitAllApps(); |
| int max_height = |
| (work_area.height() - shelf_size - kExtraTopOfScreenSpacing) / 2; |
| DCHECK_GE(max_height, default_height); |
| height = base::clamp(height_to_fit_all_apps, default_height, max_height); |
| } |
| |
| return gfx::Size(default_width, height); |
| } |
| |
| // Returns the bounds in root window coordinates for the bubble widget. |
| gfx::Rect ComputeBubbleBounds(aura::Window* root_window, |
| AppListBubbleView* bubble_view) { |
| const gfx::Rect work_area = GetWorkAreaForBubble(root_window); |
| const gfx::Size bubble_size = ComputeBubbleSize(root_window, bubble_view); |
| const int padding = kWorkAreaPadding; // Shorten name for readability. |
| int x = 0; |
| int y = 0; |
| switch (Shelf::ForWindow(root_window)->alignment()) { |
| case ShelfAlignment::kBottom: |
| case ShelfAlignment::kBottomLocked: |
| if (base::i18n::IsRTL()) |
| x = work_area.right() - padding - bubble_size.width(); |
| else |
| x = work_area.x() + padding; |
| y = work_area.bottom() - padding - bubble_size.height(); |
| break; |
| case ShelfAlignment::kLeft: |
| x = work_area.x() + padding; |
| y = work_area.y() + padding; |
| break; |
| case ShelfAlignment::kRight: |
| x = work_area.right() - padding - bubble_size.width(); |
| y = work_area.y() + padding; |
| break; |
| } |
| return gfx::Rect(x, y, bubble_size.width(), bubble_size.height()); |
| } |
| |
| // Creates a bubble widget for the display with `root_window`. The widget is |
| // owned by its native widget. |
| views::Widget* CreateBubbleWidget(aura::Window* root_window) { |
| views::Widget* widget = new views::Widget(); |
| views::Widget::InitParams params( |
| views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.name = "AppListBubble"; |
| params.parent = |
| Shell::GetContainer(root_window, kShellWindowId_AppListContainer); |
| // AppListBubbleView handles round corners and blur via layers. |
| params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; |
| params.layer_type = ui::LAYER_NOT_DRAWN; |
| widget->Init(std::move(params)); |
| return widget; |
| } |
| |
| } // namespace |
| |
| AppListBubblePresenter::AppListBubblePresenter( |
| AppListControllerImpl* controller) |
| : controller_(controller) { |
| DCHECK(controller_); |
| } |
| |
| AppListBubblePresenter::~AppListBubblePresenter() { |
| CHECK(!views::WidgetObserver::IsInObserverList()); |
| } |
| |
| void AppListBubblePresenter::Shutdown() { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| // Aborting in-progress animations will run their cleanup callbacks, which |
| // might close the widget. |
| if (bubble_view_) |
| bubble_view_->AbortAllAnimations(); |
| if (bubble_widget_) |
| bubble_widget_->CloseNow(); // Calls OnWidgetDestroying(). |
| DCHECK(!bubble_widget_); |
| DCHECK(!bubble_view_); |
| } |
| |
| void AppListBubblePresenter::Show(int64_t display_id) { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| DCHECK(features::IsProductivityLauncherEnabled()); |
| if (is_target_visibility_show_) |
| return; |
| |
| if (bubble_view_) |
| bubble_view_->AbortAllAnimations(); |
| |
| is_target_visibility_show_ = true; |
| target_page_ = AppListBubblePage::kApps; |
| |
| // Refresh the continue tasks before opening the launcher. If a file doesn't |
| // exist on disk anymore then the launcher should not create or animate the |
| // continue task view for that suggestion. |
| controller_->GetClient()->StartZeroStateSearch( |
| base::BindOnce(&AppListBubblePresenter::OnZeroStateSearchDone, |
| weak_factory_.GetWeakPtr(), display_id), |
| kZeroStateSearchTimeout); |
| } |
| |
| void AppListBubblePresenter::OnZeroStateSearchDone(int64_t display_id) { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| aura::Window* root_window = Shell::GetRootWindowForDisplayId(display_id); |
| // Display might have disconnected during zero state refresh. |
| if (!root_window) |
| return; |
| |
| Shelf* shelf = Shelf::ForWindow(root_window); |
| ApplicationDragAndDropHost* drag_and_drop_host = |
| shelf->shelf_widget()->GetDragAndDropHostForAppList(); |
| HomeButton* home_button = shelf->navigation_widget()->GetHomeButton(); |
| |
| if (!bubble_widget_) { |
| // If the bubble widget is null, this is the first show. Construct views. |
| base::TimeTicks time_shown = base::TimeTicks::Now(); |
| |
| bubble_widget_ = CreateBubbleWidget(root_window); |
| bubble_widget_->GetNativeWindow()->SetEventTargeter( |
| std::make_unique<AppListEventTargeter>(controller_)); |
| bubble_view_ = bubble_widget_->SetContentsView( |
| std::make_unique<AppListBubbleView>(controller_, drag_and_drop_host)); |
| // Arrow left/right and up/down triggers the same focus movement as |
| // tab/shift+tab. |
| bubble_widget_->widget_delegate()->SetEnableArrowKeyTraversal(true); |
| |
| bubble_widget_->AddObserver(this); |
| // Set up event filter to close the bubble for clicks outside the bubble |
| // that don't cause window activation changes (e.g. clicks on wallpaper or |
| // blank areas of shelf). |
| bubble_event_filter_ = std::make_unique<AppListBubbleEventFilter>( |
| bubble_widget_, home_button, |
| base::BindRepeating(&AppListBubblePresenter::OnPressOutsideBubble, |
| base::Unretained(this))); |
| |
| UmaHistogramTimes("Apps.AppListBubbleCreationTime", |
| base::TimeTicks::Now() - time_shown); |
| } else { |
| DCHECK(bubble_view_); |
| // The bubble widget is cached, but it may change displays. Update pointers |
| // that are tied to the display. |
| bubble_view_->SetDragAndDropHostOfCurrentAppList(drag_and_drop_host); |
| bubble_event_filter_->SetButton(home_button); |
| // The observer for the correct display will be added below. |
| aura::client::GetFocusClient(bubble_widget_->GetNativeWindow()) |
| ->RemoveObserver(this); |
| } |
| // The widget bounds sometimes depend on the height of the apps grid, so set |
| // the bounds after creating and setting the contents. This may cause the |
| // bubble to change displays. |
| bubble_widget_->SetBounds(ComputeBubbleBounds(root_window, bubble_view_)); |
| |
| // Bubble launcher is always keyboard traversable. Update every show in case |
| // we are coming out of tablet mode. |
| controller_->SetKeyboardTraversalMode(true); |
| |
| // The focus client is tied to the root window, so update the observer every |
| // time the bubble is shown to make sure it tracks the right display. |
| aura::client::GetFocusClient(bubble_widget_->GetNativeWindow()) |
| ->AddObserver(this); |
| controller_->OnVisibilityWillChange(/*visible=*/true, display_id); |
| bubble_widget_->Show(); |
| // The page must be set before triggering the show animation so the correct |
| // animations are triggered. |
| bubble_view_->ShowPage(target_page_); |
| if (features::IsProductivityLauncherAnimationEnabled()) { |
| bubble_view_->StartShowAnimation(); |
| } |
| controller_->OnVisibilityChanged(/*visible=*/true, display_id); |
| } |
| |
| ShelfAction AppListBubblePresenter::Toggle(int64_t display_id) { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| DCHECK(features::IsProductivityLauncherEnabled()); |
| if (is_target_visibility_show_) { |
| Dismiss(); |
| return SHELF_ACTION_APP_LIST_DISMISSED; |
| } |
| Show(display_id); |
| return SHELF_ACTION_APP_LIST_SHOWN; |
| } |
| |
| void AppListBubblePresenter::Dismiss() { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| DCHECK(features::IsProductivityLauncherEnabled()); |
| if (!is_target_visibility_show_) |
| return; |
| |
| // Check for view because the code could be waiting for zero-state search |
| // results before first show. |
| if (bubble_view_) |
| bubble_view_->AbortAllAnimations(); |
| |
| // Must call before setting `is_target_visibility_show_` to false. |
| const int64_t display_id = GetDisplayId(); |
| |
| is_target_visibility_show_ = false; |
| |
| // Reset keyboard traversal in case the user switches to tablet launcher. |
| // Must happen before widget is destroyed. |
| controller_->SetKeyboardTraversalMode(false); |
| |
| controller_->ViewClosing(); |
| controller_->OnVisibilityWillChange(/*visible=*/false, display_id); |
| if (features::IsProductivityLauncherAnimationEnabled()) { |
| if (bubble_view_) { |
| bubble_view_->StartHideAnimation( |
| base::BindOnce(&AppListBubblePresenter::OnHideAnimationEnded, |
| weak_factory_.GetWeakPtr())); |
| } |
| } else { |
| // Check for widget because the code could be waiting for zero-state search |
| // results before first show. |
| if (bubble_widget_) |
| OnHideAnimationEnded(); |
| } |
| controller_->OnVisibilityChanged(/*visible=*/false, display_id); |
| |
| // Clean up assistant if it is showing. |
| controller_->ScheduleCloseAssistant(); |
| } |
| |
| aura::Window* AppListBubblePresenter::GetWindow() const { |
| return is_target_visibility_show_ && bubble_widget_ |
| ? bubble_widget_->GetNativeWindow() |
| : nullptr; |
| } |
| |
| bool AppListBubblePresenter::IsShowing() const { |
| return is_target_visibility_show_; |
| } |
| |
| bool AppListBubblePresenter::IsShowingEmbeddedAssistantUI() const { |
| if (!is_target_visibility_show_) |
| return false; |
| DCHECK(bubble_widget_); |
| return bubble_view_->IsShowingEmbeddedAssistantUI(); |
| } |
| |
| void AppListBubblePresenter::OnTemporarySortOrderChanged( |
| const absl::optional<AppListSortOrder>& new_order) { |
| if (!bubble_view_) |
| return; |
| bubble_view_->apps_page()->OnTemporarySortOrderChanged(new_order); |
| } |
| |
| void AppListBubblePresenter::ShowEmbeddedAssistantUI() { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| target_page_ = AppListBubblePage::kAssistant; |
| // `bubble_view_` does not exist while waiting for zero-state results. |
| // OnZeroStateSearchDone() sets the page in that case. |
| if (bubble_view_) { |
| DCHECK(bubble_widget_); |
| bubble_view_->ShowEmbeddedAssistantUI(); |
| } |
| } |
| |
| void AppListBubblePresenter::OnWidgetDestroying(views::Widget* widget) { |
| DVLOG(1) << __PRETTY_FUNCTION__; |
| // NOTE: While the widget is usually cached after Show(), this method can be |
| // called on monitor disconnect. Clean up state. |
| // `bubble_event_filter_` holds a pointer to the widget. |
| bubble_event_filter_.reset(); |
| aura::client::GetFocusClient(bubble_widget_->GetNativeView()) |
| ->RemoveObserver(this); |
| bubble_widget_->RemoveObserver(this); |
| bubble_widget_ = nullptr; |
| bubble_view_ = nullptr; |
| } |
| |
| void AppListBubblePresenter::OnWindowFocused(aura::Window* gained_focus, |
| aura::Window* lost_focus) { |
| if (!is_target_visibility_show_) |
| return; |
| |
| aura::Window* app_list_container = |
| bubble_widget_->GetNativeWindow()->parent(); |
| |
| // If the bubble or one of its children (e.g. an uninstall dialog) gained |
| // focus, the bubble should stay open. |
| if (gained_focus && app_list_container->Contains(gained_focus)) |
| return; |
| |
| // Otherwise, if the bubble or one of its children lost focus, the bubble |
| // should close. |
| if (lost_focus && app_list_container->Contains(lost_focus)) |
| Dismiss(); |
| } |
| |
| void AppListBubblePresenter::OnDisplayMetricsChanged( |
| const display::Display& display, |
| uint32_t changed_metrics) { |
| if (!IsShowing()) |
| return; |
| // Ignore changes to displays that aren't showing the launcher. |
| if (display.id() != GetDisplayId()) |
| return; |
| aura::Window* root_window = |
| bubble_widget_->GetNativeWindow()->GetRootWindow(); |
| bubble_widget_->SetBounds(ComputeBubbleBounds(root_window, bubble_view_)); |
| } |
| |
| void AppListBubblePresenter::OnPressOutsideBubble() { |
| // Presses outside the bubble could be activating a shelf item. Record the |
| // app list state prior to dismissal. |
| controller_->RecordAppListState(); |
| Dismiss(); |
| } |
| |
| int64_t AppListBubblePresenter::GetDisplayId() const { |
| if (!is_target_visibility_show_ || !bubble_widget_) |
| return display::kInvalidDisplayId; |
| return display::Screen::GetScreen() |
| ->GetDisplayNearestView(bubble_widget_->GetNativeView()) |
| .id(); |
| } |
| |
| void AppListBubblePresenter::OnHideAnimationEnded() { |
| // Hiding the launcher causes a window activation change. If the launcher is |
| // hiding because the user opened a system tray bubble, don't immediately |
| // close the bubble in response. |
| auto lock = TrayBackgroundView::DisableCloseBubbleOnWindowActivated(); |
| bubble_widget_->Hide(); |
| |
| controller_->MaybeCloseAssistant(); |
| } |
| |
| } // namespace ash |