chromium/ash/app_list/app_list_bubble_presenter.cc

// Copyright 2021 The Chromium Authors
// 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 <algorithm>
#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/apps_collections_controller.h"
#include "ash/app_list/views/app_list_bubble_apps_collections_page.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/public/cpp/app_list/app_list_client.h"
#include "ash/public/cpp/app_list/app_list_features.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/strings/grit/ash_strings.h"
#include "ash/system/tray/tray_background_view.h"
#include "ash/wm/container_finder.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/activation_client.h"

namespace ash {
namespace {

using 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::RectF 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.
  gfx::RectF shelf_bounds(Shelf::ForWindow(root_window)->GetIdealBounds());
  wm::TranslateRectToScreen(root_window, &shelf_bounds);
  work_area.Subtract(shelf_bounds);

  return gfx::ToRoundedRect(work_area);
}

int GetBubbleWidth(gfx::Rect work_area, aura::Window* root_window) {
  // As of August 2021 the assistant cards require a minimum width of 640. If
  // the cards become narrower then this could be reduced.
  return work_area.width() < 1200 ? 544 : 640;
}

// 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;
  const int shelf_size = ShelfConfig::Get()->shelf_size();
  const gfx::Rect work_area = GetWorkAreaForBubble(root_window);
  int height = default_height;

  const int width = GetBubbleWidth(work_area, root_window);
  // 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 = std::clamp(height_to_fit_all_apps, default_height, max_height);
  }

  return gfx::Size(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::NATIVE_WIDGET_OWNS_WIDGET,
      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__;
  if (is_target_visibility_show_)
    return;

  if (bubble_view_)
    bubble_view_->AbortAllAnimations();

  is_target_visibility_show_ = true;

  target_page_ = AppsCollectionsController::Get()->ShouldShowAppsCollection()
                     ? AppListBubblePage::kAppsCollections
                     : AppListBubblePage::kApps;

  controller_->OnVisibilityWillChange(/*visible=*/true, display_id);

  // 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__;
  // Dismiss() might have been called while waiting for zero-state results.
  if (!is_target_visibility_show_)
    return;

  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()->SetTitle(l10n_util::GetStringUTF16(
        IDS_APP_LIST_LAUNCHER_ACCESSIBILITY_ANNOUNCEMENT));
    bubble_widget_->GetNativeWindow()->SetEventTargeter(
        std::make_unique<AppListEventTargeter>(controller_));
    bubble_view_ = bubble_widget_->SetContentsView(
        std::make_unique<AppListBubbleView>(controller_, drag_and_drop_host));
    // Some of Assistant UIs have to be initialized explicitly. See details in
    // the comment of AppListBubbleView::InitializeUIForBubbleView.
    bubble_view_->InitializeUIForBubbleView();
    // 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);
    Shell::Get()->activation_client()->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);
    // Refresh suggestions now that zero-state search data is updated.
    bubble_view_->UpdateSuggestions();
    bubble_event_filter_->SetButton(home_button);
  }

  // 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);

  shelf_observer_.Reset();
  shelf_observer_.Observe(shelf);

  bubble_widget_->Show();
  // The page must be set before triggering the show animation so the correct
  // animations are triggered.
  bubble_view_->ShowPage(target_page_);
  const bool is_side_shelf = !shelf->IsHorizontalAlignment();
  bubble_view_->StartShowAnimation(is_side_shelf);
  controller_->OnVisibilityChanged(/*visible=*/true, display_id);
}

