blob: 0bac15b7b2ab6f8edd0963a7683af447347acb52 [file] [log] [blame]
// Copyright 2014 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 "ui/app_list/views/start_page_view.h"
#include <algorithm>
#include <string>
#include "base/i18n/rtl.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/app_list/app_list_constants.h"
#include "ui/app_list/app_list_item.h"
#include "ui/app_list/app_list_model.h"
#include "ui/app_list/app_list_view_delegate.h"
#include "ui/app_list/search_result.h"
#include "ui/app_list/views/all_apps_tile_item_view.h"
#include "ui/app_list/views/app_list_main_view.h"
#include "ui/app_list/views/contents_view.h"
#include "ui/app_list/views/custom_launcher_page_view.h"
#include "ui/app_list/views/search_box_view.h"
#include "ui/app_list/views/search_result_container_view.h"
#include "ui/app_list/views/search_result_tile_item_view.h"
#include "ui/app_list/views/tile_item_view.h"
#include "ui/gfx/canvas.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/widget/widget.h"
namespace app_list {
namespace {
// Layout constants.
constexpr int kInstantContainerSpacing = 24;
constexpr int kSearchBoxAndTilesSpacing = 35;
constexpr int kStartPageSearchBoxWidth = 480;
// WebView constants.
constexpr int kWebViewWidth = 700;
constexpr int kWebViewHeight = 224;
// Tile container constants.
constexpr int kTileSpacing = 7;
constexpr int kNumStartPageTilesCols = 5;
constexpr int kTilesHorizontalMarginLeft = 145;
constexpr int kLauncherPageBackgroundWidth = 400;
// An invisible placeholder view which fills the space for the search box view
// in a box layout. The search box view itself is a child of the AppListView
// (because it is visible on many different pages).
class SearchBoxSpacerView : public views::View {
public:
explicit SearchBoxSpacerView(const gfx::Size& search_box_size)
: size_(kStartPageSearchBoxWidth, search_box_size.height()) {}
~SearchBoxSpacerView() override {}
// Overridden from views::View:
gfx::Size GetPreferredSize() const override { return size_; }
private:
gfx::Size size_;
DISALLOW_COPY_AND_ASSIGN(SearchBoxSpacerView);
};
} // namespace
class CustomLauncherPageBackgroundView : public views::View {
public:
explicit CustomLauncherPageBackgroundView(
const std::string& custom_launcher_page_name)
: selected_(false),
custom_launcher_page_name_(custom_launcher_page_name) {
set_background(views::Background::CreateSolidBackground(kSelectedColor));
}
~CustomLauncherPageBackgroundView() override {}
void SetSelected(bool selected) {
selected_ = selected;
SetVisible(selected);
if (selected)
NotifyAccessibilityEvent(ui::AX_EVENT_SELECTION, true);
}
bool selected() { return selected_; }
// Overridden from views::View:
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
node_data->role = ui::AX_ROLE_BUTTON;
node_data->SetName(custom_launcher_page_name_);
}
private:
bool selected_;
std::string custom_launcher_page_name_;
DISALLOW_COPY_AND_ASSIGN(CustomLauncherPageBackgroundView);
};
// A container that holds the start page recommendation tiles and the all apps
// tile.
class StartPageView::StartPageTilesContainer
: public SearchResultContainerView {
public:
StartPageTilesContainer(ContentsView* contents_view,
AllAppsTileItemView* all_apps_button,
AppListViewDelegate* view_delegate);
~StartPageTilesContainer() override;
TileItemView* GetTileItemView(int index);
const std::vector<SearchResultTileItemView*>& tile_views() const {
return search_result_tile_views_;
}
AllAppsTileItemView* all_apps_button() { return all_apps_button_; }
// Overridden from SearchResultContainerView:
int Update() override;
void UpdateSelectedIndex(int old_selected, int new_selected) override;
void OnContainerSelected(bool from_bottom,
bool directional_movement) override;
void NotifyFirstResultYIndex(int y_index) override;
int GetYSize() override;
private:
void CreateAppsGrid(int apps_num);
ContentsView* contents_view_;
AppListViewDelegate* view_delegate_;
std::vector<SearchResultTileItemView*> search_result_tile_views_;
AllAppsTileItemView* all_apps_button_;
DISALLOW_COPY_AND_ASSIGN(StartPageTilesContainer);
};
StartPageView::StartPageTilesContainer::StartPageTilesContainer(
ContentsView* contents_view,
AllAppsTileItemView* all_apps_button,
AppListViewDelegate* view_delegate)
: contents_view_(contents_view),
view_delegate_(view_delegate),
all_apps_button_(all_apps_button) {
set_background(
views::Background::CreateSolidBackground(kLabelBackgroundColor));
all_apps_button_->SetHoverStyle(TileItemView::HOVER_STYLE_ANIMATE_SHADOW);
all_apps_button_->SetParentBackgroundColor(kLabelBackgroundColor);
CreateAppsGrid(kNumStartPageTiles);
}
StartPageView::StartPageTilesContainer::~StartPageTilesContainer() {
}
TileItemView* StartPageView::StartPageTilesContainer::GetTileItemView(
int index) {
DCHECK_GT(num_results(), index);
if (index == num_results() - 1)
return all_apps_button_;
return search_result_tile_views_[index];
}
int StartPageView::StartPageTilesContainer::Update() {
// Ignore updates and disable buttons when transitioning to a different
// state.
if (contents_view_->GetActiveState() != AppListModel::STATE_START) {
for (auto* view : search_result_tile_views_)
view->SetEnabled(false);
return num_results();
}
std::vector<SearchResult*> display_results =
AppListModel::FilterSearchResultsByDisplayType(
results(), SearchResult::DISPLAY_RECOMMENDATION, kNumStartPageTiles);
if (display_results.size() != search_result_tile_views_.size()) {
// We should recreate the grid layout in this case.
for (size_t i = 0; i < search_result_tile_views_.size(); ++i)
delete search_result_tile_views_[i];
search_result_tile_views_.clear();
RemoveChildView(all_apps_button_);
CreateAppsGrid(std::min(kNumStartPageTiles, display_results.size()));
}
// Update the tile item results.
for (size_t i = 0; i < search_result_tile_views_.size(); ++i) {
SearchResult* item = nullptr;
if (i < display_results.size())
item = display_results[i];
search_result_tile_views_[i]->SetSearchResult(item);
search_result_tile_views_[i]->SetEnabled(true);
}
Layout();
parent()->Layout();
// Add 1 to the results size to account for the all apps button.
return display_results.size() + 1;
}
void StartPageView::StartPageTilesContainer::UpdateSelectedIndex(
int old_selected,
int new_selected) {
if (old_selected >= 0)
GetTileItemView(old_selected)->SetSelected(false);
if (new_selected >= 0)
GetTileItemView(new_selected)->SetSelected(true);
}
void StartPageView::StartPageTilesContainer::OnContainerSelected(
bool /*from_bottom*/,
bool /*directional_movement*/) {
NOTREACHED();
}
void StartPageView::StartPageTilesContainer::NotifyFirstResultYIndex(
int /*y_index*/) {
NOTREACHED();
}
int StartPageView::StartPageTilesContainer::GetYSize() {
NOTREACHED();
return 0;
}
void StartPageView::StartPageTilesContainer::CreateAppsGrid(int apps_num) {
DCHECK(search_result_tile_views_.empty());
views::GridLayout* tiles_layout_manager = new views::GridLayout(this);
SetLayoutManager(tiles_layout_manager);
views::ColumnSet* column_set = tiles_layout_manager->AddColumnSet(0);
column_set->AddPaddingColumn(0, kTilesHorizontalMarginLeft);
for (int col = 0; col < kNumStartPageTilesCols; ++col) {
column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0,
views::GridLayout::USE_PREF, 0, 0);
column_set->AddPaddingColumn(0, kTileSpacing);
}
// Add SearchResultTileItemViews to the container.
int i = 0;
for (; i < apps_num; ++i) {
SearchResultTileItemView* tile_item =
new SearchResultTileItemView(this, view_delegate_);
if (i % kNumStartPageTilesCols == 0)
tiles_layout_manager->StartRow(0, 0);
tiles_layout_manager->AddView(tile_item);
AddChildView(tile_item);
tile_item->SetParentBackgroundColor(kLabelBackgroundColor);
tile_item->SetHoverStyle(TileItemView::HOVER_STYLE_ANIMATE_SHADOW);
search_result_tile_views_.push_back(tile_item);
}
// Also add a special "all apps" button to the end of the container.
all_apps_button_->UpdateIcon();
if (i % kNumStartPageTilesCols == 0)
tiles_layout_manager->StartRow(0, 0);
tiles_layout_manager->AddView(all_apps_button_);
AddChildView(all_apps_button_);
}
////////////////////////////////////////////////////////////////////////////////
// StartPageView implementation:
StartPageView::StartPageView(AppListMainView* app_list_main_view,
AppListViewDelegate* view_delegate)
: app_list_main_view_(app_list_main_view),
view_delegate_(view_delegate),
search_box_spacer_view_(new SearchBoxSpacerView(
app_list_main_view->search_box_view()->GetPreferredSize())),
instant_container_(new views::View),
custom_launcher_page_background_(new CustomLauncherPageBackgroundView(
view_delegate_->GetModel()->custom_launcher_page_name())),
tiles_container_(new StartPageTilesContainer(
app_list_main_view->contents_view(),
new AllAppsTileItemView(app_list_main_view_->contents_view()),
view_delegate)) {
// The view containing the start page WebContents and SearchBoxSpacerView.
InitInstantContainer();
AddChildView(instant_container_);
// The view containing the start page tiles.
AddChildView(tiles_container_);
AddChildView(custom_launcher_page_background_);
tiles_container_->SetResults(view_delegate_->GetModel()->results());
Reset();
}
StartPageView::~StartPageView() {
}
void StartPageView::InitInstantContainer() {
views::BoxLayout* instant_layout_manager = new views::BoxLayout(
views::BoxLayout::kVertical, 0, 0, kInstantContainerSpacing);
instant_layout_manager->set_inside_border_insets(
gfx::Insets(0, 0, kSearchBoxAndTilesSpacing, 0));
instant_layout_manager->set_main_axis_alignment(
views::BoxLayout::MAIN_AXIS_ALIGNMENT_END);
instant_layout_manager->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
instant_container_->SetLayoutManager(instant_layout_manager);
views::View* web_view = view_delegate_->CreateStartPageWebView(
gfx::Size(kWebViewWidth, kWebViewHeight));
if (web_view) {
web_view->SetFocusBehavior(FocusBehavior::NEVER);
instant_container_->AddChildView(web_view);
}
instant_container_->AddChildView(search_box_spacer_view_);
}
void StartPageView::MaybeOpenCustomLauncherPage() {
// Switch to the custom page.
ContentsView* contents_view = app_list_main_view_->contents_view();
if (!app_list_main_view_->ShouldShowCustomLauncherPage())
return;
UMA_HISTOGRAM_ENUMERATION(kPageOpenedHistogram,
AppListModel::STATE_CUSTOM_LAUNCHER_PAGE,
AppListModel::STATE_LAST);
contents_view->SetActiveState(AppListModel::STATE_CUSTOM_LAUNCHER_PAGE);
}
void StartPageView::Reset() {
tiles_container_->Update();
}
void StartPageView::UpdateForTesting() {
tiles_container_->Update();
}
const std::vector<SearchResultTileItemView*>& StartPageView::tile_views()
const {
return tiles_container_->tile_views();
}
TileItemView* StartPageView::all_apps_button() const {
return tiles_container_->all_apps_button();
}
void StartPageView::OnShown() {
// When the start page is shown, show or hide the custom launcher page
// based on whether it is enabled.
CustomLauncherPageView* custom_page_view =
app_list_main_view_->contents_view()->custom_page_view();
if (custom_page_view) {
custom_page_view->SetVisible(
app_list_main_view_->ShouldShowCustomLauncherPage());
}
tiles_container_->Update();
tiles_container_->ClearSelectedIndex();
custom_launcher_page_background_->SetSelected(false);
}
gfx::Rect StartPageView::GetPageBoundsForState(
AppListModel::State state) const {
gfx::Rect onscreen_bounds(GetFullContentsBounds());
if (state == AppListModel::STATE_START)
return onscreen_bounds;
return GetAboveContentsOffscreenBounds(onscreen_bounds.size());
}
gfx::Rect StartPageView::GetSearchBoxBounds() const {
return search_box_spacer_view_->bounds() +
GetPageBoundsForState(AppListModel::STATE_START).OffsetFromOrigin();
}
void StartPageView::Layout() {
gfx::Rect bounds(GetContentsBounds());
bounds.set_height(instant_container_->GetHeightForWidth(bounds.width()));
instant_container_->SetBoundsRect(bounds);
// Tiles begin where the instant container ends.
bounds.set_y(bounds.bottom());
bounds.set_height(tiles_container_->GetHeightForWidth(bounds.width()));
tiles_container_->SetBoundsRect(bounds);
CustomLauncherPageView* custom_launcher_page_view =
app_list_main_view_->contents_view()->custom_page_view();
if (!custom_launcher_page_view)
return;
bounds = app_list_main_view_->contents_view()
->custom_page_view()
->GetCollapsedLauncherPageBounds();
bounds.Intersect(GetContentsBounds());
bounds.ClampToCenteredSize(
gfx::Size(kLauncherPageBackgroundWidth, bounds.height()));
custom_launcher_page_background_->SetBoundsRect(bounds);
}
bool StartPageView::OnKeyPressed(const ui::KeyEvent& event) {
const int forward_dir = base::i18n::IsRTL() ? -1 : 1;
int selected_index = tiles_container_->selected_index();
if (custom_launcher_page_background_->selected()) {
selected_index = tiles_container_->num_results();
switch (event.key_code()) {
case ui::VKEY_RETURN:
MaybeOpenCustomLauncherPage();
return true;
default:
break;
}
} else if (selected_index >= 0 &&
tiles_container_->GetTileItemView(selected_index)
->OnKeyPressed(event)) {
return true;
}
int dir = 0;
switch (event.key_code()) {
case ui::VKEY_LEFT:
dir = -forward_dir;
break;
case ui::VKEY_RIGHT:
// Don't go to the custom launcher page from the All apps tile.
if (selected_index != tiles_container_->num_results() - 1)
dir = forward_dir;
break;
case ui::VKEY_UP:
// Up selects the first tile if the custom launcher is selected.
if (custom_launcher_page_background_->selected()) {
selected_index = -1;
dir = 1;
}
break;
case ui::VKEY_DOWN:
// Down selects the first tile if nothing is selected.
dir = 1;
// If something is selected, select the custom launcher page.
if (tiles_container_->IsValidSelectionIndex(selected_index))
selected_index = tiles_container_->num_results() - 1;
break;
case ui::VKEY_TAB:
dir = event.IsShiftDown() ? -1 : 1;
break;
default:
break;
}
if (dir == 0)
return false;
if (selected_index == -1) {
custom_launcher_page_background_->SetSelected(false);
tiles_container_->SetSelectedIndex(
dir == -1 ? tiles_container_->num_results() - 1 : 0);
return true;
}
int selection_index = selected_index + dir;
if (tiles_container_->IsValidSelectionIndex(selection_index)) {
custom_launcher_page_background_->SetSelected(false);
tiles_container_->SetSelectedIndex(selection_index);
return true;
}
if (selection_index == tiles_container_->num_results() &&
app_list_main_view_->ShouldShowCustomLauncherPage()) {
custom_launcher_page_background_->SetSelected(true);
tiles_container_->ClearSelectedIndex();
return true;
}
if (event.key_code() == ui::VKEY_TAB && selection_index == -1)
tiles_container_->ClearSelectedIndex(); // ContentsView will handle focus.
return false;
}
bool StartPageView::OnMousePressed(const ui::MouseEvent& event) {
ContentsView* contents_view = app_list_main_view_->contents_view();
if (contents_view->custom_page_view() &&
!contents_view->custom_page_view()
->GetCollapsedLauncherPageBounds()
.Contains(event.location())) {
return false;
}
MaybeOpenCustomLauncherPage();
return true;
}
bool StartPageView::OnMouseWheel(const ui::MouseWheelEvent& event) {
// Negative y_offset is a downward scroll.
if (event.y_offset() < 0) {
MaybeOpenCustomLauncherPage();
return true;
}
return false;
}
void StartPageView::OnGestureEvent(ui::GestureEvent* event) {
if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN &&
event->details().scroll_y_hint() < 0) {
MaybeOpenCustomLauncherPage();
}
ContentsView* contents_view = app_list_main_view_->contents_view();
if (event->type() == ui::ET_GESTURE_TAP &&
contents_view->custom_page_view() &&
contents_view->custom_page_view()
->GetCollapsedLauncherPageBounds()
.Contains(event->location())) {
MaybeOpenCustomLauncherPage();
}
}
void StartPageView::OnScrollEvent(ui::ScrollEvent* event) {
// Negative y_offset is a downward scroll (or upward, if Australian Scrolling
// is enabled).
if (event->type() == ui::ET_SCROLL && event->y_offset() < 0)
MaybeOpenCustomLauncherPage();
}
TileItemView* StartPageView::GetTileItemView(size_t index) {
return tiles_container_->GetTileItemView(index);
}
} // namespace app_list