// Copyright 2012 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/model/app_list_model.h"
#include <string>
#include <utility>
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_model_observer.h"
#include "ash/public/cpp/app_list/app_list_model_delegate.h"
#include "base/logging.h"
namespace ash {
AppListModel::AppListModel(AppListModelDelegate* app_list_model_delegate)
: delegate_(app_list_model_delegate),
top_level_item_list_(
std::make_unique<AppListItemList>(app_list_model_delegate)) {
item_list_scoped_observations_.AddObservation(top_level_item_list_.get());
}
AppListModel::~AppListModel() {
item_list_scoped_observations_.RemoveAllObservations();
}
void AppListModel::AddObserver(AppListModelObserver* observer) {
observers_.AddObserver(observer);
}
void AppListModel::RemoveObserver(AppListModelObserver* observer) {
observers_.RemoveObserver(observer);
}
void AppListModel::SetStatus(AppListModelStatus status) {
if (status_ == status)
return;
status_ = status;
for (auto& observer : observers_)
observer.OnAppListModelStatusChanged();
}
AppListItem* AppListModel::FindItem(const std::string& id) {
AppListItem* item = top_level_item_list_->FindItem(id);
if (item)
return item;
for (size_t i = 0; i < top_level_item_list_->item_count(); ++i) {
AppListItem* child_item =
top_level_item_list_->item_at(i)->FindChildItem(id);
if (child_item)
return child_item;
}
return nullptr;
}
AppListFolderItem* AppListModel::FindFolderItem(const std::string& id) {
AppListItem* item = top_level_item_list_->FindItem(id);
if (item && item->GetItemType() == AppListFolderItem::kItemType)
return static_cast<AppListFolderItem*>(item);
DCHECK(!item);
return nullptr;
}
AppListFolderItem* AppListModel::CreateFolderItem(
const std::string& folder_id) {
DCHECK(!top_level_item_list()->FindItem(folder_id));
std::unique_ptr<AppListFolderItem> new_folder =
std::make_unique<AppListFolderItem>(folder_id, delegate_);
new_folder->set_position(
top_level_item_list_->CreatePositionBefore(syncer::StringOrdinal()));
AppListItem* new_folder_item = AddItemToRootListAndNotify(
std::move(new_folder), ReparentItemReason::kAdd);
return static_cast<AppListFolderItem*>(new_folder_item);
}
AppListItem* AppListModel::AddItem(std::unique_ptr<AppListItem> item) {
DCHECK(!item->IsInFolder());
DCHECK(!top_level_item_list()->FindItem(item->id()));
return AddItemToRootListAndNotify(std::move(item), ReparentItemReason::kAdd);
}
void AppListModel::SetItemMetadata(const std::string& id,
std::unique_ptr<AppListItemMetadata> data) {
AppListItem* item = FindItem(id);
if (!item)
return;
// TODO(https://crbug.com/1252433): refactor this function because the current
// implementation is bug prone.
// data may not contain valid position or icon. Preserve it in this case.
if (!data->position.IsValid())
data->position = item->position();
// Update the item's position and name based on the metadata.
if (!data->position.Equals(item->position()))
SetItemPosition(item, data->position);
if (data->name != item->name()) {
SetItemName(item, data->name);
}
if (data->accessible_name != item->accessible_name()) {
SetItemAccessibleName(item, data->accessible_name);
}
if (data->progress > item->progress() ||
data->app_status != item->app_status()) {
item->SetProgress(data->progress);
item->SetAppStatus(data->app_status);
DVLOG(2) << "AppListModel::SetProgress: " << item->ToDebugString();
for (auto& observer : observers_) {
observer.OnAppListItemUpdated(item);
}
}
if (data->icon.isNull()) {
// Folder icons are generated on ash side so the icon of the metadata passed
// from chrome side is null. Do not alter `item` default icon in this case.
data->icon = item->GetDefaultIcon();
data->icon_color = item->GetDefaultIconColor();
}
if (data->folder_id != item->folder_id())
MoveItemToFolder(item, data->folder_id);
item->SetMetadata(std::move(data));
}
AppListItem* AppListModel::AddItemToFolder(std::unique_ptr<AppListItem> item,
const std::string& folder_id) {
if (folder_id.empty())
return AddItem(std::move(item));
DVLOG(2) << "AddItemToFolder: " << item->id() << ": " << folder_id;
CHECK_NE(folder_id, item->folder_id());
DCHECK_NE(AppListFolderItem::kItemType, item->GetItemType());
AppListFolderItem* dest_folder = FindFolderItem(folder_id);
if (!dest_folder)
dest_folder = CreateFolderItem(folder_id);
DCHECK(!dest_folder->item_list()->FindItem(item->id()))
<< "Already in folder: " << dest_folder->id();
return AddItemToFolderListAndNotify(dest_folder, std::move(item),
ReparentItemReason::kAdd);
}
const std::string AppListModel::MergeItems(const std::string& target_item_id,
const std::string& source_item_id) {
DVLOG(2) << "MergeItems: " << source_item_id << " -> " << target_item_id;
if (target_item_id == source_item_id) {
LOG(WARNING) << "MergeItems tried to drop item onto itself ("
<< source_item_id << " -> " << target_item_id << ").";
return "";
}
// Find the target item.
AppListItem* target_item = top_level_item_list_->FindItem(target_item_id);
if (!target_item) {
LOG(ERROR) << "MergeItems: Target no longer exists.";
return "";
}
AppListItem* source_item = FindItem(source_item_id);
if (!source_item) {
LOG(ERROR) << "MergeItems: Source no longer exists.";
return "";
}
// If the target item is a folder, just add the source item to it.
if (target_item->GetItemType() == AppListFolderItem::kItemType) {
AppListFolderItem* target_folder =
static_cast<AppListFolderItem*>(target_item);
if (target_folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM) {
LOG(WARNING) << "MergeItems called with OEM folder as target";
return "";
}
delegate_->RequestMoveItemToFolder(source_item_id, target_item_id);
return target_folder->id();
}
return delegate_->RequestFolderCreation(target_item_id, source_item_id);
}
void AppListModel::MoveItemToFolder(AppListItem* item,
const std::string& folder_id) {
DVLOG(2) << "MoveItemToFolder: " << folder_id << " <- "
<< item->ToDebugString();
if (item->folder_id() == folder_id)
return;
if (!item->IsInFolder()) {
AppListFolderItem* dest_folder = FindFolderItem(folder_id);
if (!dest_folder)
dest_folder = CreateFolderItem(folder_id);
// Handle the case that `item` is a top list item.
std::unique_ptr<AppListItem> item_ptr = RemoveFromTopList(item);
AddItemToFolderListAndNotify(dest_folder, std::move(item_ptr),
ReparentItemReason::kUpdate);
return;
}
ReparentOrDeleteItemInFolder(item, folder_id);
}
bool AppListModel::MoveItemToRootAt(AppListItem* item,
syncer::StringOrdinal position) {
DVLOG(2) << "MoveItemToRootAt: "
<< "[" << position.ToDebugString() << "]"
<< " <- " << item->ToDebugString();
if (item->folder_id().empty())
return false;
AppListFolderItem* src_folder = FindFolderItem(item->folder_id());
if (src_folder &&
src_folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM) {
LOG(WARNING) << "MoveItemToFolderAt called with OEM folder as source";
return false;
}
delegate_->RequestMoveItemToRoot(
item->id(), top_level_item_list_->CreatePositionBefore(position));
return true;
}
void AppListModel::SetItemPosition(AppListItem* item,
const syncer::StringOrdinal& new_position) {
if (!item->IsInFolder()) {
SetRootItemPosition(item, new_position);
return;
}
// The code below handles the case that `item` has a parent folder.
AppListFolderItem* folder = FindFolderItem(item->folder_id());
DCHECK(folder);
folder->item_list()->SetItemPosition(item, new_position);
for (auto& observer : observers_)
observer.OnAppListItemUpdated(item);
}
void AppListModel::SetItemName(AppListItem* item, const std::string& name) {
item->SetName(name);
DVLOG(2) << "AppListModel::SetItemName: " << item->ToDebugString();
for (auto& observer : observers_)
observer.OnAppListItemUpdated(item);
}
void AppListModel::SetItemAccessibleName(AppListItem* item,
const std::string& name) {
item->SetAccessibleName(name);
for (auto& observer : observers_) {
observer.OnAppListItemUpdated(item);
}
}
void AppListModel::DeleteItem(const std::string& id) {
AppListItem* item = FindItem(id);
if (!item)
return;
const std::string copied_folder_id = item->folder_id();
if (!item->IsInFolder()) {
DCHECK_EQ(0u, item->ChildItemCount())
<< "Invalid call to DeleteItem for item with children: " << id;
for (auto& observer : observers_)
observer.OnAppListItemWillBeDeleted(item);
if (item->GetItemType() == AppListFolderItem::kItemType) {
item_list_scoped_observations_.RemoveObservation(
static_cast<AppListFolderItem*>(item)->item_list());
}
top_level_item_list_->DeleteItem(id);
return;
}
// Destroy `item`.
ReparentOrDeleteItemInFolder(item,
/*destination_folder_id=*/std::nullopt);
}
// Private methods
void AppListModel::OnListItemMoved(size_t from_index,
size_t to_index,
AppListItem* item) {
for (auto& observer : observers_)
observer.OnAppListItemUpdated(item);
}
AppListItem* AppListModel::AddItemToRootListAndNotify(
std::unique_ptr<AppListItem> item_ptr,
ReparentItemReason reason) {
DCHECK(!item_ptr->IsInFolder());
if (reason == ReparentItemReason::kAdd &&
item_ptr->GetItemType() == AppListFolderItem::kItemType) {
item_list_scoped_observations_.AddObservation(
static_cast<AppListFolderItem*>(item_ptr.get())->item_list());
}
AppListItem* item = top_level_item_list_->AddItem(std::move(item_ptr));
NotifyItemParentChange(item, reason);
return item;
}
AppListItem* AppListModel::AddItemToFolderListAndNotify(
AppListFolderItem* folder,
std::unique_ptr<AppListItem> item_ptr,
ReparentItemReason reason) {
CHECK_NE(folder->id(), item_ptr->folder_id());
// Calling `AppListItemList::AddItem()` could trigger
// `AppListModel::SetItemMetadata()` so set the folder id before addition.
item_ptr->set_folder_id(folder->id());
AppListItem* item = folder->item_list()->AddItem(std::move(item_ptr));
NotifyItemParentChange(item, reason);
return item;
}
void AppListModel::NotifyItemParentChange(AppListItem* item,
ReparentItemReason reason) {
for (auto& observer : observers_) {
switch (reason) {
case ReparentItemReason::kAdd:
observer.OnAppListItemAdded(item);
break;
case ReparentItemReason::kUpdate:
observer.OnAppListItemUpdated(item);
break;
}
}
}
std::unique_ptr<AppListItem> AppListModel::RemoveFromTopList(
AppListItem* item) {
DCHECK(!item->IsInFolder());
if (item->GetItemType() == AppListFolderItem::kItemType) {
item_list_scoped_observations_.RemoveObservation(
static_cast<AppListFolderItem*>(item)->item_list());
}
return top_level_item_list_->RemoveItem(item->id());
}
void AppListModel::ReparentOrDeleteItemInFolder(
AppListItem* item,
std::optional<std::string> destination_folder_id) {
AppListFolderItem* folder = FindFolderItem(item->folder_id());
DCHECK(folder) << "Folder not found for item: " << item->ToDebugString();
const std::string item_parent_id = item->folder_id();
std::unique_ptr<AppListItem> removed_item =
RemoveItemFromFolder(folder, item);
if (destination_folder_id.has_value()) {
// When an item is removed from a folder, it can be moved to the top
// list or a folder.
if (destination_folder_id->empty()) {
AddItemToRootListAndNotify(std::move(removed_item),
ReparentItemReason::kUpdate);
} else {
// Create a folder if the destination folder doesn't exist.
AppListFolderItem* destination_folder =
FindFolderItem(*destination_folder_id);
if (!destination_folder)
destination_folder = CreateFolderItem(*destination_folder_id);
AddItemToFolderListAndNotify(destination_folder, std::move(removed_item),
ReparentItemReason::kUpdate);
}
} else {
// Destroy `removed_item` and notify observers.
for (auto& observer : observers_)
observer.OnAppListItemWillBeDeleted(item);
removed_item.reset(); // Deletes item.
}
// Delete the folder if the folder becomes empty after child removal.
DeleteFolderIfEmpty(item_parent_id);
}
std::unique_ptr<AppListItem> AppListModel::RemoveItemFromFolder(
AppListFolderItem* folder,
AppListItem* item) {
CHECK_EQ(item->folder_id(), folder->id());
std::unique_ptr<AppListItem> removed_item =
folder->item_list()->RemoveItem(item->id());
removed_item->set_folder_id("");
return removed_item;
}
void AppListModel::DeleteFolderIfEmpty(const std::string& folder_id) {
const AppListFolderItem* folder = FindFolderItem(folder_id);
if (!folder || folder->item_list()->item_count())
return;
DVLOG(2) << "Deleting empty folder: " << folder->ToDebugString();
std::string copy_id = folder->id();
DeleteItem(copy_id);
}
void AppListModel::SetRootItemPosition(
AppListItem* item,
const syncer::StringOrdinal& new_position) {
DCHECK(!item->IsInFolder());
DCHECK(FindItem(item->id()));
const bool index_change =
top_level_item_list_->SetItemPosition(item, new_position);
// If `index_change` is true, `OnListItemMoved()` is called and model
// observers are signaled. Nothing to do so return early.
if (index_change)
return;
for (auto& observer : observers_)
observer.OnAppListItemUpdated(item);
}
} // namespace ash