blob: 1c7b78b9a071249fddeaf79fc69fcceccc1d1cf1 [file] [log] [blame]
// Copyright (c) 2012 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/app_list_view.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/model/app_list_model.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/contents_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/wallpaper_types.h"
#include "base/bind.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/string_util.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/platform/aura_window_properties.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_switches.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/scoped_layer_animation_settings.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/image/image_skia.h"
#include "ui/gfx/interpolated_transform.h"
#include "ui/gfx/skia_util.h"
#include "ui/keyboard/keyboard_controller.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/shadow_types.h"
using ash::ColorProfileType;
namespace app_list {
namespace {
// The height of the half app list from the bottom of the screen.
constexpr int kHalfAppListHeight = 561;
// The fraction of app list height that the app list must be released at in
// order to transition to the next state.
constexpr int kAppListThresholdDenominator = 3;
// The scroll offset in order to transition from PEEKING to FULLSCREEN
constexpr int kAppListMinScrollToSwitchStates = 20;
// The DIP distance from the bezel in which a gesture drag end results in a
// closed app list.
constexpr int kAppListBezelMargin = 50;
// The size of app info dialog in fullscreen app list.
constexpr int kAppInfoDialogWidth = 512;
constexpr int kAppInfoDialogHeight = 384;
// The animation duration for app list movement.
constexpr float kAppListAnimationDurationTestMs = 0;
constexpr float kAppListAnimationDurationMs = 200;
constexpr float kAppListAnimationDurationFromFullscreenMs = 250;
// Events within this threshold from the top of the view will be reserved for
// home launcher gestures, if they can be processed.
constexpr int kAppListHomeLaucherGesturesThreshold = 32;
// Quality of the shield background blur.
constexpr float kAppListBlurQuality = 0.33f;
// Set animation durations to 0 for testing.
static bool short_animations_for_testing;
// This view forwards the focus to the search box widget by providing it as a
// FocusTraversable when a focus search is provided.
class SearchBoxFocusHost : public views::View {
public:
explicit SearchBoxFocusHost(views::Widget* search_box_widget)
: search_box_widget_(search_box_widget) {}
~SearchBoxFocusHost() override {}
views::FocusTraversable* GetFocusTraversable() override {
return search_box_widget_;
}
private:
views::Widget* search_box_widget_;
DISALLOW_COPY_AND_ASSIGN(SearchBoxFocusHost);
};
SkColor GetBackgroundShieldColor(const std::vector<SkColor>& prominent_colors,
float color_opacity) {
const U8CPU sk_opacity_value = static_cast<U8CPU>(255 * color_opacity);
const SkColor default_color = SkColorSetA(
app_list::AppListView::kDefaultBackgroundColor, sk_opacity_value);
if (prominent_colors.empty())
return default_color;
DCHECK_EQ(static_cast<size_t>(ColorProfileType::NUM_OF_COLOR_PROFILES),
prominent_colors.size());
const SkColor dark_muted =
prominent_colors[static_cast<int>(ColorProfileType::DARK_MUTED)];
if (SK_ColorTRANSPARENT == dark_muted)
return default_color;
return SkColorSetA(
color_utils::GetResultingPaintColor(
SkColorSetA(SK_ColorBLACK, AppListView::kAppListColorDarkenAlpha),
dark_muted),
sk_opacity_value);
}
DEFINE_UI_CLASS_PROPERTY_KEY(bool, kExcludeWindowFromEventHandling, false);
// This targeter prevents routing events to sub-windows, such as
// RenderHostWindow in order to handle events in context of app list.
class AppListEventTargeter : public aura::WindowTargeter {
public:
AppListEventTargeter() = default;
~AppListEventTargeter() override = default;
// aura::WindowTargeter:
bool SubtreeShouldBeExploredForEvent(aura::Window* window,
const ui::LocatedEvent& event) override {
if (window->GetProperty(kExcludeWindowFromEventHandling)) {
// Allow routing to sub-windows for ET_MOUSE_MOVED event which is used by
// accessibility to enter the mode of exploration of WebView contents.
if (event.type() != ui::ET_MOUSE_MOVED)
return false;
}
return aura::WindowTargeter::SubtreeShouldBeExploredForEvent(window, event);
}
private:
DISALLOW_COPY_AND_ASSIGN(AppListEventTargeter);
};
class StateAnimationMetricsReporter : public ui::AnimationMetricsReporter {
public:
StateAnimationMetricsReporter() = default;
~StateAnimationMetricsReporter() override = default;
void Report(int value) override {
UMA_HISTOGRAM_PERCENTAGE("Apps.StateTransition.AnimationSmoothness", value);
}
private:
DISALLOW_COPY_AND_ASSIGN(StateAnimationMetricsReporter);
};
} // namespace
// An animation observer to hide the view at the end of the animation.
class HideViewAnimationObserver : public ui::ImplicitAnimationObserver {
public:
HideViewAnimationObserver() : target_(NULL) {}
~HideViewAnimationObserver() override {
if (target_)
StopObservingImplicitAnimations();
}
void SetTarget(views::View* target) {
if (target_)
StopObservingImplicitAnimations();
target_ = target;
}
private:
// Overridden from ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
if (target_) {
target_->SetVisible(false);
target_ = NULL;
}
}
views::View* target_;
DISALLOW_COPY_AND_ASSIGN(HideViewAnimationObserver);
};
// An animation observer to transition between states.
class TransitionAnimationObserver : public ui::ImplicitAnimationObserver {
public:
explicit TransitionAnimationObserver(AppListView* view) : view_(view) {}
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
DCHECK(view_);
view_->Layout();
}
private:
AppListView* const view_;
DISALLOW_COPY_AND_ASSIGN(TransitionAnimationObserver);
};
// The view for the app list background shield which changes color and radius.
class AppListBackgroundShieldView : public views::View {
public:
explicit AppListBackgroundShieldView(ui::LayerType layer_type)
: color_(AppListView::kDefaultBackgroundColor), corner_radius_(0) {
SetPaintToLayer(layer_type);
layer()->SetFillsBoundsOpaquely(false);
if (layer()->type() == ui::LAYER_SOLID_COLOR)
layer()->SetColor(color_);
}
~AppListBackgroundShieldView() override = default;
void UpdateColor(SkColor color) {
color_ = color;
if (layer()->type() == ui::LAYER_SOLID_COLOR)
layer()->SetColor(color);
else
SchedulePaint();
}
void UpdateCornerRadius(int corner_radius) {
corner_radius_ = corner_radius;
if (!layer())
SchedulePaint();
}
// Overridden from views::View:
void OnPaint(gfx::Canvas* canvas) override {
cc::PaintFlags flags;
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(color_);
canvas->DrawRoundRect(GetContentsBounds(), corner_radius_, flags);
}
SkColor GetColorForTest() const { return color_; }
private:
SkColor color_;
int corner_radius_;
DISALLOW_COPY_AND_ASSIGN(AppListBackgroundShieldView);
};
// Animation used to translate AppListView as well as its child views.
class AppListAnimation : public ui::LayerAnimationElement {
public:
AppListAnimation(gfx::PointF start_pos,
gfx::PointF end_pos,
base::TimeDelta duration,
gfx::Tween::Type tween_type,
AppListView* view)
: ui::LayerAnimationElement(ui::LayerAnimationElement::TRANSFORM,
duration),
transform_(
std::make_unique<ui::InterpolatedTranslation>(start_pos, end_pos)),
view_(view),
tween_type_(tween_type) {}
~AppListAnimation() override = default;
// ui::LayerAnimationElement:
void OnStart(ui::LayerAnimationDelegate* delegate) override {}
bool OnProgress(double current,
ui::LayerAnimationDelegate* delegate) override {
const double progress = gfx::Tween::CalculateValue(tween_type_, current);
delegate->SetTransformFromAnimation(
transform_->Interpolate(progress),
ui::PropertyChangeReason::FROM_ANIMATION);
// Update child views' layout at each animation frame because child views'
// padding changes along with the app list view's bounds.
view_->app_list_main_view()->contents_view()->UpdateYPositionAndOpacity();
return true;
}
void OnGetTarget(TargetValue* target) const override {}
void OnAbort(ui::LayerAnimationDelegate* delegate) override {}
private:
std::unique_ptr<ui::InterpolatedTransform> transform_;
AppListView* view_;
gfx::Tween::Type tween_type_;
DISALLOW_COPY_AND_ASSIGN(AppListAnimation);
};
////////////////////////////////////////////////////////////////////////////////
// AppListView::TestApi
AppListView::TestApi::TestApi(AppListView* view) : view_(view) {}
AppListView::TestApi::~TestApi() = default;
AppsGridView* AppListView::TestApi::GetRootAppsGridView() {
return view_->GetRootAppsGridView();
}
////////////////////////////////////////////////////////////////////////////////
// AppListView:
AppListView::AppListView(AppListViewDelegate* delegate)
: delegate_(delegate),
model_(delegate->GetModel()),
search_model_(delegate->GetSearchModel()),
is_background_blur_enabled_(app_list_features::IsBackgroundBlurEnabled()),
hide_view_animation_observer_(
std::make_unique<HideViewAnimationObserver>()),
transition_animation_observer_(
std::make_unique<TransitionAnimationObserver>(this)),
state_animation_metrics_reporter_(
std::make_unique<StateAnimationMetricsReporter>()),
weak_ptr_factory_(this) {
CHECK(delegate);
}
AppListView::~AppListView() {
hide_view_animation_observer_.reset();
// Remove child views first to ensure no remaining dependencies on delegate_.
RemoveAllChildViews(true);
}
// static
void AppListView::ExcludeWindowFromEventHandling(aura::Window* window) {
DCHECK(window);
window->SetProperty(kExcludeWindowFromEventHandling, true);
}
// static
void AppListView::SetShortAnimationForTesting(bool enabled) {
short_animations_for_testing = enabled;
}
// static
bool AppListView::ShortAnimationsForTesting() {
return short_animations_for_testing;
}
void AppListView::Initialize(const InitParams& params) {
base::Time start_time = base::Time::Now();
is_tablet_mode_ = params.is_tablet_mode;
is_side_shelf_ = params.is_side_shelf;
InitContents(params.initial_apps_page);
AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
AddAccelerator(ui::Accelerator(ui::VKEY_BROWSER_BACK, ui::EF_NONE));
parent_window_ = params.parent;
InitializeFullscreen(params.parent);
InitChildWidgets();
SetState(app_list_state_);
// Ensures that the launcher won't open underneath the a11y keyboard
CloseKeyboardIfVisible();
// Tablet mode is enabled before the app list is shown, so apply the changes
// that should occur upon entering the tablet mode here.
if (is_tablet_mode())
OnTabletModeChanged(is_tablet_mode_);
UMA_HISTOGRAM_TIMES(kAppListCreationTimeHistogram,
base::Time::Now() - start_time);
RecordFolderMetrics();
}
void AppListView::SetDragAndDropHostOfCurrentAppList(
ApplicationDragAndDropHost* drag_and_drop_host) {
app_list_main_view_->SetDragAndDropHostOfCurrentAppList(drag_and_drop_host);
}
void AppListView::ShowWhenReady() {
app_list_main_view_->ShowAppListWhenReady();
}
void AppListView::Dismiss() {
CloseKeyboardIfVisible();
app_list_main_view_->Close();
SetState(AppListViewState::CLOSED);
delegate_->DismissAppList();
GetWidget()->Deactivate();
}
bool AppListView::CloseOpenedPage() {
if (!app_list_main_view_)
return false;
if (app_list_main_view_->contents_view()->IsShowingSearchResults() ||
GetAppsContainerView()->IsInFolderView()) {
return app_list_main_view_->contents_view()->Back();
}
return false;
}
void AppListView::Back() {
app_list_main_view_->contents_view()->Back();
}
void AppListView::OnPaint(gfx::Canvas* canvas) {
views::WidgetDelegateView::OnPaint(canvas);
if (!next_paint_callback_.is_null()) {
next_paint_callback_.Run();
next_paint_callback_.Reset();
}
}
const char* AppListView::GetClassName() const {
return "AppListView";
}
bool AppListView::CanProcessEventsWithinSubtree() const {
if (!delegate_->CanProcessEventsOnApplistViews())
return false;
return views::View::CanProcessEventsWithinSubtree();
}
bool AppListView::AcceleratorPressed(const ui::Accelerator& accelerator) {
switch (accelerator.key_code()) {
case ui::VKEY_ESCAPE:
case ui::VKEY_BROWSER_BACK:
// If the ContentsView does not handle the back action, then this is the
// top level, so we close the app list.
if (!app_list_main_view_->contents_view()->Back() && !is_tablet_mode()) {
Dismiss();
}
break;
default:
NOTREACHED();
return false;
}
// Don't let DialogClientView handle the accelerator.
return true;
}
void AppListView::Layout() {
const gfx::Rect contents_bounds = GetContentsBounds();
// Exclude the shelf height from the contents bounds to avoid apps grid from
// overlapping with shelf.
gfx::Rect main_bounds = contents_bounds;
main_bounds.Inset(0, 0, 0, AppListConfig::instance().shelf_height());
// The AppListMainView's size is supposed to be the same as AppsContainerView.
const gfx::Size min_main_size = GetAppsContainerView()->GetMinimumSize();
if ((main_bounds.width() > 0 && main_bounds.height() > 0) &&
(main_bounds.width() < min_main_size.width() ||
main_bounds.height() < min_main_size.height())) {
// Scale down the AppListMainView if AppsContainerView does not fit in the
// display.
const float scale = std::min(
(main_bounds.width()) / static_cast<float>(min_main_size.width()),
main_bounds.height() / static_cast<float>(min_main_size.height()));
DCHECK_GT(scale, 0);
const gfx::RectF scaled_main_bounds(main_bounds.x(), main_bounds.y(),
main_bounds.width() / scale,
main_bounds.height() / scale);
gfx::Transform transform;
transform.Scale(scale, scale);
app_list_main_view_->SetTransform(transform);
app_list_main_view_->SetBoundsRect(gfx::ToEnclosedRect(scaled_main_bounds));
} else {
app_list_main_view_->SetTransform(gfx::Transform());
app_list_main_view_->SetBoundsRect(main_bounds);
}
gfx::Rect app_list_background_shield_bounds = contents_bounds;
// Inset bottom by 2 * |kAppListBackgroundRadius| to account for the rounded
// corners on the top and bottom of the |app_list_background_shield_|.
// Only add the inset to the bottom to keep padding at the top of the AppList
// the same.
app_list_background_shield_bounds.Inset(0, 0, 0,
-kAppListBackgroundRadius * 2);
app_list_background_shield_->SetBoundsRect(app_list_background_shield_bounds);
app_list_background_shield_->UpdateCornerRadius(kAppListBackgroundRadius);
if (is_background_blur_enabled_ && app_list_background_shield_mask_ &&
!is_tablet_mode() &&
app_list_background_shield_->layer()->size() !=
app_list_background_shield_mask_->layer()->size()) {
// Update the blur mask for the |app_list_background_shield_| with same
// shape and size if their bounds don't match.
app_list_background_shield_mask_->layer()->SetBounds(
app_list_background_shield_bounds);
}
UpdateAppListBackgroundYPosition();
}
ax::mojom::Role AppListView::GetAccessibleWindowRole() const {
// Default role of root view is ax::mojom::Role::kWindow which traps ChromeVox
// focus within the root view. Assign ax::mojom::Role::kGroup here to allow
// the focus to move from elements in app list view to search box.
return ax::mojom::Role::kGroup;
}
class AppListView::FullscreenWidgetObserver : views::WidgetObserver {
public:
explicit FullscreenWidgetObserver(app_list::AppListView* view)
: widget_observer_(this) {
view_ = view;
widget_observer_.Add(view_->GetWidget());
}
~FullscreenWidgetObserver() override {}
// Overridden from WidgetObserver:
void OnWidgetClosing(views::Widget* widget) override {
if (view_->app_list_state() != AppListViewState::CLOSED)
view_->SetState(AppListViewState::CLOSED);
widget_observer_.Remove(view_->GetWidget());
}
private:
app_list::AppListView* view_;
ScopedObserver<views::Widget, WidgetObserver> widget_observer_;
DISALLOW_COPY_AND_ASSIGN(FullscreenWidgetObserver);
};
views::View* AppListView::GetAppListBackgroundShieldForTest() {
return app_list_background_shield_;
}
SkColor AppListView::GetAppListBackgroundShieldColorForTest() {
DCHECK(app_list_background_shield_);
return app_list_background_shield_->GetColorForTest();
}
void AppListView::InitContents(int initial_apps_page) {
// The shield view that colors/blurs the background of the app list and
// makes it transparent.
bool use_background_blur = is_background_blur_enabled_ && !is_tablet_mode();
app_list_background_shield_ = new AppListBackgroundShieldView(
use_background_blur ? ui::LAYER_SOLID_COLOR : ui::LAYER_TEXTURED);
SetBackgroundShieldColor();
if (use_background_blur) {
app_list_background_shield_mask_ = views::Painter::CreatePaintedLayer(
views::Painter::CreateSolidRoundRectPainter(SK_ColorBLACK,
kAppListBackgroundRadius));
app_list_background_shield_mask_->layer()->SetFillsBoundsOpaquely(false);
app_list_background_shield_->layer()->SetMaskLayer(
app_list_background_shield_mask_->layer());
app_list_background_shield_->layer()->SetBackgroundBlur(
AppListConfig::instance().blur_radius());
app_list_background_shield_->layer()->SetBackdropFilterQuality(
kAppListBlurQuality);
}
AddChildView(app_list_background_shield_);
app_list_main_view_ = new AppListMainView(delegate_, this);
AddChildView(app_list_main_view_);
// This will be added to the |search_box_widget_| after the app list widget is
// initialized.
search_box_view_ = new SearchBoxView(app_list_main_view_, delegate_, this);
search_box_view_->Init();
app_list_main_view_->Init(0, search_box_view_);
announcement_view_ = new views::View();
AddChildView(announcement_view_);
}
void AppListView::InitChildWidgets() {
DCHECK(search_box_view_);
// Create the search box widget.
views::Widget::InitParams search_box_widget_params(
views::Widget::InitParams::TYPE_CONTROL);
search_box_widget_params.parent = GetWidget()->GetNativeView();
search_box_widget_params.opacity =
views::Widget::InitParams::TRANSLUCENT_WINDOW;
search_box_widget_params.name = "SearchBoxView";
search_box_widget_params.delegate = search_box_view_;
// Create a widget for the SearchBoxView to live in. This allows the
// SearchBoxView to be on top of the custom launcher page's WebContents
// (otherwise the search box events will be captured by the WebContents).
search_box_widget_ = new views::Widget;
search_box_widget_->Init(search_box_widget_params);
// Assign an accessibility role to the native window of search box widget, so
// that hitting search+right could move ChromeVox focus across search box to
// other elements in app list view.
search_box_widget_->GetNativeWindow()->SetProperty(
ui::kAXRoleOverride,
static_cast<ax::mojom::Role>(ax::mojom::Role::kGroup));
// The search box will not naturally receive focus by itself (because it is in
// a separate widget). Create this SearchBoxFocusHost in the main widget to
// forward the focus search into to the search box.
search_box_focus_host_ = new SearchBoxFocusHost(search_box_widget_);
AddChildView(search_box_focus_host_);
search_box_widget_->SetFocusTraversableParentView(search_box_focus_host_);
search_box_widget_->SetFocusTraversableParent(
GetWidget()->GetFocusTraversable());
app_list_main_view_->contents_view()->Layout();
}
void AppListView::InitializeFullscreen(gfx::NativeView parent) {
fullscreen_widget_ = new views::Widget;
views::Widget::InitParams app_list_overlay_view_params(
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
app_list_overlay_view_params.name = "AppList";
app_list_overlay_view_params.parent = parent;
app_list_overlay_view_params.delegate = this;
app_list_overlay_view_params.opacity =
views::Widget::InitParams::TRANSLUCENT_WINDOW;
app_list_overlay_view_params.layer_type = ui::LAYER_NOT_DRAWN;
fullscreen_widget_->Init(app_list_overlay_view_params);
fullscreen_widget_->GetNativeWindow()->SetEventTargeter(
std::make_unique<AppListEventTargeter>());
// The widget's initial position will be off the bottom of the display.
// Set native view's bounds directly to avoid screen position controller
// setting bounds in the display where the widget has the largest
// intersection.
// TODO(mash): Redesign this animation to position the widget to cover the
// entire screen, then animate the layer up into position. crbug.com/768437
// The initial bounds of app list should be the same as that in closed state.
fullscreen_widget_->GetNativeView()->SetBounds(
GetPreferredWidgetBoundsForState(AppListViewState::CLOSED));
// Enable arrow key in FocusManager. Arrow left/right and up/down triggers
// the same focus movement as tab/shift+tab.
fullscreen_widget_->GetFocusManager()
->set_arrow_key_traversal_enabled_for_widget(true);
widget_observer_ = std::make_unique<FullscreenWidgetObserver>(this);
}
void AppListView::HandleClickOrTap(ui::LocatedEvent* event) {
// If the virtual keyboard is visible, dismiss the keyboard and return early.
if (CloseKeyboardIfVisible()) {
search_box_view_->NotifyGestureEvent();
return;
}
// Clear focus if the located event is not handled by any child view.
GetFocusManager()->ClearFocus();
if (GetAppsContainerView()->IsInFolderView()) {
// Close the folder if it is opened.
GetAppsContainerView()->app_list_folder_view()->CloseFolderPage();
return;
}
if ((event->IsGestureEvent() &&
(event->AsGestureEvent()->type() == ui::ET_GESTURE_LONG_PRESS ||
event->AsGestureEvent()->type() == ui::ET_GESTURE_LONG_TAP ||
event->AsGestureEvent()->type() == ui::ET_GESTURE_TWO_FINGER_TAP)) ||
(event->IsMouseEvent() &&
event->AsMouseEvent()->IsOnlyRightMouseButton())) {
// Don't show menus on empty areas of the AppListView in clamshell mode.
if (!is_tablet_mode())
return;
// Home launcher is shown on top of wallpaper with trasparent background. So
// trigger the wallpaper context menu for the same events.
gfx::Point onscreen_location(event->location());
ConvertPointToScreen(this, &onscreen_location);
delegate_->ShowWallpaperContextMenu(
onscreen_location, event->IsGestureEvent() ? ui::MENU_SOURCE_TOUCH
: ui::MENU_SOURCE_MOUSE);
return;
}
if (!search_box_view_->is_search_box_active()) {
if (!is_tablet_mode())
Dismiss();
return;
}
search_box_view_->ClearSearch();
search_box_view_->SetSearchBoxActive(false, ui::ET_UNKNOWN);
}
void AppListView::StartDrag(const gfx::Point& location) {
// Convert drag point from widget coordinates to screen coordinates because
// the widget bounds changes during the dragging.
initial_drag_point_ = location;
ConvertPointToScreen(this, &initial_drag_point_);
initial_window_bounds_ = fullscreen_widget_->GetWindowBoundsInScreen();
if (app_list_state_ == AppListViewState::PEEKING)
drag_started_from_peeking_ = true;
}
void AppListView::UpdateDrag(const gfx::Point& location) {
// Update the widget bounds based on the initial widget bounds and drag delta.
gfx::Point location_in_screen_coordinates = location;
ConvertPointToScreen(this, &location_in_screen_coordinates);
int new_y_position = location_in_screen_coordinates.y() -
initial_drag_point_.y() + initial_window_bounds_.y();
UpdateYPositionAndOpacity(new_y_position,
GetAppListBackgroundOpacityDuringDragging());
}
void AppListView::EndDrag(const gfx::Point& location) {
// When the SearchBoxView closes the app list, ignore the final event.
if (app_list_state_ == AppListViewState::CLOSED)
return;
// Change the app list state based on where the drag ended. If fling velocity
// was over the threshold, snap to the next state in the direction of the
// fling.
if (std::abs(last_fling_velocity_) >= kDragVelocityThreshold) {
// If the user releases drag with velocity over the threshold, snap to
// the next state, ignoring the drag release position.
if (last_fling_velocity_ > 0) {
switch (app_list_state_) {
case AppListViewState::PEEKING:
case AppListViewState::HALF:
case AppListViewState::FULLSCREEN_SEARCH:
case AppListViewState::FULLSCREEN_ALL_APPS:
Dismiss();
break;
case AppListViewState::CLOSED:
NOTREACHED();
break;
}
} else {
switch (app_list_state_) {
case AppListViewState::FULLSCREEN_ALL_APPS:
case AppListViewState::FULLSCREEN_SEARCH:
SetState(app_list_state_);
break;
case AppListViewState::HALF:
SetState(AppListViewState::FULLSCREEN_SEARCH);
break;
case AppListViewState::PEEKING:
UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram,
kSwipe, kMaxPeekingToFullscreen);
SetState(AppListViewState::FULLSCREEN_ALL_APPS);
break;
case AppListViewState::CLOSED:
NOTREACHED();
break;
}
}
} else {
const int fullscreen_height = GetFullscreenStateHeight();
int app_list_height = 0;
switch (app_list_state_) {
case AppListViewState::FULLSCREEN_ALL_APPS:
case AppListViewState::FULLSCREEN_SEARCH:
app_list_height = fullscreen_height;
break;
case AppListViewState::HALF:
app_list_height = std::min(fullscreen_height, kHalfAppListHeight);
break;
case AppListViewState::PEEKING: {
const int peeking_height =
AppListConfig::instance().peeking_app_list_height();
app_list_height = peeking_height;
break;
}
case AppListViewState::CLOSED:
NOTREACHED();
break;
}
const int app_list_threshold =
app_list_height / kAppListThresholdDenominator;
gfx::Point location_in_screen_coordinates = location;
ConvertPointToScreen(this, &location_in_screen_coordinates);
const int drag_delta =
initial_drag_point_.y() - location_in_screen_coordinates.y();
const int location_y_in_current_work_area =
location_in_screen_coordinates.y() -
GetDisplayNearestView().work_area().y();
// If the drag ended near the bezel, close the app list and return early.
if (location_y_in_current_work_area >=
(fullscreen_height - kAppListBezelMargin)) {
Dismiss();
return;
}
switch (app_list_state_) {
case AppListViewState::FULLSCREEN_ALL_APPS:
if (drag_delta < -app_list_threshold) {
if (is_tablet_mode_ || is_side_shelf_)
Dismiss();
else
SetState(AppListViewState::PEEKING);
} else {
SetState(app_list_state_);
}
break;
case AppListViewState::FULLSCREEN_SEARCH:
if (drag_delta < -app_list_threshold)
Dismiss();
else
SetState(app_list_state_);
break;
case AppListViewState::HALF:
if (drag_delta > app_list_threshold)
SetState(AppListViewState::FULLSCREEN_SEARCH);
else if (drag_delta < -app_list_threshold)
Dismiss();
else
SetState(app_list_state_);
break;
case AppListViewState::PEEKING:
if (drag_delta > app_list_threshold) {
SetState(AppListViewState::FULLSCREEN_ALL_APPS);
UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram,
kSwipe, kMaxPeekingToFullscreen);
} else if (drag_delta < -app_list_threshold) {
Dismiss();
} else {
SetState(app_list_state_);
}
break;
case AppListViewState::CLOSED:
NOTREACHED();
break;
}
}
drag_started_from_peeking_ = false;
UpdateChildViewsYPositionAndOpacity();
initial_drag_point_ = gfx::Point();
}
void AppListView::SetChildViewsForStateTransition(
AppListViewState target_state) {
if (target_state != AppListViewState::PEEKING &&
target_state != AppListViewState::FULLSCREEN_ALL_APPS)
return;
if (GetAppsContainerView()->IsInFolderView())
GetAppsContainerView()->ResetForShowApps();
if (target_state == AppListViewState::PEEKING) {
app_list_main_view_->contents_view()->SetActiveState(
ash::AppListState::kStateStart);
// Set the apps to first page at STATE_START state.
PaginationModel* pagination_model = GetAppsPaginationModel();
if (pagination_model->total_pages() > 0 &&
pagination_model->selected_page() != 0) {
pagination_model->SelectPage(0, false /* animate */);
}
} else {
app_list_main_view_->contents_view()->SetActiveState(
ash::AppListState::kStateApps, !is_side_shelf_);
}
}
void AppListView::ConvertAppListStateToFullscreenEquivalent(
AppListViewState* target_state) {
if (!(is_side_shelf_ || is_tablet_mode_))
return;
// If side shelf or tablet mode are active, all transitions should be
// made to the tablet mode/side shelf friendly versions.
if (*target_state == AppListViewState::HALF) {
*target_state = AppListViewState::FULLSCREEN_SEARCH;
} else if (*target_state == AppListViewState::PEEKING) {
// FULLSCREEN_ALL_APPS->PEEKING in tablet/side shelf mode should close
// instead of going to PEEKING.
*target_state = app_list_state_ == AppListViewState::FULLSCREEN_ALL_APPS
? AppListViewState::CLOSED
: AppListViewState::FULLSCREEN_ALL_APPS;
}
}
void AppListView::RecordStateTransitionForUma(AppListViewState new_state) {
AppListStateTransitionSource transition =
GetAppListStateTransitionSource(new_state);
// kMaxAppListStateTransition denotes a transition we are not interested in
// recording (ie. PEEKING->PEEKING).
if (transition == kMaxAppListStateTransition)
return;
UMA_HISTOGRAM_ENUMERATION(kAppListStateTransitionSourceHistogram, transition,
kMaxAppListStateTransition);
switch (transition) {
case kPeekingToFullscreenAllApps:
case KHalfToFullscreenSearch:
base::RecordAction(base::UserMetricsAction("AppList_PeekingToFull"));
break;
case kFullscreenAllAppsToPeeking:
base::RecordAction(base::UserMetricsAction("AppList_FullToPeeking"));
break;
default:
break;
}
}
void AppListView::MaybeCreateAccessibilityEvent(AppListViewState new_state) {
if (new_state != AppListViewState::PEEKING &&
new_state != AppListViewState::FULLSCREEN_ALL_APPS)
return;
base::string16 state_announcement;
if (new_state == AppListViewState::PEEKING) {
state_announcement = l10n_util::GetStringUTF16(
IDS_APP_LIST_SUGGESTED_APPS_ACCESSIBILITY_ANNOUNCEMENT);
} else {
state_announcement = l10n_util::GetStringUTF16(
IDS_APP_LIST_ALL_APPS_ACCESSIBILITY_ANNOUNCEMENT);
}
announcement_view_->GetViewAccessibility().OverrideName(state_announcement);
announcement_view_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
}
display::Display AppListView::GetDisplayNearestView() const {
return display::Screen::GetScreen()->GetDisplayNearestView(parent_window_);
}
AppsContainerView* AppListView::GetAppsContainerView() {
return app_list_main_view_->contents_view()->GetAppsContainerView();
}
AppsGridView* AppListView::GetRootAppsGridView() {
return GetAppsContainerView()->apps_grid_view();
}
AppsGridView* AppListView::GetFolderAppsGridView() {
return GetAppsContainerView()->app_list_folder_view()->items_grid_view();
}
AppListStateTransitionSource AppListView::GetAppListStateTransitionSource(
AppListViewState target_state) const {
switch (app_list_state_) {
case AppListViewState::CLOSED:
// CLOSED->X transitions are not useful for UMA.
return kMaxAppListStateTransition;
case AppListViewState::PEEKING:
switch (target_state) {
case AppListViewState::CLOSED:
return kPeekingToClosed;
case AppListViewState::HALF:
return kPeekingToHalf;
case AppListViewState::FULLSCREEN_ALL_APPS:
return kPeekingToFullscreenAllApps;
case AppListViewState::PEEKING:
// PEEKING->PEEKING is used when resetting the widget position after a
// failed state transition. Not useful for UMA.
return kMaxAppListStateTransition;
case AppListViewState::FULLSCREEN_SEARCH:
// PEEKING->FULLSCREEN_SEARCH is not a valid transition.
NOTREACHED();
return kMaxAppListStateTransition;
}
case AppListViewState::HALF:
switch (target_state) {
case AppListViewState::CLOSED:
return kHalfToClosed;
case AppListViewState::PEEKING:
return kHalfToPeeking;
case AppListViewState::FULLSCREEN_SEARCH:
return KHalfToFullscreenSearch;
case AppListViewState::HALF:
// HALF->HALF is used when resetting the widget position after a
// failed state transition. Not useful for UMA.
return kMaxAppListStateTransition;
case AppListViewState::FULLSCREEN_ALL_APPS:
// HALF->FULLSCREEN_ALL_APPS is not a valid transition.
NOTREACHED();
return kMaxAppListStateTransition;
}
case AppListViewState::FULLSCREEN_ALL_APPS:
switch (target_state) {
case AppListViewState::CLOSED:
return kFullscreenAllAppsToClosed;
case AppListViewState::PEEKING:
return kFullscreenAllAppsToPeeking;
case AppListViewState::FULLSCREEN_SEARCH:
return kFullscreenAllAppsToFullscreenSearch;
case AppListViewState::HALF:
// FULLSCREEN_ALL_APPS->HALF is not a valid transition.
NOTREACHED();
return kMaxAppListStateTransition;
case AppListViewState::FULLSCREEN_ALL_APPS:
// FULLSCREEN_ALL_APPS->FULLSCREEN_ALL_APPS is used when resetting the
// widget positon after a failed state transition. Not useful for UMA.
return kMaxAppListStateTransition;
}
case AppListViewState::FULLSCREEN_SEARCH:
switch (target_state) {
case AppListViewState::CLOSED:
return kFullscreenSearchToClosed;
case AppListViewState::FULLSCREEN_ALL_APPS:
return kFullscreenSearchToFullscreenAllApps;
case AppListViewState::FULLSCREEN_SEARCH:
// FULLSCREEN_SEARCH->FULLSCREEN_SEARCH is used when resetting the
// widget position after a failed state transition. Not useful for
// UMA.
return kMaxAppListStateTransition;
case AppListViewState::PEEKING:
// FULLSCREEN_SEARCH->PEEKING is not a valid transition.
NOTREACHED();
return kMaxAppListStateTransition;
case AppListViewState::HALF:
// FULLSCREEN_SEARCH->HALF is not a valid transition.
NOTREACHED();
return kMaxAppListStateTransition;
}
}
}
views::View* AppListView::GetInitiallyFocusedView() {
return app_list_main_view_->search_box_view()->search_box();
}
void AppListView::OnScrollEvent(ui::ScrollEvent* event) {
if (!HandleScroll(gfx::Vector2d(event->x_offset(), event->y_offset()),
event->type())) {
return;
}
event->SetHandled();
event->StopPropagation();
}
void AppListView::OnMouseEvent(ui::MouseEvent* event) {
switch (event->type()) {
case ui::ET_MOUSE_PRESSED:
event->SetHandled();
HandleClickOrTap(event);
break;
case ui::ET_MOUSEWHEEL:
if (HandleScroll(event->AsMouseWheelEvent()->offset(), ui::ET_MOUSEWHEEL))
event->SetHandled();
break;
default:
break;
}
}
void AppListView::OnGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::ET_GESTURE_TAP:
case ui::ET_GESTURE_LONG_PRESS:
case ui::ET_GESTURE_LONG_TAP:
case ui::ET_GESTURE_TWO_FINGER_TAP:
SetIsInDrag(false);
event->SetHandled();
HandleClickOrTap(event);
break;
case ui::ET_SCROLL_FLING_START:
case ui::ET_GESTURE_SCROLL_BEGIN: {
// If the search box is active when we start our drag, let it know.
if (search_box_view_->is_search_box_active())
search_box_view_->NotifyGestureEvent();
if (event->location().y() < kAppListHomeLaucherGesturesThreshold) {
if (delegate_->ProcessHomeLauncherGesture(event, gfx::Point())) {
SetIsInDrag(false);
event->SetHandled();
HandleClickOrTap(event);
return;
}
}
// Avoid scrolling events for the app list in tablet mode.
if (is_side_shelf_ || is_tablet_mode())
return;
// There may be multiple scroll begin events in one drag because the
// relative location of the finger and widget is almost unchanged and
// scroll begin event occurs when the relative location changes beyond a
// threshold. So avoid resetting the initial drag point in drag.
if (!is_in_drag_)
StartDrag(event->location());
SetIsInDrag(true);
event->SetHandled();
break;
}
case ui::ET_GESTURE_SCROLL_UPDATE: {
gfx::Point location_in_screen = event->location();
views::View::ConvertPointToScreen(this, &location_in_screen);
if (delegate_->ProcessHomeLauncherGesture(event, location_in_screen)) {
SetIsInDrag(true);
event->SetHandled();
return;
}
// Avoid scrolling events for the app list in tablet mode.
if (is_side_shelf_ || is_tablet_mode())
return;
SetIsInDrag(true);
last_fling_velocity_ = event->details().scroll_y();
UpdateDrag(event->location());
event->SetHandled();
break;
}
case ui::ET_GESTURE_END: {
gfx::Point location_in_screen = event->location();
views::View::ConvertPointToScreen(this, &location_in_screen);
if (delegate_->ProcessHomeLauncherGesture(event, location_in_screen)) {
SetIsInDrag(false);
event->SetHandled();
return;
}
if (!is_in_drag_)
break;
// Avoid scrolling events for the app list in tablet mode.
if (is_side_shelf_ || is_tablet_mode())
return;
SetIsInDrag(false);
EndDrag(event->location());
event->SetHandled();
break;
}
case ui::ET_MOUSEWHEEL: {
if (HandleScroll(event->AsMouseWheelEvent()->offset(), ui::ET_MOUSEWHEEL))
event->SetHandled();
break;
}
default:
break;
}
}
void AppListView::OnKeyEvent(ui::KeyEvent* event) {
RedirectKeyEventToSearchBox(event);
}
void AppListView::OnTabletModeChanged(bool started) {
is_tablet_mode_ = started;
// Bottom shelf is enforced in tablet mode. When tablet mode ends, the
// AppListView is destroyed so no need to update |is_side_shelf_|.
if (started)
is_side_shelf_ = false;
search_box_view_->OnTabletModeChanged(started);
search_model_->SetTabletMode(started);
GetAppsContainerView()->OnTabletModeChanged(started);
if (!started) {
Dismiss();
return;
}
if (is_in_drag_) {
SetIsInDrag(false);
UpdateChildViewsYPositionAndOpacity();
}
// Set fullscreen state. When current state is fullscreen, we still need to
// set it again because app list may be in dragging.
SetState(app_list_state_ == AppListViewState::HALF ||
app_list_state_ == AppListViewState::FULLSCREEN_SEARCH
? AppListViewState::FULLSCREEN_SEARCH
: AppListViewState::FULLSCREEN_ALL_APPS);
// Put app list window in corresponding container based on whether the
// tablet mode is enabled.
aura::Window* window = GetWidget()->GetNativeWindow();
aura::Window* root_window = window->GetRootWindow();
aura::Window* parent_window =
root_window->GetChildById(ash::kShellWindowId_AppListTabletModeContainer);
if (parent_window && !parent_window->Contains(window))
parent_window->AddChild(window);
// Update background color opacity.
SetBackgroundShieldColor();
// Update background blur.
if (is_background_blur_enabled_)
app_list_background_shield_->layer()->SetBackgroundBlur(0);
}
void AppListView::OnWallpaperColorsChanged() {
SetBackgroundShieldColor();
search_box_view_->OnWallpaperColorsChanged();
}
bool AppListView::HandleScroll(const gfx::Vector2d& offset,
ui::EventType type) {
// Ignore 0-offset events to prevent spurious dismissal, see crbug.com/806338
// The system generates 0-offset ET_SCROLL_FLING_CANCEL events during simple
// touchpad mouse moves. Those may be passed via mojo APIs and handled here.
if ((offset.y() == 0 && offset.x() == 0) || is_in_drag() ||
ShouldIgnoreScrollEvents()) {
return false;
}
if (app_list_state_ != AppListViewState::PEEKING &&
app_list_state_ != AppListViewState::FULLSCREEN_ALL_APPS) {
return false;
}
// Let the Apps grid view handle the event first in FULLSCREEN_ALL_APPS.
if (app_list_state_ == AppListViewState::FULLSCREEN_ALL_APPS) {
AppsGridView* apps_grid_view = GetAppsContainerView()->IsInFolderView()
? GetFolderAppsGridView()
: GetRootAppsGridView();
if (apps_grid_view->HandleScrollFromAppListView(offset, type))
return true;
}
// The AppList should not be dismissed with scroll in tablet mode.
if (is_tablet_mode())
return true;
// If the event is a mousewheel event, the offset is always large enough,
// otherwise the offset must be larger than the scroll threshold.
if (type == ui::ET_MOUSEWHEEL ||
abs(offset.y()) > kAppListMinScrollToSwitchStates) {
if (app_list_state_ == AppListViewState::FULLSCREEN_ALL_APPS) {
if (offset.y() > 0)
Dismiss();
return true;
}
SetState(AppListViewState::FULLSCREEN_ALL_APPS);
const AppListPeekingToFullscreenSource source =
type == ui::ET_MOUSEWHEEL ? kMousewheelScroll : kMousepadScroll;
UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram, source,
kMaxPeekingToFullscreen);
}
return true;
}
void AppListView::SetState(AppListViewState new_state) {
// Do not allow the state to be changed once it has been set to CLOSED.
if (app_list_state_ == AppListViewState::CLOSED)
return;
AppListViewState new_state_override = new_state;
ConvertAppListStateToFullscreenEquivalent(&new_state_override);
MaybeCreateAccessibilityEvent(new_state_override);
SetChildViewsForStateTransition(new_state_override);
StartAnimationForState(new_state_override);
RecordStateTransitionForUma(new_state_override);
model_->SetStateFullscreen(new_state_override);
app_list_state_ = new_state_override;
// Animations are skipped for side shelf mode, so trigger a layout to update
// children immediately.
if (is_side_shelf_)
Layout();
if (new_state_override == AppListViewState::CLOSED) {
return;
}
if (fullscreen_widget_->IsActive()) {
// Reset the focus to initially focused view. This should be
// done before updating visibility of views, because setting
// focused view invisible automatically moves focus to next
// focusable view, which potentially causes bugs.
GetInitiallyFocusedView()->RequestFocus();
}
// Updates the visibility of app list items according to the change of
// |app_list_state_|.
GetAppsContainerView()->UpdateControlVisibility(app_list_state_, is_in_drag_);
}
void AppListView::StartAnimationForState(AppListViewState target_state) {
if (is_side_shelf_)
return;
// The close animation is handled by the delegate.
if (target_state == AppListViewState::CLOSED)
return;
const display::Display display = GetDisplayNearestView();
const int target_state_y = GetPreferredWidgetYForState(target_state);
gfx::Rect target_bounds = fullscreen_widget_->GetNativeView()->bounds();
const int original_state_y = target_bounds.origin().y();
target_bounds.set_y(target_state_y);
int animation_duration;
// If animating to or from a fullscreen state, animate over 250ms, else
// animate over 200 ms.
if (ShortAnimationsForTesting()) {
animation_duration = kAppListAnimationDurationTestMs;
} else if (is_fullscreen() ||
target_state == AppListViewState::FULLSCREEN_ALL_APPS ||
target_state == AppListViewState::FULLSCREEN_SEARCH) {
animation_duration = kAppListAnimationDurationFromFullscreenMs;
} else {
animation_duration = kAppListAnimationDurationMs;
}
if (fullscreen_widget_->GetNativeView()->bounds().y() ==
display.work_area().bottom()) {
// If the animation start position is the bottom of the screen, activate the
// fade in animation. This prevents the search box from flashing at the
// bottom of the screen as it goes behind the shelf.
app_list_main_view_->contents_view()->FadeInOnOpen(
base::TimeDelta::FromMilliseconds(animation_duration));
}
ui::Layer* layer = fullscreen_widget_->GetLayer();
layer->SetBounds(target_bounds);
gfx::Transform transform;
const int y_offset = original_state_y - target_state_y;
transform.Translate(0, y_offset);
layer->SetTransform(transform);
ui::LayerAnimator* animator = layer->GetAnimator();
animator->StopAnimating();
ui::ScopedLayerAnimationSettings settings(animator);
settings.SetTransitionDuration(
base::TimeDelta::FromMilliseconds(animation_duration));
settings.SetTweenType(gfx::Tween::EASE_OUT);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings.SetAnimationMetricsReporter(state_animation_metrics_reporter_.get());
settings.AddObserver(transition_animation_observer_.get());
animator->StartAnimation(
new ui::LayerAnimationSequence(std::make_unique<AppListAnimation>(
gfx::PointF(0, y_offset), gfx::PointF(),
base::TimeDelta::FromMilliseconds(animation_duration),
gfx::Tween::EASE_OUT, this)));
}
void AppListView::StartCloseAnimation(base::TimeDelta animation_duration) {
if (is_side_shelf_)
return;
// If animating from PEEKING, animate the opacity twice as fast so the
// SearchBoxView does not flash behind the shelf.
if (app_list_state_ == AppListViewState::PEEKING ||
app_list_state_ == AppListViewState::CLOSED) {
animation_duration /= 2;
}
SetState(AppListViewState::CLOSED);
app_list_main_view_->contents_view()->FadeOutOnClose(animation_duration);
}
void AppListView::SetStateFromSearchBoxView(bool search_box_is_empty,
bool triggered_by_contents_change) {
switch (app_list_state_) {
case AppListViewState::PEEKING:
if (app_list_features::IsZeroStateSuggestionsEnabled()) {
if (!search_box_is_empty || search_box_view()->is_search_box_active())
SetState(AppListViewState::HALF);
} else {
if (!search_box_is_empty)
SetState(AppListViewState::HALF);
}
break;
case AppListViewState::HALF:
if (app_list_features::IsZeroStateSuggestionsEnabled()) {
if (search_box_is_empty && !triggered_by_contents_change)
SetState(AppListViewState::PEEKING);
} else {
if (search_box_is_empty)
SetState(AppListViewState::PEEKING);
}
break;
case AppListViewState::FULLSCREEN_SEARCH:
if (app_list_features::IsZeroStateSuggestionsEnabled()) {
if (search_box_is_empty && !triggered_by_contents_change) {
SetState(AppListViewState::FULLSCREEN_ALL_APPS);
app_list_main_view()->contents_view()->SetActiveState(
ash::AppListState::kStateApps);
}
} else {
if (search_box_is_empty) {
SetState(AppListViewState::FULLSCREEN_ALL_APPS);
app_list_main_view()->contents_view()->SetActiveState(
ash::AppListState::kStateApps);
}
}
break;
case AppListViewState::FULLSCREEN_ALL_APPS:
if (app_list_features::IsZeroStateSuggestionsEnabled()) {
if (!search_box_is_empty ||
(search_box_is_empty && triggered_by_contents_change))
SetState(AppListViewState::FULLSCREEN_SEARCH);
} else {
if (!search_box_is_empty)
SetState(AppListViewState::FULLSCREEN_SEARCH);
}
break;
case AppListViewState::CLOSED:
// We clean search on app list close.
break;
}
}
void AppListView::UpdateYPositionAndOpacity(int y_position_in_screen,
float background_opacity) {
DCHECK(!is_side_shelf_);
if (app_list_state_ == AppListViewState::CLOSED)
return;
if (fullscreen_widget_->GetLayer()->GetAnimator()->IsAnimatingProperty(
ui::LayerAnimationElement::TRANSFORM)) {
fullscreen_widget_->GetLayer()->GetAnimator()->StopAnimatingProperty(
ui::LayerAnimationElement::TRANSFORM);
}
SetIsInDrag(true);
background_opacity_in_drag_ = background_opacity;
gfx::Rect new_widget_bounds = fullscreen_widget_->GetWindowBoundsInScreen();
app_list_y_position_in_screen_ = std::min(
std::max(y_position_in_screen, GetDisplayNearestView().work_area().y()),
GetScreenBottom() - AppListConfig::instance().shelf_height());
new_widget_bounds.set_y(app_list_y_position_in_screen_);
gfx::NativeView native_view = fullscreen_widget_->GetNativeView();
::wm::ConvertRectFromScreen(native_view->parent(), &new_widget_bounds);
native_view->SetBounds(new_widget_bounds);
UpdateChildViewsYPositionAndOpacity();
}
void AppListView::OffsetYPositionOfAppList(int offset) {
gfx::NativeView native_view = fullscreen_widget_->GetNativeView();
gfx::Transform transform;
transform.Translate(0, offset);
native_view->SetTransform(transform);
}
PaginationModel* AppListView::GetAppsPaginationModel() {
return GetRootAppsGridView()->pagination_model();
}
gfx::Rect AppListView::GetAppInfoDialogBounds() const {
gfx::Rect app_info_bounds(GetDisplayNearestView().work_area());
app_info_bounds.ClampToCenteredSize(
gfx::Size(kAppInfoDialogWidth, kAppInfoDialogHeight));
return app_info_bounds;
}
void AppListView::SetIsInDrag(bool is_in_drag) {
if (app_list_state_ == AppListViewState::CLOSED)
return;
if (is_in_drag == is_in_drag_)
return;
is_in_drag_ = is_in_drag;
GetAppsContainerView()->UpdateControlVisibility(app_list_state_, is_in_drag_);
}
int AppListView::GetScreenBottom() const {
return GetDisplayNearestView().bounds().bottom();
}
int AppListView::GetCurrentAppListHeight() const {
if (!fullscreen_widget_)
return AppListConfig::instance().shelf_height();
return GetScreenBottom() - fullscreen_widget_->GetWindowBoundsInScreen().y();
}
float AppListView::GetAppListTransitionProgress() const {
const float current_height = GetCurrentAppListHeight();
const float peeking_height =
AppListConfig::instance().peeking_app_list_height();
if (current_height <= peeking_height) {
// Currently transition progress is between closed and peeking state.
// Calculate the progress of this transition.
const float shelf_height =
GetScreenBottom() - GetDisplayNearestView().work_area().bottom();
// When screen is rotated, the current height might be smaller than shelf
// height for just one moment, which results in negative progress. So force
// the progress to be non-negative.
return std::max(0.0f, (current_height - shelf_height) /
(peeking_height - shelf_height));
}
// Currently transition progress is between peeking and fullscreen state.
// Calculate the progress of this transition.
const float fullscreen_height_above_peeking =
GetFullscreenStateHeight() - peeking_height;
const float current_height_above_peeking = current_height - peeking_height;
DCHECK_GT(fullscreen_height_above_peeking, 0);
DCHECK_LE(current_height_above_peeking, fullscreen_height_above_peeking);
return 1 + current_height_above_peeking / fullscreen_height_above_peeking;
}
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();
}
AppListViewState AppListView::CalculateStateAfterShelfDrag(
const ui::GestureEvent& gesture_in_screen,
float launcher_above_shelf_bottom_amount) const {
AppListViewState app_list_state = AppListViewState::PEEKING;
if (gesture_in_screen.type() == ui::ET_SCROLL_FLING_START &&
fabs(gesture_in_screen.details().velocity_y()) > kDragVelocityThreshold) {
// If the scroll sequence terminates with a fling, show the fullscreen app
// list if the fling was fast enough and in the correct direction, otherwise
// close it.
app_list_state = gesture_in_screen.details().velocity_y() < 0
? AppListViewState::FULLSCREEN_ALL_APPS
: AppListViewState::CLOSED;
} else {
// Snap the app list to corresponding state according to the snapping
// thresholds.
if (is_tablet_mode_) {
app_list_state =
launcher_above_shelf_bottom_amount > kDragSnapToFullscreenThreshold
? AppListViewState::FULLSCREEN_ALL_APPS
: AppListViewState::CLOSED;
} else {
if (launcher_above_shelf_bottom_amount <= kDragSnapToClosedThreshold)
app_list_state = AppListViewState::CLOSED;
else if (launcher_above_shelf_bottom_amount <=
kDragSnapToPeekingThreshold)
app_list_state = AppListViewState::PEEKING;
else
app_list_state = AppListViewState::FULLSCREEN_ALL_APPS;
}
}
// Deal with the situation of dragging app list from shelf while typing in
// the search box.
if (app_list_state == AppListViewState::FULLSCREEN_ALL_APPS) {
ash::AppListState active_state =
app_list_main_view_->contents_view()->GetActiveState();
if (active_state == ash::AppListState::kStateSearchResults)
app_list_state = AppListViewState::FULLSCREEN_SEARCH;
}
return app_list_state;
}
void AppListView::UpdateChildViewsYPositionAndOpacity() {
if (app_list_state_ == AppListViewState::CLOSED)
return;
UpdateAppListBackgroundYPosition();
// Update the opacity of the background shield.
SetBackgroundShieldColor();
search_box_view_->UpdateOpacity();
app_list_main_view_->contents_view()->UpdateYPositionAndOpacity();
}
void AppListView::RedirectKeyEventToSearchBox(ui::KeyEvent* event) {
if (event->handled())
return;
// Allow text input inside the Assistant page.
if (app_list_main_view()->contents_view()->IsShowingEmbeddedAssistantUI())
return;
views::Textfield* search_box = search_box_view_->search_box();
const bool is_search_box_focused = search_box->HasFocus();
const bool is_folder_header_view_focused = GetAppsContainerView()
->app_list_folder_view()
->folder_header_view()
->HasTextFocus();
// Do not redirect the key event to the |search_box_| when focus is on a
// text field.
if (is_search_box_focused || is_folder_header_view_focused)
return;
// Do not redirect the arrow keys as they are are used for focus traversal.
if (IsUnhandledArrowKeyEvent(*event))
return;
// Redirect key event to |search_box_|.
search_box->OnKeyEvent(event);
if (event->handled()) {
// Set search box focused if the key event is consumed.
search_box->RequestFocus();
return;
}
// Insert it into search box if the key event is a character. Released
// key should not be handled to prevent inserting duplicate character.
if (event->type() == ui::ET_KEY_PRESSED)
search_box->InsertChar(*event);
}
void AppListView::OnScreenKeyboardShown(bool shown) {
if (onscreen_keyboard_shown_ == shown)
return;
onscreen_keyboard_shown_ = shown;
if (shown && GetAppsContainerView()->IsInFolderView()) {
// Move the app list up to prevent folders being blocked by the
// on-screen keyboard.
OffsetYPositionOfAppList(
GetAppsContainerView()->app_list_folder_view()->GetYOffsetForFolder());
} else {
// If the keyboard is closing or a folder isn't being shown, reset
// the app list's position
OffsetYPositionOfAppList(0);
}
}
bool AppListView::CloseKeyboardIfVisible() {
// TODO(ginko) abstract this function to be in |keyboard::KeyboardController|
if (!keyboard::KeyboardController::HasInstance())
return false;
auto* const keyboard_controller = keyboard::KeyboardController::Get();
if (keyboard_controller->IsKeyboardVisible()) {
keyboard_controller->HideKeyboardByUser();
return true;
}
return false;
}
void AppListView::OnParentWindowBoundsChanged() {
// Set the |fullscreen_widget_| size to fit the new display metrics.
fullscreen_widget_->GetNativeView()->SetBounds(
GetPreferredWidgetBoundsForState(app_list_state_));
// Update the |fullscreen_widget_| bounds to accomodate the new work
// area.
SetState(app_list_state_);
}
float AppListView::GetAppListBackgroundOpacityDuringDragging() {
float top_of_applist = fullscreen_widget_->GetWindowBoundsInScreen().y();
const int shelf_height = AppListConfig::instance().shelf_height();
float dragging_height =
std::max((GetScreenBottom() - shelf_height - top_of_applist), 0.f);
float coefficient =
std::min(dragging_height / (kNumOfShelfSize * shelf_height), 1.0f);
float shield_opacity =
is_background_blur_enabled_ ? kAppListOpacityWithBlur : kAppListOpacity;
// Assume shelf is opaque when start to drag down the launcher.
const float shelf_opacity = 1.0f;
return coefficient * shield_opacity + (1 - coefficient) * shelf_opacity;
}
void AppListView::GetWallpaperProminentColors(
AppListViewDelegate::GetWallpaperProminentColorsCallback callback) {
delegate_->GetWallpaperProminentColors(std::move(callback));
}
void AppListView::SetBackgroundShieldColor() {
// There is a chance when AppListView::OnWallpaperColorsChanged is called
// from AppListViewDelegate, the |app_list_background_shield_| is not
// initialized.
if (!app_list_background_shield_)
return;
// Opacity is set on the color instead of the layer because changing opacity
// of the layer changes opacity of the blur effect, which is not desired.
float color_opacity = kAppListOpacity;
if (is_tablet_mode_) {
// The Homecher background should have an opacity of 0.
color_opacity = 0;
} else if (is_in_drag_) {
// Allow a custom opacity while the AppListView is dragging to show a
// gradual opacity change when dragging from the shelf.
color_opacity = background_opacity_in_drag_;
} else if (is_background_blur_enabled_) {
color_opacity = kAppListOpacityWithBlur;
}
GetWallpaperProminentColors(base::BindOnce(
[](base::WeakPtr<AppListView> self, float color_opacity,
const std::vector<SkColor>& prominent_colors) {
self->app_list_background_shield_->UpdateColor(
GetBackgroundShieldColor(prominent_colors, color_opacity));
},
weak_ptr_factory_.GetWeakPtr(), color_opacity));
}
void AppListView::RecordFolderMetrics() {
int number_of_apps_in_folders = 0;
int number_of_folders = 0;
AppListItemList* item_list =
app_list_main_view_->model()->top_level_item_list();
for (size_t i = 0; i < item_list->item_count(); ++i) {
AppListItem* item = item_list->item_at(i);
if (item->GetItemType() != AppListFolderItem::kItemType)
continue;
++number_of_folders;
AppListFolderItem* folder = static_cast<AppListFolderItem*>(item);
if (folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM)
continue; // Don't count items in OEM folders.
number_of_apps_in_folders += folder->item_list()->item_count();
}
UMA_HISTOGRAM_COUNTS_100(kNumberOfFoldersHistogram, number_of_folders);
UMA_HISTOGRAM_COUNTS_100(kNumberOfAppsInFoldersHistogram,
number_of_apps_in_folders);
}
bool AppListView::ShouldIgnoreScrollEvents() {
// When the app list is doing state change animation or the apps grid view is
// in transition, ignore the scroll events to prevent triggering extra state
// changes or transtions.
return fullscreen_widget_->GetLayer()->GetAnimator()->is_animating() ||
GetRootAppsGridView()->pagination_model()->has_transition();
}
int AppListView::GetPreferredWidgetYForState(AppListViewState state) {
// Note that app list container fills the screen, so we can treat the
// container's y as the top of display.
const display::Display display = GetDisplayNearestView();
const gfx::Rect work_area_bounds = display.work_area();
switch (state) {
case AppListViewState::PEEKING:
return display.bounds().height() -
AppListConfig::instance().peeking_app_list_height();
case AppListViewState::HALF:
return std::max(work_area_bounds.y(),
display.bounds().height() - kHalfAppListHeight);
case AppListViewState::FULLSCREEN_ALL_APPS:
case AppListViewState::FULLSCREEN_SEARCH:
// The ChromeVox panel as well as the Docked Magnifier viewport affect the
// workarea of the display. We need to account for that when applist is in
// fullscreen to avoid being shown below them.
return work_area_bounds.y() - display.bounds().y();
case AppListViewState::CLOSED:
// Align the widget y with shelf y to avoid flicker in show animation. In
// side shelf mode, the widget y is the top of work area because the
// widget does not animate.
return (is_side_shelf_ ? work_area_bounds.y()
: work_area_bounds.bottom()) -
display.bounds().y();
}
}
gfx::Rect AppListView::GetPreferredWidgetBoundsForState(
AppListViewState state) {
// Use parent's width instead of display width to avoid 1 px gap (See
// https://crbug.com/884889).
CHECK(fullscreen_widget_);
aura::Window* parent = fullscreen_widget_->GetNativeView()->parent();
CHECK(parent);
return gfx::Rect(0, GetPreferredWidgetYForState(state),
parent->bounds().width(), GetFullscreenStateHeight());
}
void AppListView::UpdateAppListBackgroundYPosition() {
// Update the y position of the background shield.
gfx::Transform transform;
if (is_in_drag_) {
float app_list_transition_progress = GetAppListTransitionProgress();
if (app_list_transition_progress >= 1 &&
app_list_transition_progress <= 2) {
// Translate background shield so that it ends drag at y position
// -|kAppListBackgroundRadius| when dragging between peeking and
// fullscreen.
transform.Translate(
0, -kAppListBackgroundRadius * (app_list_transition_progress - 1));
}
} else if (is_fullscreen()) {
transform.Translate(0, -kAppListBackgroundRadius);
}
app_list_background_shield_->SetTransform(transform);
}
} // namespace app_list