// Copyright 2023 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/shelf/desk_button_widget.h"
#include "ash/focus_cycler.h"
#include "ash/public/cpp/shelf_prefs.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/scrollable_shelf_view.h"
#include "ash/shelf/shelf_focus_cycler.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shelf/shelf_navigation_widget.h"
#include "ash/shell.h"
#include "ash/wm/desks/desk_button/desk_button_container.h"
#include "ash/wm/desks/desks_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "base/i18n/rtl.h"
#include "base/ranges/algorithm.h"
#include "ui/aura/window_targeter.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/metadata/view_factory_internal.h"
#include "ui/views/view.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace {
gfx::Point GetScreenLocationForEvent(aura::Window* root,
const ui::LocatedEvent& event) {
gfx::Point screen_location;
if (event.target()) {
screen_location = event.target()->GetScreenLocation(event);
} else {
screen_location = event.root_location();
wm::ConvertPointToScreen(root, &screen_location);
}
return screen_location;
}
} // namespace
namespace ash {
// Customized window targeter that lets events fall through to the shelf if they
// do not intersect with desk button UIs.
class DeskButtonWindowTargeter : public aura::WindowTargeter {
public:
explicit DeskButtonWindowTargeter(DeskButtonWidget* desk_button_widget)
: desk_button_widget_(desk_button_widget) {}
DeskButtonWindowTargeter(const DeskButtonWindowTargeter&) = delete;
DeskButtonWindowTargeter& operator=(const DeskButtonWindowTargeter&) = delete;
// aura::WindowTargeter:
bool SubtreeShouldBeExploredForEvent(aura::Window* window,
const ui::LocatedEvent& event) override {
// Convert to screen coordinate. Do not process the event if it's not on the
// delegate view.
const gfx::Point screen_location =
GetScreenLocationForEvent(window->GetRootWindow(), event);
const gfx::Rect screen_bounds =
desk_button_widget_->delegate_view()->GetBoundsInScreen();
if (!screen_bounds.Contains(screen_location)) {
return false;
}
// Process the event if it intersects with desk button UI, otherwise let the
// event fall through to the shelf.
return desk_button_widget_->GetDeskButtonContainer()
->IntersectsWithDeskButtonUi(screen_location);
}
private:
const raw_ptr<DeskButtonWidget> desk_button_widget_;
};
DeskButtonWidget::DelegateView::DelegateView() = default;
DeskButtonWidget::DelegateView::~DelegateView() = default;
void DeskButtonWidget::DelegateView::Init(
DeskButtonWidget* desk_button_widget) {
CHECK(desk_button_widget);
desk_button_widget_ = desk_button_widget;
SetPaintToLayer(ui::LAYER_NOT_DRAWN);
GetContentsView()->AddChildView(views::Builder<DeskButtonContainer>()
.CopyAddressTo(&desk_button_container_)
.Init(desk_button_widget_)
.Build());
AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
}
bool DeskButtonWidget::DelegateView::CanActivate() const {
// We don't want mouse clicks to activate us, but we need to allow
// activation when the user is using the keyboard (FocusCycler).
return Shell::Get()->focus_cycler()->widget_activating() == GetWidget();
}
void DeskButtonWidget::DelegateView::Layout(PassKey) {
if (!desk_button_widget_ || !desk_button_container_) {
return;
}
// Update the desk button container.
desk_button_container_->set_zero_state(
!desk_button_widget_->IsHorizontalShelf());
desk_button_container_->UpdateUi(DesksController::Get()->active_desk());
// Calculate bounds of the desk button container.
const gfx::Size widget_size =
desk_button_widget_->GetWindowBoundsInScreen().size();
const gfx::Size container_size = desk_button_container_->GetPreferredSize();
gfx::Point container_origin;
if (desk_button_widget_->IsHorizontalShelf()) {
container_origin = gfx::Point(
widget_size.width() - kDeskButtonWidgetInsetsHorizontal.right() -
container_size.width(),
kDeskButtonWidgetInsetsHorizontal.top());
} else {
container_origin = gfx::Point(kDeskButtonWidgetInsetsVertical.left(),
widget_size.height() -
kDeskButtonWidgetInsetsVertical.bottom() -
container_size.height());
}
desk_button_container_->SetBoundsRect({container_origin, container_size});
}
bool DeskButtonWidget::DelegateView::AcceleratorPressed(
const ui::Accelerator& accelerator) {
CHECK_EQ(accelerator.key_code(), ui::VKEY_ESCAPE);
GetWidget()->Deactivate();
return true;
}
DeskButtonWidget::DeskButtonWidget(Shelf* shelf) : shelf_(shelf) {
CHECK(shelf_);
}
DeskButtonWidget::~DeskButtonWidget() = default;
// static
int DeskButtonWidget::GetMaxLength(bool horizontal_shelf) {
const int container_len =
DeskButtonContainer::GetMaxLength(!horizontal_shelf);
return container_len + (horizontal_shelf
? kDeskButtonWidgetInsetsHorizontal.width()
: kDeskButtonWidgetInsetsVertical.height());
}
bool DeskButtonWidget::ShouldReserveSpaceFromShelf() const {
const ShelfLayoutManager* layout_manager = shelf_->shelf_layout_manager();
Shell* shell = Shell::Get();
PrefService* prefs =
shell->session_controller()->GetLastActiveUserPrefService();
return layout_manager->is_active_session_state() &&
!shell->IsInTabletMode() && prefs && GetDeskButtonVisibility(prefs);
}
bool DeskButtonWidget::ShouldBeVisible() const {
const OverviewController* overview_controller =
Shell::Get()->overview_controller();
return ShouldReserveSpaceFromShelf() &&
!overview_controller->InOverviewSession();
}
void DeskButtonWidget::PrepareForAlignmentChange() {
delegate_view_->desk_button_container()->PrepareForAlignmentChange();
}
void DeskButtonWidget::CalculateTargetBounds() {
if (!ShouldBeVisible()) {
target_bounds_ = gfx::Rect();
return;
}
gfx::Point widget_origin;
gfx::Size widget_size;
// The position of this widget is always dependant on the hotseat widget.
const gfx::Rect hotseat_bounds = shelf_->hotseat_widget()->GetTargetBounds();
const gfx::Insets shelf_padding =
shelf_->hotseat_widget()
->scrollable_shelf_view()
->CalculateMirroredEdgePadding(/*use_target_bounds=*/true);
const int app_icon_end_padding = ShelfConfig::Get()->GetAppIconEndPadding();
const int max_length = GetMaxLength(IsHorizontalShelf());
if (IsHorizontalShelf()) {
widget_size = gfx::Size(max_length, hotseat_bounds.height());
widget_origin = gfx::Point(
base::i18n::IsRTL() ? hotseat_bounds.right() - shelf_padding.right() -
app_icon_end_padding
: hotseat_bounds.x() + shelf_padding.left() +
app_icon_end_padding - widget_size.width(),
hotseat_bounds.y());
} else {
widget_size = gfx::Size(hotseat_bounds.width(), max_length);
widget_origin = gfx::Point(hotseat_bounds.x(),
hotseat_bounds.y() + shelf_padding.top() +
app_icon_end_padding - widget_size.height());
}
target_bounds_ = gfx::Rect(widget_origin, widget_size);
}
gfx::Rect DeskButtonWidget::GetTargetBounds() const {
return target_bounds_;
}
void DeskButtonWidget::UpdateLayout(bool animate) {
const gfx::Rect initial_bounds = GetWindowBoundsInScreen();
const bool visibility = GetVisible();
const bool target_visibility = ShouldBeVisible();
if (initial_bounds == target_bounds_ && visibility == target_visibility) {
return;
}
if (!animate || visibility != target_visibility || initial_bounds.IsEmpty() ||
target_bounds_.IsEmpty()) {
if (target_visibility && !target_bounds_.IsEmpty()) {
SetBounds(target_bounds_);
ShowInactive();
} else {
Hide();
}
return;
}
// We only animate x axis movement for bottom shelf and y axis movement for
// side shelf when the widget size remains the same and non empty.
const bool animate_transform =
initial_bounds.size() == target_bounds_.size() &&
!target_bounds_.IsEmpty() &&
((IsHorizontalShelf() && initial_bounds.y() == target_bounds_.y()) ||
(!IsHorizontalShelf() && initial_bounds.x() == target_bounds_.x()));
if (animate_transform) {
const gfx::Transform initial_transform = gfx::TransformBetweenRects(
gfx::RectF(target_bounds_), gfx::RectF(initial_bounds));
SetBounds(target_bounds_);
GetNativeView()->layer()->SetTransform(initial_transform);
}
ui::ScopedLayerAnimationSettings animation_setter(
GetNativeView()->layer()->GetAnimator());
animation_setter.SetTransitionDuration(
ShelfConfig::Get()->shelf_animation_duration());
animation_setter.SetTweenType(gfx::Tween::EASE_OUT);
animation_setter.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
if (animate_transform) {
GetNativeView()->layer()->SetTransform(gfx::Transform());
} else {
SetBounds(target_bounds_);
}
}
void DeskButtonWidget::UpdateTargetBoundsForGesture(int shelf_position) {
if (IsHorizontalShelf()) {
target_bounds_.set_y(shelf_position);
} else {
target_bounds_.set_x(shelf_position);
}
}
void DeskButtonWidget::HandleLocaleChange() {
delegate_view_->desk_button_container()->HandleLocaleChange();
}
void DeskButtonWidget::Initialize(aura::Window* container) {
CHECK(container);
delegate_view_ = new DelegateView();
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.name = "DeskButtonWidget";
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.delegate = delegate_view_;
params.parent = container;
params.layer_type = ui::LAYER_NOT_DRAWN;
Init(std::move(params));
set_focus_on_creation(false);
delegate_view_->SetEnableArrowKeyTraversal(true);
delegate_view_->Init(this);
CalculateTargetBounds();
UpdateLayout(/*animate=*/false);
GetNativeWindow()->SetEventTargeter(
std::make_unique<DeskButtonWindowTargeter>(/*desk_button_widget=*/this));
}
DeskButtonContainer* DeskButtonWidget::GetDeskButtonContainer() const {
return delegate_view_->desk_button_container();
}
bool DeskButtonWidget::IsHorizontalShelf() const {
return shelf_->IsHorizontalAlignment();
}
void DeskButtonWidget::SetDefaultChildToFocus(
views::View* default_child_to_focus) {
CHECK(!default_child_to_focus || (default_child_to_focus->GetVisible() &&
default_child_to_focus->GetEnabled()));
default_child_to_focus_ = default_child_to_focus;
}
void DeskButtonWidget::StoreDeskButtonFocus() {
stored_focused_view_ = ShouldBeVisible() && IsActive()
? GetFocusManager()->GetFocusedView()
: nullptr;
CHECK(!stored_focused_view_ || (stored_focused_view_->GetVisible() &&
stored_focused_view_->GetEnabled()));
}
void DeskButtonWidget::RestoreDeskButtonFocus() {
if (ShouldBeVisible() && stored_focused_view_) {
default_child_to_focus_ = stored_focused_view_;
stored_focused_view_ = nullptr;
Shell::Get()->focus_cycler()->FocusWidget(this);
}
}
void DeskButtonWidget::MaybeFocusOut(bool reverse) {
// Only focus visible and enabled views.
std::vector<views::View*> views;
for (auto view : GetDeskButtonContainer()->children()) {
if (view->GetVisible() && view->GetEnabled()) {
views.emplace_back(view);
}
}
// The desk button will still be drawn in LTR, with the previous desk button
// on the left, when in RTL mode.
if (base::i18n::IsRTL()) {
base::ranges::reverse(views);
}
views::View* focused_view = GetFocusManager()->GetFocusedView();
const int count = views.size();
int focused = base::ranges::find(views, focused_view) - std::begin(views);
if (focused == count) {
GetFocusManager()
->GetNextFocusableView(nullptr, nullptr, !reverse, false)
->RequestFocus();
return;
}
int next = focused + (reverse ? -1 : 1);
if (next < 0 || next >= count) {
shelf_->shelf_focus_cycler()->FocusOut(reverse, SourceView::kDeskButton);
return;
}
views[next]->RequestFocus();
}
bool DeskButtonWidget::OnNativeWidgetActivationChanged(bool active) {
if (!Widget::OnNativeWidgetActivationChanged(active)) {
return false;
}
if (active && default_child_to_focus_) {
default_child_to_focus_->RequestFocus();
default_child_to_focus_ = nullptr;
}
return true;
}
} // namespace ash