#include "ash/app_list/views/app_list_view.h"
#include <algorithm>
#include <cmath>
#include <memory>
#include <utility>
#include "ash/app_list/app_list_event_targeter.h"
#include "ash/app_list/app_list_metrics.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_main_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/button_focus_skipper.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/paged_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/ime_util_chromeos.h"
namespace ash {
namespace {
constexpr int kAppListPageResetTimeLimitMinutes = 20;
bool skip_page_reset_timer_for_testing = false;
class SearchBoxFocusHost : public views::View {
METADATA_HEADER(SearchBoxFocusHost, views::View)
public:
explicit SearchBoxFocusHost(views::Widget* search_box_widget)
: search_box_widget_(search_box_widget) {}
SearchBoxFocusHost(const SearchBoxFocusHost&) = delete;
SearchBoxFocusHost& operator=(const SearchBoxFocusHost&) = delete;
~SearchBoxFocusHost() override = default;
views::FocusTraversable* GetFocusTraversable() override {
if (search_box_widget_->IsVisible())
return search_box_widget_;
return nullptr;
}
private:
raw_ptr<views::Widget> search_box_widget_;
};
BEGIN_METADATA(SearchBoxFocusHost)
END_METADATA
float ComputeSubpixelOffset(const display::Display& display, float value) {
float pixel_position = std::round(display.device_scale_factor() * value);
float dp_position = pixel_position / display.device_scale_factor();
return dp_position - std::floor(value);
}
}
AppListView::ScopedContentsResetDisabler::ScopedContentsResetDisabler(
AppListView* view)
: view_(view) {
DCHECK(!view_->disable_contents_reset_when_showing_);
view_->disable_contents_reset_when_showing_ = true;
}
AppListView::ScopedContentsResetDisabler::~ScopedContentsResetDisabler() {
DCHECK(view_->disable_contents_reset_when_showing_);
view_->disable_contents_reset_when_showing_ = false;
}
class AppListView::StateAnimationMetricsReporter {
public:
StateAnimationMetricsReporter() = default;
StateAnimationMetricsReporter(const StateAnimationMetricsReporter&) = delete;
StateAnimationMetricsReporter& operator=(
const StateAnimationMetricsReporter&) = delete;
~StateAnimationMetricsReporter() = default;
void SetTabletModeAnimationTransition(
TabletModeAnimationTransition transition) {
tablet_transition_ = transition;
}
void Reset();
metrics_util::SmoothnessCallback GetReportCallback() {
return base::BindRepeating(&StateAnimationMetricsReporter::RecordMetrics,
std::move(tablet_transition_));
}
private:
static void RecordMetrics(
std::optional<TabletModeAnimationTransition> transition,
int value);
std::optional<TabletModeAnimationTransition> tablet_transition_;
};
void AppListView::StateAnimationMetricsReporter::Reset() {
tablet_transition_.reset();
}
void AppListView::StateAnimationMetricsReporter::RecordMetrics(
std::optional<TabletModeAnimationTransition> tablet_transition,
int value) {
UMA_HISTOGRAM_PERCENTAGE("Apps.StateTransition.AnimationSmoothness", value);
if (!tablet_transition)
return;
switch (*tablet_transition) {
case TabletModeAnimationTransition::kHomeButtonShow:
UMA_HISTOGRAM_PERCENTAGE(
"Apps.HomeLauncherTransition.AnimationSmoothness."
"PressAppListButtonShow",
value);
break;
case TabletModeAnimationTransition::kHideHomeLauncherForWindow:
UMA_HISTOGRAM_PERCENTAGE(
"Apps.HomeLauncherTransition.AnimationSmoothness."
"HideLauncherForWindow",
value);
break;
case TabletModeAnimationTransition::kEnterFullscreenAllApps:
UMA_HISTOGRAM_PERCENTAGE(
"Apps.HomeLauncherTransition.AnimationSmoothness."
"EnterFullscreenAllApps",
value);
break;
case TabletModeAnimationTransition::kEnterFullscreenSearch:
UMA_HISTOGRAM_PERCENTAGE(
"Apps.HomeLauncherTransition.AnimationSmoothness."
"EnterFullscreenSearch",
value);
break;
case TabletModeAnimationTransition::kFadeInOverview:
UMA_HISTOGRAM_PERCENTAGE(
"Apps.HomeLauncherTransition.AnimationSmoothness.FadeInOverview",
value);
break;
case TabletModeAnimationTransition::kFadeOutOverview:
UMA_HISTOGRAM_PERCENTAGE(
"Apps.HomeLauncherTransition.AnimationSmoothness.FadeOutOverview",
value);
break;
}
}
AppListView::TestApi::TestApi(AppListView* view) : view_(view) {
DCHECK(view_);
}
AppListView::TestApi::~TestApi() = default;
PagedAppsGridView* AppListView::TestApi::GetRootAppsGridView() {
return view_->GetRootAppsGridView();
}
AppListView::AppListView(AppListViewDelegate* delegate)
: delegate_(delegate),
state_animation_metrics_reporter_(
std::make_unique<StateAnimationMetricsReporter>()) {
CHECK(delegate);
SetAccessibleWindowRole(ax::mojom::Role::kGroup);
}
AppListView::~AppListView() {
a11y_announcer_->Shutdown();
RemoveAllChildViews();
}
void AppListView::SetSkipPageResetTimerForTesting(bool enabled) {
skip_page_reset_timer_for_testing = enabled;
}
void AppListView::InitView(gfx::NativeView parent) {
base::AutoReset<bool> auto_reset(&is_building_, true);
time_shown_ = base::Time::Now();
InitContents();
InitWidget(parent);
}
void AppListView::InitContents() {
DCHECK(!app_list_main_view_);
DCHECK(!search_box_view_);
a11y_announcer_ = std::make_unique<AppListA11yAnnouncer>(
AddChildView(std::make_unique<views::View>()));
auto app_list_main_view = std::make_unique<AppListMainView>(delegate_, this);
auto search_box_view =
std::make_unique<SearchBoxView>(app_list_main_view.get(), delegate_,
false);
search_box_view->InitializeForFullscreenLauncher();
button_focus_skipper_ = std::make_unique<ButtonFocusSkipper>(this);
button_focus_skipper_->AddButton(search_box_view->sunfish_button());
button_focus_skipper_->AddButton(search_box_view->gemini_button());
app_list_main_view_ = AddChildView(std::move(app_list_main_view));
search_box_view_ = AddChildView(std::move(search_box_view));
app_list_main_view_->Init(0, search_box_view_);
}
void AppListView::InitWidget(gfx::NativeView parent) {
DCHECK(!GetWidget());
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.name = "AppList";
params.parent = parent;
params.delegate = this;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.layer_type = ui::LAYER_NOT_DRAWN;
views::Widget* widget = new views::Widget;
widget->Init(std::move(params));
DCHECK_EQ(widget, GetWidget());
widget->GetNativeWindow()->SetEventTargeter(
std::make_unique<AppListEventTargeter>(delegate_));
SetEnableArrowKeyTraversal(true);
widget->GetNativeView()->AddObserver(this);
}
void AppListView::Show(AppListViewState preferred_state) {
if (!time_shown_.has_value())
time_shown_ = base::Time::Now();
AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
AddAccelerator(ui::Accelerator(ui::VKEY_BROWSER_BACK, ui::EF_NONE));
UpdateWidget();
if (!disable_contents_reset_when_showing_)
app_list_main_view_->contents_view()->ResetForShow();
SetState(preferred_state);
DCHECK(is_fullscreen());
CloseKeyboardIfVisible();
app_list_main_view_->ShowAppListWhenReady();
UMA_HISTOGRAM_TIMES("Apps.AppListCreationTime",
base::Time::Now() - time_shown_.value());
time_shown_ = std::nullopt;
}
void AppListView::CloseOpenedPage() {
if (HandleCloseOpenFolder())
return;
HandleCloseOpenSearchBox();
}
bool AppListView::HandleCloseOpenFolder() {
if (GetAppsContainerView()->IsInFolderView()) {
GetAppsContainerView()->app_list_folder_view()->CloseFolderPage();
return true;
}
return false;
}
bool AppListView::HandleCloseOpenSearchBox() {
if (app_list_main_view_ &&
app_list_main_view_->contents_view()->IsShowingSearchResults()) {
return Back();
}
return false;
}
bool AppListView::Back() {
if (app_list_main_view_)
return app_list_main_view_->contents_view()->Back();
return false;
}
void AppListView::OnPaint(gfx::Canvas* canvas) {
views::WidgetDelegateView::OnPaint(canvas);
}
bool AppListView::AcceleratorPressed(const ui::Accelerator& accelerator) {
switch (accelerator.key_code()) {
case ui::VKEY_ESCAPE:
case ui::VKEY_BROWSER_BACK:
Back();
break;
default:
NOTREACHED();
}
return true;
}
void AppListView::Layout(PassKey) {
if (is_building_)
return;
if (GetWidget()->GetLayer()->GetAnimator() &&
GetWidget()->GetLayer()->GetAnimator()->is_animating()) {
return;
}
const gfx::Rect contents_bounds = GetContentsBounds();
gfx::Rect main_bounds = contents_bounds;
main_bounds.Inset(GetMainViewInsetsForShelf());
app_list_main_view_->SetBoundsRect(main_bounds);
LayoutSuperclass<views::WidgetDelegateView>(this);
}
bool AppListView::IsFolderBeingRenamed() {
return GetAppsContainerView()
->app_list_folder_view()
->folder_header_view()
->HasTextFocus();
}
void AppListView::UpdatePageResetTimer(bool app_list_visibility) {
if (app_list_visibility) {
page_reset_timer_.Stop();
return;
}
page_reset_timer_.Start(FROM_HERE,
base::Minutes(kAppListPageResetTimeLimitMinutes),
this, &AppListView::SelectInitialAppsPage);
if (skip_page_reset_timer_for_testing)
page_reset_timer_.FireNow();
}
gfx::Insets AppListView::GetMainViewInsetsForShelf() const {
return gfx::Insets::TLBR(0, 0, delegate_->GetSystemShelfInsetsInTabletMode(),
0);
}
void AppListView::UpdateWidget() {
GetWidget()->GetNativeView()->SetBounds(GetPreferredWidgetBounds());
ResetSubpixelPositionOffset(GetWidget()->GetNativeView()->layer());
}
void AppListView::HandleClickOrTap(ui::LocatedEvent* event) {
if (CloseKeyboardIfVisible()) {
search_box_view_->NotifyGestureEvent();
if (search_box_view_->HasSearch()) {
return;
}
}
GetFocusManager()->ClearFocus();
if (GetAppsContainerView()->IsInFolderView()) {
GetAppsContainerView()->app_list_folder_view()->CloseFolderPage();
return;
}
if ((event->IsGestureEvent() &&
(event->AsGestureEvent()->type() == ui::EventType::kGestureLongPress ||
event->AsGestureEvent()->type() == ui::EventType::kGestureLongTap ||
event->AsGestureEvent()->type() ==
ui::EventType::kGestureTwoFingerTap)) ||
(event->IsMouseEvent() &&
event->AsMouseEvent()->IsOnlyRightMouseButton())) {
gfx::Point onscreen_location(event->location());
ConvertPointToScreen(this, &onscreen_location);
delegate_->ShowWallpaperContextMenu(
onscreen_location, event->IsGestureEvent()
? ui::mojom::MenuSourceType::kTouch
: ui::mojom::MenuSourceType::kMouse);
return;
}
if (search_box_view_->is_search_box_active())
search_box_view_->ClearSearchAndDeactivateSearchBox();
}
void AppListView::SetChildViewsForStateTransition(
AppListViewState target_state) {
if (target_state == AppListViewState::kFullscreenSearch) {
return;
}
if (GetAppsContainerView()->IsInFolderView())
GetAppsContainerView()->ResetForShowApps();
if (target_state != AppListViewState::kClosed) {
app_list_main_view_->contents_view()->SetActiveState(
AppListState::kStateApps, true);
}
}
void AppListView::MaybeCreateAccessibilityEvent(AppListViewState new_state) {
if (new_state == app_list_state_ || !delegate_->AppListTargetVisibility())
return;
if (new_state == AppListViewState::kFullscreenAllApps)
a11y_announcer_->AnnounceAppListShown();
}
void AppListView::EnsureWidgetBoundsMatchCurrentState() {
const gfx::Rect new_target_bounds = GetPreferredWidgetBounds();
aura::Window* window = GetWidget()->GetNativeView();
if (new_target_bounds == window->GetTargetBounds())
return;
GetWidget()->GetNativeView()->SetBounds(new_target_bounds);
ResetSubpixelPositionOffset(GetWidget()->GetNativeView()->layer());
SetState(target_app_list_state_);
}
display::Display AppListView::GetDisplayNearestView() const {
return display::Screen::Get()->GetDisplayNearestView(
GetWidget()->GetNativeWindow()->parent());
}
AppsContainerView* AppListView::GetAppsContainerView() {
return app_list_main_view_->contents_view()->apps_container_view();
}
PagedAppsGridView* AppListView::GetRootAppsGridView() {
return GetAppsContainerView()->apps_grid_view();
}
views::View* AppListView::GetInitiallyFocusedView() {
return app_list_main_view_->search_box_view()->search_box();
}
void AppListView::OnScrollEvent(ui::ScrollEvent* event) {
if (!HandleScroll(event->location(),
gfx::Vector2d(event->x_offset(), event->y_offset()),
event->type())) {
return;
}
event->SetHandled();
event->StopPropagation();
}
void AppListView::OnMouseEvent(ui::MouseEvent* event) {
if (app_list_state_ == AppListViewState::kClosed)
return;
switch (event->type()) {
case ui::EventType::kMousePressed:
case ui::EventType::kMouseDragged:
event->SetHandled();
break;
case ui::EventType::kMouseReleased:
event->SetHandled();
HandleClickOrTap(event);
break;
case ui::EventType::kMousewheel:
if (HandleScroll(event->location(), event->AsMouseWheelEvent()->offset(),
ui::EventType::kMousewheel)) {
event->SetHandled();
}
break;
default:
break;
}
}
void AppListView::OnGestureEvent(ui::GestureEvent* event) {
if (app_list_state_ == AppListViewState::kClosed)
return;
switch (event->type()) {
case ui::EventType::kGestureTap:
case ui::EventType::kGestureLongPress:
case ui::EventType::kGestureLongTap:
case ui::EventType::kGestureTwoFingerTap:
event->SetHandled();
HandleClickOrTap(event);
break;
default:
break;
}
}
void AppListView::OnKeyEvent(ui::KeyEvent* event) {
RedirectKeyEventToSearchBox(event);
}
bool AppListView::HandleScroll(const gfx::Point& location,
const gfx::Vector2d& offset,
ui::EventType type) {
if (ShouldIgnoreScrollEvents())
return false;
if (GetAppsContainerView()->IsInFolderView())
return false;
PagedAppsGridView* apps_grid_view = GetRootAppsGridView();
gfx::Point root_apps_grid_location(location);
views::View::ConvertPointToTarget(this, apps_grid_view,
&root_apps_grid_location);
bool is_in_vertical_bounds =
root_apps_grid_location.y() > apps_grid_view->GetLocalBounds().y() &&
root_apps_grid_location.y() < apps_grid_view->GetLocalBounds().bottom();
if (is_in_vertical_bounds) {
apps_grid_view->HandleScrollFromParentView(offset, type);
}
return true;
}
void AppListView::SetState(AppListViewState new_state) {
target_app_list_state_ = new_state;
set_state_weak_factory_.InvalidateWeakPtrs();
base::WeakPtr<AppListView> set_state_request =
set_state_weak_factory_.GetWeakPtr();
SetChildViewsForStateTransition(new_state);
if (!set_state_request)
return;
MaybeCreateAccessibilityEvent(new_state);
app_list_main_view_->contents_view()->OnAppListViewTargetStateChanged(
new_state);
app_list_state_ = new_state;
if (delegate_)
delegate_->OnViewStateChanged(new_state);
if (GetWidget()->IsActive()) {
GetInitiallyFocusedView()->RequestFocus();
}
UpdateWindowTitle();
GetAppsContainerView()->UpdateControlVisibility(app_list_state_);
}
void AppListView::UpdateWindowTitle() {
if (!GetWidget())
return;
gfx::NativeView window = GetWidget()->GetNativeView();
AppListState contents_view_state = delegate_->GetCurrentAppListPage();
if (window) {
if (contents_view_state == AppListState::kStateSearchResults ||
contents_view_state == AppListState::kStateEmbeddedAssistant) {
window->SetTitle(l10n_util::GetStringUTF16(
IDS_APP_LIST_LAUNCHER_ACCESSIBILITY_ANNOUNCEMENT));
return;
}
switch (target_app_list_state_) {
case AppListViewState::kFullscreenAllApps:
window->SetTitle(l10n_util::GetStringUTF16(
IDS_APP_LIST_ALL_APPS_ACCESSIBILITY_ANNOUNCEMENT));
break;
case AppListViewState::kClosed:
case AppListViewState::kFullscreenSearch:
break;
}
}
}
void AppListView::OnAppListVisibilityWillChange(bool visible) {
GetAppsContainerView()->OnAppListVisibilityWillChange(visible);
}
void AppListView::OnAppListVisibilityChanged(bool shown) {
GetAppsContainerView()->OnAppListVisibilityChanged(shown);
}
void AppListView::SetStateFromSearchBoxView(bool search_box_is_empty,
bool triggered_by_contents_change) {
switch (target_app_list_state_) {
case AppListViewState::kFullscreenSearch:
if (search_box_is_empty && !triggered_by_contents_change)
SetState(AppListViewState::kFullscreenAllApps);
break;
case AppListViewState::kFullscreenAllApps:
if (!search_box_is_empty ||
(search_box_is_empty && triggered_by_contents_change)) {
SetState(AppListViewState::kFullscreenSearch);
}
break;
case AppListViewState::kClosed:
break;
}
}
void AppListView::OffsetYPositionOfAppList(int offset) {
gfx::NativeView native_view = GetWidget()->GetNativeView();
gfx::Transform transform;
transform.Translate(0, offset);
native_view->SetTransform(transform);
}
PaginationModel* AppListView::GetAppsPaginationModel() {
return GetRootAppsGridView()->pagination_model();
}
void AppListView::OnHomeLauncherGainingFocusWithoutAnimation() {
if (GetFocusManager()->GetFocusedView() != GetInitiallyFocusedView())
GetInitiallyFocusedView()->RequestFocus();
}
void AppListView::SelectInitialAppsPage() {
if (GetAppsPaginationModel()->total_pages() > 0 &&
GetAppsPaginationModel()->selected_page() != 0) {
GetAppsPaginationModel()->SelectPage(0, false );
}
}
int AppListView::GetFullscreenStateHeight() const {
const display::Display display = GetDisplayNearestView();
const gfx::Rect display_bounds = display.bounds();
return display_bounds.height() - display.work_area().y() + display_bounds.y();
}
metrics_util::SmoothnessCallback
AppListView::GetStateTransitionMetricsReportCallback() {
return state_animation_metrics_reporter_->GetReportCallback();
}
void AppListView::ResetTransitionMetricsReporter() {
state_animation_metrics_reporter_->Reset();
}
void AppListView::OnWindowDestroying(aura::Window* window) {
DCHECK_EQ(GetWidget()->GetNativeView(), window);
window->RemoveObserver(this);
}
void AppListView::RedirectKeyEventToSearchBox(ui::KeyEvent* event) {
if (event->handled())
return;
views::Textfield* search_box = search_box_view_->search_box();
const bool is_search_box_focused = search_box->HasFocus();
if (is_search_box_focused || IsFolderBeingRenamed())
return;
if (IsArrowKeyEvent(*event) && !search_box_view_->is_search_box_active())
return;
search_box->OnKeyEvent(event);
if (event->handled()) {
search_box->RequestFocus();
return;
}
if (event->type() == ui::EventType::kKeyPressed) {
search_box->InsertChar(*event);
}
}
void AppListView::OnScreenKeyboardShown(bool shown) {
if (onscreen_keyboard_shown_ == shown)
return;
onscreen_keyboard_shown_ = shown;
if (shown && GetAppsContainerView()->IsInFolderView()) {
const int folder_offset =
GetAppsContainerView()->app_list_folder_view()->GetYOffsetForFolder();
if (folder_offset != 0) {
OffsetYPositionOfAppList(folder_offset);
GetAppsContainerView()->app_list_folder_view()->UpdateShadowBounds();
offset_to_show_folder_with_onscreen_keyboard_ = true;
}
} else if (offset_to_show_folder_with_onscreen_keyboard_) {
OffsetYPositionOfAppList(0);
GetAppsContainerView()->app_list_folder_view()->UpdateShadowBounds();
offset_to_show_folder_with_onscreen_keyboard_ = false;
}
if (!shown) {
GetWidget()->GetNativeView()->ClearProperty(
wm::kVirtualKeyboardRestoreBoundsKey);
EnsureWidgetBoundsMatchCurrentState();
}
}
bool AppListView::CloseKeyboardIfVisible() {
if (!keyboard::KeyboardUIController::HasInstance())
return false;
auto* const keyboard_controller = keyboard::KeyboardUIController::Get();
if (keyboard_controller->IsKeyboardVisible()) {
keyboard_controller->HideKeyboardByUser();
return true;
}
return false;
}
void AppListView::OnParentWindowBoundsChanged() {
EnsureWidgetBoundsMatchCurrentState();
}
bool AppListView::ShouldIgnoreScrollEvents() {
if (app_list_state_ != AppListViewState::kFullscreenAllApps)
return true;
return GetWidget()->GetLayer()->GetAnimator()->is_animating() ||
GetRootAppsGridView()->pagination_model()->has_transition();
}
gfx::Rect AppListView::GetPreferredWidgetBounds() {
CHECK(GetWidget());
aura::Window* parent = GetWidget()->GetNativeView()->parent();
CHECK(parent);
const display::Display display = GetDisplayNearestView();
const gfx::Rect work_area_bounds = display.work_area();
const int preferred_widget_y = work_area_bounds.y() - display.bounds().y();
return delegate_->SnapBoundsToDisplayEdge(
gfx::Rect(0, preferred_widget_y, parent->bounds().width(),
GetFullscreenStateHeight()));
}
void AppListView::OnTabletModeAnimationTransitionNotified(
TabletModeAnimationTransition animation_transition) {
state_animation_metrics_reporter_->SetTabletModeAnimationTransition(
animation_transition);
}
void AppListView::ResetSubpixelPositionOffset(ui::Layer* layer) {
const display::Display display = GetDisplayNearestView();
const gfx::Rect& bounds = layer->bounds();
layer->SetSubpixelPositionOffset(
gfx::Vector2dF(ComputeSubpixelOffset(display, bounds.x()),
ComputeSubpixelOffset(display, bounds.y())));
}
BEGIN_METADATA(AppListView)
END_METADATA
}