ShelfAction AppListBubblePresenter::Toggle(int64_t display_id) {
  DVLOG(1) << __PRETTY_FUNCTION__;
  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__;
  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 (bubble_view_) {
    aura::Window* bubble_window = bubble_view_->GetWidget()->GetNativeWindow();
    DCHECK(bubble_window);
    Shelf* shelf = Shelf::ForWindow(bubble_window);
    const bool is_side_shelf = !shelf->IsHorizontalAlignment();
    bubble_view_->StartHideAnimation(
        is_side_shelf,
        base::BindOnce(&AppListBubblePresenter::OnHideAnimationEnded,
                       weak_factory_.GetWeakPtr()));
  }
  controller_->OnVisibilityChanged(/*visible=*/false, display_id);

  // Clean up assistant if it is showing.
  controller_->ScheduleCloseAssistant();

  shelf_observer_.Reset();
  if (bubble_view_)
    bubble_view_->SetDragAndDropHostOfCurrentAppList(nullptr);
}

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;

  // Bubble view is null while the bubble widget is being initialized for show.
  // In this case, return true iff the app list will show the assistant page
  // when initialized.
  if (!bubble_view_)
    return target_page_ == AppListBubblePage::kAssistant;

  return bubble_view_->IsShowingEmbeddedAssistantUI();
}

void AppListBubblePresenter::UpdateContinueSectionVisibility() {
  if (bubble_view_)
    bubble_view_->UpdateContinueSectionVisibility();
}

void AppListBubblePresenter::UpdateForNewSortingOrder(
    const std::optional<AppListSortOrder>& new_order,
    bool animate,
    base::OnceClosure update_position_closure) {
  DCHECK_EQ(animate, !update_position_closure.is_null());

  if (!bubble_view_) {
    // A rare case. Still handle it for safety.
    if (update_position_closure)
      std::move(update_position_closure).Run();
    return;
  }

  bubble_view_->UpdateForNewSortingOrder(new_order, animate,
                                         std::move(update_position_closure));
}

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();
  Shell::Get()->activation_client()->RemoveObserver(this);
  bubble_widget_->RemoveObserver(this);
  bubble_widget_ = nullptr;
  bubble_view_ = nullptr;
}

void AppListBubblePresenter::OnWindowActivated(ActivationReason reason,
                                               aura::Window* gained_active,
                                               aura::Window* lost_active) {
  if (!is_target_visibility_show_)
    return;

  if (gained_active) {
    if (auto* container = GetContainerForWindow(gained_active)) {
      const int container_id = container->GetId();
      // The bubble can be shown without activation if:
      // 1. The bubble or one of its children (e.g. an uninstall dialog) gains
      //    activation; OR
      // 2. The shelf gains activation (e.g. by pressing Alt-Shift-L); OR
      // 3. A help bubble container's descendant gains activation.
      if (container_id == kShellWindowId_AppListContainer ||
          container_id == kShellWindowId_ShelfContainer ||
          container_id == kShellWindowId_HelpBubbleContainer) {
        return;
      }
    }
  }

  // Closing the bubble for "press" type events is handled by
  // `bubble_event_filter_`. Activation can change when a user merely moves the
  // cursor outside the app list bounds, so losing activation should not close
  // the bubble.
  if (reason == wm::ActivationChangeObserver::ActivationReason::INPUT_EVENT)
    return;

  aura::Window* app_list_container =
      bubble_widget_->GetNativeWindow()->parent();

  // Otherwise, if the bubble or one of its children lost activation or if
  // something other than the bubble gains activation, the bubble should close.
  if ((lost_active && app_list_container->Contains(lost_active)) ||
      (gained_active && !app_list_container->Contains(gained_active))) {
    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::OnShelfShuttingDown() {
  shelf_observer_.Reset();
  if (bubble_view_)
    bubble_view_->SetDragAndDropHostOfCurrentAppList(nullptr);
}

void AppListBubblePresenter::OnPressOutsideBubble(
    const ui::LocatedEvent& event) {
  // Presses outside the bubble could be activating a shelf item. Record the
  // app list state prior to dismissal.
  controller_->RecordAppListState();

  // The press outside the bubble might spawn a menu. If the bubble is active at
  // the end of the hide animation, an activation change event will cause the
  // menu to close. Deactivate now so menus stay open. https://crbug.com/1299088
  if (bubble_widget_->IsActive()) {
    bubble_widget_->Deactivate();
  }
  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();
}

int AppListBubblePresenter::GetPreferredBubbleWidth(
    aura::Window* root_window) const {
  return GetBubbleWidth(GetWorkAreaForBubble(root_window), root_window);
}

}  // namespace ash