[go: nahoru, domu]

blob: 1fbd1ebc84bdc0ddbd9b82fc1e96dd6869196948 [file] [log] [blame]
// Copyright 2020 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/system/holding_space/holding_space_tray.h"
#include <memory>
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/holding_space/holding_space_client.h"
#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/holding_space/holding_space_metrics.h"
#include "ash/public/cpp/holding_space/holding_space_prefs.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/holding_space/holding_space_tray_bubble.h"
#include "ash/system/holding_space/holding_space_tray_icon.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "base/containers/adapters.h"
#include "components/prefs/pref_change_registrar.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/metadata/metadata_impl_macros.h"
#include "ui/views/vector_icons.h"
namespace ash {
namespace {
using ::ui::mojom::DragOperation;
// Animation.
constexpr base::TimeDelta kAnimationDuration =
base::TimeDelta::FromMilliseconds(167);
// Helpers ---------------------------------------------------------------------
// Animates the specified `view` to the given `target_opacity`.
void AnimateToTargetOpacity(views::View* view, float target_opacity) {
DCHECK(view->layer());
if (view->layer()->GetTargetOpacity() == target_opacity)
return;
ui::ScopedLayerAnimationSettings settings(view->layer()->GetAnimator());
settings.SetPreemptionStrategy(
ui::LayerAnimator::PreemptionStrategy::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings.SetTransitionDuration(kAnimationDuration);
view->layer()->SetOpacity(target_opacity);
}
// Returns the file paths extracted from the specified `data` which are *not*
// pinned to the attached holding space model.
std::vector<base::FilePath> ExtractUnpinnedFilePaths(
const ui::OSExchangeData& data) {
if (!data.HasFile())
return {};
std::vector<ui::FileInfo> filenames;
if (!data.GetFilenames(&filenames))
return {};
HoldingSpaceModel* const model = HoldingSpaceController::Get()->model();
std::vector<base::FilePath> unpinned_file_paths;
for (const ui::FileInfo& filename : filenames) {
const base::FilePath& file_path = filename.path;
if (!model->ContainsItem(HoldingSpaceItem::Type::kPinnedFile, file_path))
unpinned_file_paths.push_back(file_path);
}
return unpinned_file_paths;
}
// Returns whether previews are enabled.
bool IsPreviewsEnabled() {
auto* prefs = Shell::Get()->session_controller()->GetActivePrefService();
return features::IsTemporaryHoldingSpacePreviewsEnabled() && prefs &&
holding_space_prefs::IsPreviewsEnabled(prefs);
}
// Returns whether the holding space model contains any finalized items.
bool ModelContainsFinalizedItems(HoldingSpaceModel* model) {
for (const auto& item : model->items()) {
if (item->IsFinalized())
return true;
}
return false;
}
// Creates the default tray icon.
std::unique_ptr<views::ImageView> CreateDefaultTrayIcon() {
auto icon = std::make_unique<views::ImageView>();
icon->SetID(kHoldingSpaceTrayDefaultIconId);
icon->SetImage(gfx::CreateVectorIcon(
kHoldingSpaceIcon, kHoldingSpaceTrayIconSize,
AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary)));
icon->SetPreferredSize(gfx::Size(kTrayItemSize, kTrayItemSize));
icon->SetPaintToLayer();
icon->layer()->SetFillsBoundsOpaquely(false);
return icon;
}
// Creates the overlay to be drawn over all other child views to indicate that
// the parent view is a drop target and is capable of handling the current drag
// payload.
std::unique_ptr<views::View> CreateDropTargetOverlay() {
auto drop_target_overlay = std::make_unique<views::View>();
drop_target_overlay->SetID(kHoldingSpaceTrayDropTargetOverlayId);
drop_target_overlay->SetLayoutManager(std::make_unique<views::FillLayout>());
// Layer.
drop_target_overlay->SetPaintToLayer();
drop_target_overlay->layer()->SetFillsBoundsOpaquely(false);
// Icon.
auto* icon =
drop_target_overlay->AddChildView(std::make_unique<views::ImageView>());
icon->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);
icon->SetVerticalAlignment(views::ImageView::Alignment::kCenter);
icon->SetImage(gfx::CreateVectorIcon(
views::kUnpinIcon, kHoldingSpaceIconSize,
AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary)));
icon->SetPaintToLayer();
icon->layer()->SetFillsBoundsOpaquely(false);
return drop_target_overlay;
}
} // namespace
// HoldingSpaceTray ------------------------------------------------------------
HoldingSpaceTray::HoldingSpaceTray(Shelf* shelf) : TrayBackgroundView(shelf) {
controller_observer_.Observe(HoldingSpaceController::Get());
session_observer_.Observe(Shell::Get()->session_controller());
SetVisible(false);
// Icon.
default_tray_icon_ = tray_container()->AddChildView(CreateDefaultTrayIcon());
if (features::IsTemporaryHoldingSpacePreviewsEnabled()) {
previews_tray_icon_ = tray_container()->AddChildView(
std::make_unique<HoldingSpaceTrayIcon>(shelf));
previews_tray_icon_->SetVisible(false);
// Enable context menu, which supports an action to toggle item previews.
set_context_menu_controller(this);
}
// Drop target overlay.
// NOTE: The `drop_target_overlay_` will only be visible when:
// * a drag is in progress,
// * the drag payload contains pinnable files, and
// * the cursor is sufficiently close to this view.
drop_target_overlay_ = AddChildView(CreateDropTargetOverlay());
drop_target_overlay_->layer()->SetOpacity(0.f);
// Animation.
set_use_bounce_in_animation(true);
}
HoldingSpaceTray::~HoldingSpaceTray() = default;
void HoldingSpaceTray::Initialize() {
TrayBackgroundView::Initialize();
if (features::IsTemporaryHoldingSpacePreviewsEnabled()) {
DCHECK(previews_tray_icon_);
UpdatePreviewsVisibility();
// If previews feature is enabled, the preview icon is displayed
// conditionally, depending on user prefs state.
auto* prefs = Shell::Get()->session_controller()->GetActivePrefService();
if (prefs)
ObservePrefService(prefs);
}
// It's possible that this holding space tray was created after login, such as
// would occur if the user connects an external display. In such situations
// the holding space model will already have been attached.
if (HoldingSpaceController::Get()->model())
OnHoldingSpaceModelAttached(HoldingSpaceController::Get()->model());
}
void HoldingSpaceTray::ClickedOutsideBubble() {
CloseBubble();
}
std::u16string HoldingSpaceTray::GetAccessibleNameForTray() {
return l10n_util::GetStringUTF16(IDS_ASH_HOLDING_SPACE_A11Y_NAME);
}
views::View* HoldingSpaceTray::GetTooltipHandlerForPoint(
const gfx::Point& point) {
// Tooltip events should be handled top level, not by descendents.
return HitTestPoint(point) ? this : nullptr;
}
std::u16string HoldingSpaceTray::GetTooltipText(const gfx::Point& point) const {
return l10n_util::GetStringUTF16(IDS_ASH_HOLDING_SPACE_TITLE);
}
void HoldingSpaceTray::HandleLocaleChange() {
TooltipTextChanged();
}
void HoldingSpaceTray::HideBubbleWithView(const TrayBubbleView* bubble_view) {}
void HoldingSpaceTray::AnchorUpdated() {
if (bubble_)
bubble_->AnchorUpdated();
}
void HoldingSpaceTray::UpdateAfterLoginStatusChange() {
UpdateVisibility();
}
void HoldingSpaceTray::CloseBubble() {
if (!bubble_)
return;
holding_space_metrics::RecordPodAction(
holding_space_metrics::PodAction::kCloseBubble);
widget_observer_.Reset();
bubble_.reset();
SetIsActive(false);
}
void HoldingSpaceTray::ShowBubble() {
if (bubble_)
return;
holding_space_metrics::RecordPodAction(
holding_space_metrics::PodAction::kShowBubble);
DCHECK(tray_container());
bubble_ = std::make_unique<HoldingSpaceTrayBubble>(this);
// Observe the bubble widget so that we can do proper clean up when it is
// being destroyed. If destruction is due to a call to `CloseBubble()` we will
// have already cleaned up state but there are cases where the bubble widget
// is destroyed independent of a call to `CloseBubble()`, e.g. ESC key press.
widget_observer_.Observe(bubble_->GetBubbleWidget());
SetIsActive(true);
}
TrayBubbleView* HoldingSpaceTray::GetBubbleView() {
return bubble_ ? bubble_->GetBubbleView() : nullptr;
}
views::Widget* HoldingSpaceTray::GetBubbleWidget() const {
return bubble_ ? bubble_->GetBubbleWidget() : nullptr;
}
void HoldingSpaceTray::SetVisiblePreferred(bool visible_preferred) {
if (this->visible_preferred() == visible_preferred)
return;
holding_space_metrics::RecordPodAction(
visible_preferred ? holding_space_metrics::PodAction::kShowPod
: holding_space_metrics::PodAction::kHidePod);
TrayBackgroundView::SetVisiblePreferred(visible_preferred);
if (!visible_preferred)
CloseBubble();
}
bool HoldingSpaceTray::GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) {
*formats = ui::OSExchangeData::FILE_NAME;
return true;
}
bool HoldingSpaceTray::AreDropTypesRequired() {
return true;
}
bool HoldingSpaceTray::CanDrop(const ui::OSExchangeData& data) {
return !ExtractUnpinnedFilePaths(data).empty();
}
// TODO(crbug.com/1171059): Instead of handling `OnDragEntered()`, show the
// `drop_target_overlay_` if the cursor is within range of this view.
void HoldingSpaceTray::OnDragEntered(const ui::DropTargetEvent& event) {
if (ExtractUnpinnedFilePaths(event.data()).empty())
UpdateDropTargetState(/*is_drop_target=*/false, &event);
else
UpdateDropTargetState(/*is_drop_target=*/true, &event);
}
int HoldingSpaceTray::OnDragUpdated(const ui::DropTargetEvent& event) {
if (ExtractUnpinnedFilePaths(event.data()).empty()) {
UpdateDropTargetState(/*is_drop_target=*/false, &event);
return ui::DragDropTypes::DRAG_NONE;
}
UpdateDropTargetState(/*is_drop_target=*/true, &event);
return ui::DragDropTypes::DRAG_COPY;
}
// TODO(crbug.com/1171059): Instead of handling `OnDragExited()`, hide the
// `drop_target_overlay_` if the cursor is outside range of this view.
void HoldingSpaceTray::OnDragExited() {
UpdateDropTargetState(/*is_drop_target=*/false, /*event=*/nullptr);
}
DragOperation HoldingSpaceTray::OnPerformDrop(
const ui::DropTargetEvent& event) {
UpdateDropTargetState(/*is_drop_target=*/false, /*event=*/nullptr);
std::vector<base::FilePath> unpinned_file_paths(
ExtractUnpinnedFilePaths(event.data()));
if (unpinned_file_paths.empty())
return DragOperation::kNone;
holding_space_metrics::RecordPodAction(
holding_space_metrics::PodAction::kDragAndDropToPin);
HoldingSpaceController::Get()->client()->PinFiles(unpinned_file_paths);
return DragOperation::kCopy;
}
void HoldingSpaceTray::Layout() {
TrayBackgroundView::Layout();
// The `drop_target_overlay_` should always fill this view's bounds as they
// are perceived by the user. Note that the user perceives the bounds of this
// view to be its background bounds, not its local bounds.
drop_target_overlay_->SetBoundsRect(GetBackgroundBounds());
}
void HoldingSpaceTray::FirePreviewsUpdateTimerIfRunningForTesting() {
if (previews_update_.IsRunning())
previews_update_.FireNow();
}
void HoldingSpaceTray::UpdateVisibility() {
HoldingSpaceModel* const model = HoldingSpaceController::Get()->model();
if (!model || Shell::Get()->session_controller()->IsUserSessionBlocked()) {
SetVisiblePreferred(false);
return;
}
PrefService* prefs =
Shell::Get()->session_controller()->GetActivePrefService();
const bool has_ever_added_item =
prefs ? holding_space_prefs::GetTimeOfFirstAdd(prefs).has_value() : false;
const bool has_ever_pinned_item =
prefs ? holding_space_prefs::GetTimeOfFirstPin(prefs).has_value() : false;
// The holding space tray should not be visible in the shelf until the user
// has added their first item to holding space. Once an item has been added,
// the holding space tray will continue to be visible until the user has
// pinned their first file. After the user has pinned their first file, the
// holding space tray will only be visible in the shelf if their holding space
// contains finalized items.
SetVisiblePreferred((has_ever_added_item && !has_ever_pinned_item) ||
ModelContainsFinalizedItems(model));
}
std::u16string HoldingSpaceTray::GetAccessibleNameForBubble() {
return GetAccessibleNameForTray();
}
bool HoldingSpaceTray::ShouldEnableExtraKeyboardAccessibility() {
return Shell::Get()->accessibility_controller()->spoken_feedback().enabled();
}
void HoldingSpaceTray::HideBubble(const TrayBubbleView* bubble_view) {
CloseBubble();
}
void HoldingSpaceTray::OnHoldingSpaceModelAttached(HoldingSpaceModel* model) {
model_observer_.Observe(model);
UpdateVisibility();
UpdatePreviewsState();
}
void HoldingSpaceTray::OnHoldingSpaceModelDetached(HoldingSpaceModel* model) {
model_observer_.Reset();
UpdateVisibility();
UpdatePreviewsState();
}
void HoldingSpaceTray::OnHoldingSpaceItemsAdded(
const std::vector<const HoldingSpaceItem*>& items) {
UpdateVisibility();
UpdatePreviewsState();
}
void HoldingSpaceTray::OnHoldingSpaceItemsRemoved(
const std::vector<const HoldingSpaceItem*>& items) {
UpdateVisibility();
UpdatePreviewsState();
}
void HoldingSpaceTray::OnHoldingSpaceItemFinalized(
const HoldingSpaceItem* item) {
UpdateVisibility();
UpdatePreviewsState();
}
void HoldingSpaceTray::ExecuteCommand(int command_id, int event_flags) {
DCHECK(features::IsTemporaryHoldingSpacePreviewsEnabled());
switch (static_cast<HoldingSpaceCommandId>(command_id)) {
case HoldingSpaceCommandId::kHidePreviews:
holding_space_metrics::RecordPodAction(
holding_space_metrics::PodAction::kHidePreviews);
holding_space_prefs::SetPreviewsEnabled(
Shell::Get()->session_controller()->GetActivePrefService(), false);
break;
case HoldingSpaceCommandId::kShowPreviews:
holding_space_metrics::RecordPodAction(
holding_space_metrics::PodAction::kShowPreviews);
holding_space_prefs::SetPreviewsEnabled(
Shell::Get()->session_controller()->GetActivePrefService(), true);
break;
default:
NOTREACHED();
break;
}
}
void HoldingSpaceTray::ShowContextMenuForViewImpl(
views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
DCHECK(features::IsTemporaryHoldingSpacePreviewsEnabled());
holding_space_metrics::RecordPodAction(
holding_space_metrics::PodAction::kShowContextMenu);
context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
const bool previews_enabled = holding_space_prefs::IsPreviewsEnabled(
Shell::Get()->session_controller()->GetActivePrefService());
if (previews_enabled) {
context_menu_model_->AddItemWithIcon(
static_cast<int>(HoldingSpaceCommandId::kHidePreviews),
l10n_util::GetStringUTF16(
IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_HIDE_PREVIEWS),
ui::ImageModel::FromVectorIcon(kVisibilityOffIcon, /*color_id=*/-1,
kHoldingSpaceIconSize));
} else {
context_menu_model_->AddItemWithIcon(
static_cast<int>(HoldingSpaceCommandId::kShowPreviews),
l10n_util::GetStringUTF16(
IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_SHOW_PREVIEWS),
ui::ImageModel::FromVectorIcon(kVisibilityIcon, /*color_id=*/-1,
kHoldingSpaceIconSize));
}
const int run_types = views::MenuRunner::USE_TOUCHABLE_LAYOUT |
views::MenuRunner::CONTEXT_MENU |
views::MenuRunner::FIXED_ANCHOR;
context_menu_runner_ =
std::make_unique<views::MenuRunner>(context_menu_model_.get(), run_types);
gfx::Rect anchor = source->GetBoundsInScreen();
anchor.Inset(gfx::Insets(-kHoldingSpaceContextMenuMargin, 0));
context_menu_runner_->RunMenuAt(
source->GetWidget(), /*button_controller=*/nullptr, anchor,
views::MenuAnchorPosition::kTopLeft, source_type);
}
void HoldingSpaceTray::OnWidgetDragWillStart(views::Widget* widget) {
// The holding space bubble should be closed while dragging holding space
// items so as not to obstruct drop targets. Post the task to close the bubble
// so that we don't attempt to destroy the bubble widget before the associated
// drag event has been fully initialized.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&HoldingSpaceTray::CloseBubble,
weak_factory_.GetWeakPtr()));
}
void HoldingSpaceTray::OnWidgetDestroying(views::Widget* widget) {
CloseBubble();
}
void HoldingSpaceTray::OnActiveUserPrefServiceChanged(PrefService* prefs) {
if (!features::IsTemporaryHoldingSpacePreviewsEnabled())
return;
UpdatePreviewsState();
ObservePrefService(prefs);
}
void HoldingSpaceTray::OnSessionStateChanged(
session_manager::SessionState state) {
UpdateVisibility();
}
void HoldingSpaceTray::ObservePrefService(PrefService* prefs) {
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(prefs);
// NOTE: The callback being bound is scoped to `pref_change_registrar_` which
// is owned by `this` so it is safe to bind with an unretained raw pointer.
holding_space_prefs::AddPreviewsEnabledChangedCallback(
pref_change_registrar_.get(),
base::BindRepeating(&HoldingSpaceTray::UpdatePreviewsState,
base::Unretained(this)));
}
void HoldingSpaceTray::UpdatePreviewsState() {
UpdatePreviewsVisibility();
SchedulePreviewsIconUpdate();
}
void HoldingSpaceTray::UpdatePreviewsVisibility() {
const bool show_previews =
IsPreviewsEnabled() && HoldingSpaceController::Get()->model() &&
ModelContainsFinalizedItems(HoldingSpaceController::Get()->model());
if (PreviewsShown() == show_previews)
return;
default_tray_icon_->SetVisible(!show_previews);
DCHECK(previews_tray_icon_);
previews_tray_icon_->SetVisible(show_previews);
if (!show_previews) {
previews_tray_icon_->Clear();
previews_update_.Stop();
}
}
void HoldingSpaceTray::SchedulePreviewsIconUpdate() {
if (previews_update_.IsRunning())
return;
// Schedule async task with a short (somewhat arbitrary) delay to update
// previews so items added in quick succession are handled together.
base::TimeDelta delay = use_zero_previews_update_delay_
? base::TimeDelta()
: base::TimeDelta::FromMilliseconds(50);
previews_update_.Start(FROM_HERE, delay,
base::BindOnce(&HoldingSpaceTray::UpdatePreviewsIcon,
base::Unretained(this)));
}
void HoldingSpaceTray::UpdatePreviewsIcon() {
if (!PreviewsShown()) {
if (previews_tray_icon_)
previews_tray_icon_->Clear();
return;
}
std::vector<const HoldingSpaceItem*> items_with_previews;
std::set<base::FilePath> paths_with_previews;
for (const auto& item :
base::Reversed(HoldingSpaceController::Get()->model()->items())) {
if (!item->IsFinalized())
continue;
if (base::Contains(paths_with_previews, item->file_path()))
continue;
items_with_previews.push_back(item.get());
paths_with_previews.insert(item->file_path());
}
previews_tray_icon_->UpdatePreviews(items_with_previews);
}
bool HoldingSpaceTray::PreviewsShown() const {
return previews_tray_icon_ && previews_tray_icon_->GetVisible();
}
// TODO(crbug.com/1171059): Animate translation of tray icons.
void HoldingSpaceTray::UpdateDropTargetState(bool is_drop_target,
const ui::LocatedEvent* event) {
AnimateToTargetOpacity(drop_target_overlay_, is_drop_target ? 1.f : 0.f);
AnimateToTargetOpacity(default_tray_icon_, is_drop_target ? 0.f : 1.f);
AnimateToTargetOpacity(previews_tray_icon_, is_drop_target ? 0.f : 1.f);
const views::InkDropState target_ink_drop_state =
is_drop_target ? views::InkDropState::ACTION_PENDING
: views::InkDropState::HIDDEN;
if (GetInkDrop()->GetTargetInkDropState() == target_ink_drop_state)
return;
// Even though `event` is a `ui::LocatedEvent`, it may *not* return `true` for
// `IsLocatedEvent()` which is checked downstream when animating the ink drop.
// Since the only data that needs to propagate is `event` location, create a
// synthetic event that *will* return `true` for `IsLocatedEvent()` if needed.
std::unique_ptr<ui::MouseEvent> mouse_moved_event;
if (event && !event->IsLocatedEvent()) {
mouse_moved_event = std::make_unique<ui::MouseEvent>(
ui::ET_MOUSE_MOVED, event->location_f(), event->root_location_f(),
event->time_stamp(), /*flags=*/ui::EF_NONE,
/*changed_button_flags=*/ui::EF_NONE);
event = mouse_moved_event.get();
}
AnimateInkDrop(target_ink_drop_state, event);
}
BEGIN_METADATA(HoldingSpaceTray, TrayBackgroundView)
END_METADATA
} // namespace ash