// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/app_list/views/recent_apps_view.h"
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/model/search/search_model.h"
#include "ash/app_list/model/search/search_result.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/app_list_item_view_grid_delegate.h"
#include "ash/app_list/views/app_list_keyboard_controller.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_config_provider.h"
#include "ash/public/cpp/app_list/app_list_notifier.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "extensions/common/constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view_utils.h"
#include "url/gurl.h"
namespace ash {
namespace {
constexpr size_t kMinRecommendedApps = 4;
constexpr size_t kMaxRecommendedApps = 5;
// Converts a search result app ID to an app list item ID.
std::string ItemIdFromAppId(const std::string& app_id) {
// Convert chrome-extension://<id> to just <id>.
if (base::StartsWith(app_id, extensions::kExtensionScheme)) {
GURL url(app_id);
return url.host();
}
return app_id;
}
struct RecentAppInfo {
RecentAppInfo(AppListItem* item, SearchResult* result)
: item(item), result(result) {}
RecentAppInfo(const RecentAppInfo&) = default;
RecentAppInfo& operator=(RecentAppInfo&) = default;
~RecentAppInfo() = default;
raw_ptr<AppListItem> item;
raw_ptr<SearchResult> result;
};
// Returns a list of recent apps by filtering zero-state suggestion data.
std::vector<RecentAppInfo> GetRecentApps(
AppListModel* model,
SearchModel* search_model,
const std::vector<std::string>& ids_to_ignore) {
std::vector<RecentAppInfo> recent_apps;
SearchModel::SearchResults* results = search_model->results();
for (size_t i = 0; i < results->item_count(); ++i) {
SearchResult* result = results->GetItemAt(i);
if (result->display_type() != SearchResultDisplayType::kRecentApps)
continue;
std::string item_id = ItemIdFromAppId(result->id());
if (base::Contains(ids_to_ignore, item_id))
continue;
AppListItem* item = model->FindItem(item_id);
if (!item)
continue;
recent_apps.emplace_back(item, result);
if (recent_apps.size() == kMaxRecommendedApps)
break;
}
return recent_apps;
}
} // namespace
// The grid delegate for each AppListItemView. Recent app icons cannot be
// dragged, so this implementation is mostly a stub.
class RecentAppsView::GridDelegateImpl : public AppListItemViewGridDelegate {
public:
explicit GridDelegateImpl(AppListViewDelegate* view_delegate)
: view_delegate_(view_delegate) {}
GridDelegateImpl(const GridDelegateImpl&) = delete;
GridDelegateImpl& operator=(const GridDelegateImpl&) = delete;
~GridDelegateImpl() override = default;
// AppListItemView::GridDelegate:
bool IsInFolder() const override { return false; }
void SetSelectedView(AppListItemView* view) override {
DCHECK(view);
if (view == selected_view_)
return;
// Ensure the translucent background of the previous selection goes away.
if (selected_view_)
selected_view_->SchedulePaint();
selected_view_ = view;
// Ensure the translucent background of this selection is painted.
selected_view_->SchedulePaint();
selected_view_->NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true);
}
void ClearSelectedView() override { selected_view_ = nullptr; }
bool IsSelectedView(const AppListItemView* view) const override {
return view == selected_view_;
}
bool InitiateDrag(AppListItemView* view,
const gfx::Point& location,
const gfx::Point& root_location,
base::OnceClosure drag_start_callback,
base::OnceClosure drag_end_callback) override {
return false;
}
void StartDragAndDropHostDragAfterLongPress() override {}
bool UpdateDragFromItem(bool is_touch,
const ui::LocatedEvent& event) override {
return false;
}
void EndDrag(bool cancel) override {}
void OnAppListItemViewActivated(AppListItemView* pressed_item_view,
const ui::Event& event) override {
// NOTE: Avoid using |item->id()| as the parameter. In some rare situations,
// activating the item may destruct it. Using the reference to an object
// which may be destroyed during the procedure as the function parameter
// may bring the crash like https://crbug.com/990282.
const std::string id = pressed_item_view->item()->id();
RecordAppListByCollectionLaunched(
pressed_item_view->item()->collection_id(),
/*is_apps_collections_page=*/false);
// `this` may be deleted after activation.
view_delegate_->ActivateItem(id, event.flags(),
AppListLaunchedFrom::kLaunchedFromRecentApps,
IsAboveTheFold(pressed_item_view));
}
bool IsAboveTheFold(AppListItemView* item_view) override {
// Recent apps are always above the fold.
return true;
}
private:
const raw_ptr<AppListViewDelegate> view_delegate_;
raw_ptr<AppListItemView, DanglingUntriaged> selected_view_ = nullptr;
};
RecentAppsView::RecentAppsView(AppListKeyboardController* keyboard_controller,
AppListViewDelegate* view_delegate)
: keyboard_controller_(keyboard_controller),
view_delegate_(view_delegate),
grid_delegate_(std::make_unique<GridDelegateImpl>(view_delegate_)) {
DCHECK(keyboard_controller_);
DCHECK(view_delegate_);
layout_ = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
layout_->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
layout_->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
// TODO(https://crbug.com/1298211): This needs a designated string resource.
GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_RECENT_APPS_A11Y_NAME),
ax::mojom::NameFrom::kAttribute);
SetVisible(false);
}
RecentAppsView::~RecentAppsView() {
if (model_)
model_->RemoveObserver(this);
}
void RecentAppsView::OnAppListItemWillBeDeleted(AppListItem* item) {
std::vector<std::string> ids_to_remove;
for (AppListItemView* view : item_views_) {
if (view->item() && view->item() == item)
ids_to_remove.push_back(view->item()->id());
}
if (!ids_to_remove.empty()) {
UpdateResults(ids_to_remove);
UpdateVisibility();
}
}
void RecentAppsView::UpdateAppListConfig(const AppListConfig* app_list_config) {
app_list_config_ = app_list_config;
for (ash::AppListItemView* item_view : item_views_) {
item_view->UpdateAppListConfig(app_list_config);
}
}
void RecentAppsView::UpdateResults(
const std::vector<std::string>& ids_to_ignore) {
if (!search_model_ || !model_)
return;
DCHECK(app_list_config_);
item_views_.clear();
RemoveAllChildViews();
std::vector<RecentAppInfo> apps =
GetRecentApps(model_, search_model_, ids_to_ignore);
if (apps.size() < kMinRecommendedApps) {
if (auto* notifier = view_delegate_->GetNotifier()) {
notifier->NotifyResultsUpdated(SearchResultDisplayType::kRecentApps, {});
}
return;
}
if (auto* notifier = view_delegate_->GetNotifier()) {
std::vector<AppListNotifier::Result> notifier_results;
for (const RecentAppInfo& app : apps)
notifier_results.emplace_back(
app.result->id(), app.result->metrics_type(),
app.result->continue_file_suggestion_type());
notifier->NotifyResultsUpdated(SearchResultDisplayType::kRecentApps,
notifier_results);
}
for (const RecentAppInfo& app : apps) {
auto* item_view = AddChildView(std::make_unique<AppListItemView>(
app_list_config_, grid_delegate_.get(), app.item, view_delegate_,
AppListItemView::Context::kRecentAppsView));
item_view->UpdateAppListConfig(app_list_config_);
item_views_.push_back(item_view);
item_view->InitializeIconLoader();
}
NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged,
/*send_native_event=*/true);
}
void RecentAppsView::SetModels(SearchModel* search_model, AppListModel* model) {
if (model_ != model) {
if (model_)
model_->RemoveObserver(this);
model_ = model;
if (model_)
model_->AddObserver(this);
}
search_model_ = search_model;
UpdateResults(/*ids_to_ignore=*/{});
UpdateVisibility();
}
void RecentAppsView::UpdateVisibility() {
const bool has_enough_apps = item_views_.size() >= kMinRecommendedApps;
const bool hidden_by_user = view_delegate_->ShouldHideContinueSection();
const bool visible = has_enough_apps && !hidden_by_user;
SetVisible(visible);
if (auto* notifier = view_delegate_->GetNotifier()) {
notifier->NotifyContinueSectionVisibilityChanged(
SearchResultDisplayType::kRecentApps, visible);
}
}
int RecentAppsView::GetItemViewCount() const {
return item_views_.size();
}
AppListItemView* RecentAppsView::GetItemViewAt(int index) const {
if (static_cast<int>(item_views_.size()) <= index)
return nullptr;
return item_views_[index];
}
void RecentAppsView::DisableFocusForShowingActiveFolder(bool disabled) {
for (views::View* child : children())
child->SetEnabled(!disabled);
// Prevent items from being accessed by ChromeVox.
SetViewIgnoredForAccessibility(this, disabled);
}
bool RecentAppsView::OnKeyPressed(const ui::KeyEvent& event) {
if (event.key_code() == ui::VKEY_UP) {
MoveFocusUp();
return true;
}
if (event.key_code() == ui::VKEY_DOWN) {
MoveFocusDown();
return true;
}
return false;
}
void RecentAppsView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
// The AppsGridView's space between items is the sum of the padding on left
// and on right of the individual tiles. Because of rounding errors, there can
// be an actual difference of 1px over the actual distribution of space
// needed, and because this is not compensated on the other columns, the grid
// carries over the error making it progressively more significant for each
// column. For the RecentAppsView tiles to match the grid we need to calculate
// padding as the AppsGridView does to account for the rounding errors and
// then double it, so it is exactly the same spacing as the AppsGridView.
layout_->set_between_child_spacing(2 * CalculateTilePadding());
}
void RecentAppsView::MoveFocusUp() {
DVLOG(1) << __FUNCTION__;
// This function should only run when a child has focus.
DCHECK(Contains(GetFocusManager()->GetFocusedView()));
DCHECK(!children().empty());
keyboard_controller_->MoveFocusUpFromRecents();
}
void RecentAppsView::MoveFocusDown() {
DVLOG(1) << __FUNCTION__;
// This function should only run when a child has focus.
DCHECK(Contains(GetFocusManager()->GetFocusedView()));
int column = GetColumnOfFocusedChild();
DCHECK_GE(column, 0);
keyboard_controller_->MoveFocusDownFromRecents(column);
}
int RecentAppsView::GetColumnOfFocusedChild() const {
int column = 0;
for (views::View* child : children()) {
if (!views::IsViewClass<AppListItemView>(child))
continue;
if (child->HasFocus())
return column;
++column;
}
return -1;
}
int RecentAppsView::CalculateTilePadding() const {
int content_width = GetContentsBounds().width();
int tile_width = app_list_config_->grid_tile_width();
int width_to_distribute = content_width - kMaxRecommendedApps * tile_width;
return width_to_distribute / ((kMaxRecommendedApps - 1) * 2);
}
BEGIN_METADATA(RecentAppsView)
END_METADATA
} // namespace ash