[go: nahoru, domu]

blob: ab4c80eba506d7d8091787742911445b8decdd4a [file] [log] [blame]
// Copyright 2019 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/wm/desks/desk_mini_view.h"
#include <algorithm>
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/desks/close_desk_button.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desk_name_view.h"
#include "ash/wm/desks/desk_preview_view.h"
#include "ash/wm/desks/desks_bar_view.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_restore_util.h"
#include "base/strings/string_util.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/canvas.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
constexpr int kLabelPreviewSpacing = 8;
constexpr int kCloseButtonMargin = 8;
constexpr int kMinDeskNameViewWidth = 56;
} // namespace
// -----------------------------------------------------------------------------
// DeskMiniView
// static
int DeskMiniView::GetPreviewWidth(const gfx::Size& root_window_size,
int preview_height) {
return preview_height * root_window_size.width() / root_window_size.height();
}
// static
gfx::Rect DeskMiniView::GetDeskPreviewBounds(aura::Window* root_window,
bool compact) {
const int preview_height = DeskPreviewView::GetHeight(root_window, compact);
const auto root_size = root_window->bounds().size();
return gfx::Rect(GetPreviewWidth(root_size, preview_height), preview_height);
}
DeskMiniView::DeskMiniView(DesksBarView* owner_bar,
aura::Window* root_window,
Desk* desk)
: owner_bar_(owner_bar), root_window_(root_window), desk_(desk) {
DCHECK(root_window_);
DCHECK(root_window_->IsRootWindow());
desk_->AddObserver(this);
auto desk_name_view = std::make_unique<DeskNameView>(this);
desk_name_view->AddObserver(this);
desk_name_view->set_controller(this);
desk_name_view->SetText(desk_->name());
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
// TODO(afakhry): Tooltips.
desk_preview_ = AddChildView(std::make_unique<DeskPreviewView>(
base::BindRepeating(&DeskMiniView::OnDeskPreviewPressed,
base::Unretained(this)),
this));
desk_name_view_ = AddChildView(std::move(desk_name_view));
close_desk_button_ =
AddChildView(std::make_unique<CloseDeskButton>(base::BindRepeating(
&DeskMiniView::OnCloseButtonPressed, base::Unretained(this))));
UpdateCloseButtonVisibility();
UpdateBorderColor();
}
DeskMiniView::~DeskMiniView() {
desk_name_view_->RemoveObserver(this);
// In tests, where animations are disabled, the mini_view maybe destroyed
// before the desk.
if (desk_)
desk_->RemoveObserver(this);
}
gfx::Rect DeskMiniView::GetPreviewBoundsInScreen() const {
DCHECK(desk_preview_);
return desk_preview_->GetBoundsInScreen();
}
aura::Window* DeskMiniView::GetDeskContainer() const {
DCHECK(desk_);
return desk_->GetDeskContainerForRoot(root_window_);
}
bool DeskMiniView::IsDeskNameBeingModified() const {
return desk_name_view_->HasFocus();
}
void DeskMiniView::UpdateCloseButtonVisibility() {
// Don't show the close button when hovered while the dragged window is on
// the DesksBarView.
// For switch access, setting the close button to visible allows users to
// navigate to it.
close_desk_button_->SetVisible(
DesksController::Get()->CanRemoveDesks() &&
!owner_bar_->dragged_item_over_bar() && !owner_bar_->IsDraggingDesk() &&
(IsMouseHovered() || force_show_close_button_ ||
Shell::Get()->accessibility_controller()->IsSwitchAccessRunning()));
}
void DeskMiniView::OnWidgetGestureTap(const gfx::Rect& screen_rect,
bool is_long_gesture) {
const bool old_force_show_close_button = force_show_close_button_;
// Note that we don't want to hide the close button if it's a single tap
// within the bounds of an already visible button, which will later be handled
// as a press event on that close button that will result in closing the desk.
force_show_close_button_ =
(is_long_gesture && IsPointOnMiniView(screen_rect.CenterPoint())) ||
(!is_long_gesture && close_desk_button_->GetVisible() &&
close_desk_button_->DoesIntersectScreenRect(screen_rect));
if (old_force_show_close_button != force_show_close_button_)
UpdateCloseButtonVisibility();
}
void DeskMiniView::UpdateBorderColor() {
DCHECK(desk_);
auto* color_provider = AshColorProvider::Get();
if ((owner_bar_->dragged_item_over_bar() &&
IsPointOnMiniView(owner_bar_->last_dragged_item_screen_location())) ||
IsViewHighlighted()) {
desk_preview_->SetBorderColor(color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor));
} else if (!desk_->is_active()) {
desk_preview_->SetBorderColor(SK_ColorTRANSPARENT);
} else {
desk_preview_->SetBorderColor(color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kCurrentDeskColor));
}
}
gfx::Insets DeskMiniView::GetPreviewBorderInsets() const {
return desk_preview_->border()->GetInsets();
}
const char* DeskMiniView::GetClassName() const {
return "DeskMiniView";
}
void DeskMiniView::Layout() {
const bool compact = owner_bar_->UsesCompactLayout();
const gfx::Rect preview_bounds = GetDeskPreviewBounds(root_window_, compact);
desk_preview_->SetBoundsRect(preview_bounds);
desk_name_view_->SetVisible(!compact);
if (!compact)
LayoutDeskNameView(preview_bounds);
close_desk_button_->SetBounds(
preview_bounds.right() - CloseDeskButton::kCloseButtonSize -
kCloseButtonMargin,
kCloseButtonMargin, CloseDeskButton::kCloseButtonSize,
CloseDeskButton::kCloseButtonSize);
}
gfx::Size DeskMiniView::CalculatePreferredSize() const {
const bool compact = owner_bar_->UsesCompactLayout();
const gfx::Rect preview_bounds = GetDeskPreviewBounds(root_window_, compact);
if (compact)
return preview_bounds.size();
// The preferred size takes into account only the width of the preview
// view. Desk preview's bottom inset should be excluded to maintain
// |kLabelPreviewSpacing| between preview and desk name view.
return gfx::Size{preview_bounds.width(),
preview_bounds.height() - GetPreviewBorderInsets().bottom() +
2 * kLabelPreviewSpacing +
desk_name_view_->GetPreferredSize().height()};
}
void DeskMiniView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
desk_preview_->GetAccessibleNodeData(node_data);
// Note that the desk may have already been destroyed.
if (desk_) {
// Announce desk name.
node_data->AddStringAttribute(
ax::mojom::StringAttribute::kName,
l10n_util::GetStringFUTF8(IDS_ASH_DESKS_DESK_ACCESSIBLE_NAME,
desk_->name()));
node_data->AddStringAttribute(
ax::mojom::StringAttribute::kValue,
l10n_util::GetStringUTF8(
desk_->is_active()
? IDS_ASH_DESKS_ACTIVE_DESK_MINIVIEW_A11Y_EXTRA_TIP
: IDS_ASH_DESKS_INACTIVE_DESK_MINIVIEW_A11Y_EXTRA_TIP));
}
if (DesksController::Get()->CanRemoveDesks()) {
node_data->AddStringAttribute(
ax::mojom::StringAttribute::kDescription,
l10n_util::GetStringUTF8(
IDS_ASH_OVERVIEW_CLOSABLE_HIGHLIGHT_ITEM_A11Y_EXTRA_TIP));
}
}
void DeskMiniView::OnThemeChanged() {
views::View::OnThemeChanged();
UpdateBorderColor();
}
void DeskMiniView::OnContentChanged() {
desk_preview_->RecreateDeskContentsMirrorLayers();
}
void DeskMiniView::OnDeskDestroyed(const Desk* desk) {
// Note that the mini_view outlives the desk (which will be removed after all
// DeskController's observers have been notified of its removal) because of
// the animation.
// Note that we can't make it the other way around (i.e. make the desk outlive
// the mini_view). The desk's existence (or lack thereof) is more important
// than the existence of the mini_view, since it determines whether we can
// create new desks or remove existing ones. This determines whether the close
// button will show on hover, and whether the new_desk_button is enabled. We
// shouldn't allow that state to be wrong while the mini_views perform the
// desk removal animation.
// TODO(afakhry): Consider detaching the layer and destroying the mini_view
// directly.
DCHECK_EQ(desk_, desk);
desk_ = nullptr;
// No need to remove `this` as an observer; it's done automatically.
}
void DeskMiniView::OnDeskNameChanged(const std::u16string& new_name) {
if (is_desk_name_being_modified_)
return;
desk_name_view_->SetTextAndElideIfNeeded(new_name);
desk_name_view_->SetAccessibleName(new_name);
desk_preview_->SetAccessibleName(new_name);
Layout();
}
views::View* DeskMiniView::GetView() {
return this;
}
void DeskMiniView::MaybeActivateHighlightedView() {
DesksController::Get()->ActivateDesk(desk(),
DesksSwitchSource::kMiniViewButton);
}
void DeskMiniView::MaybeCloseHighlightedView() {
OnCloseButtonPressed();
}
void DeskMiniView::MaybeSwapHighlightedView(bool right) {
const int old_index = owner_bar_->GetMiniViewIndex(this);
DCHECK_NE(old_index, -1);
const bool mirrored = owner_bar_->GetMirrored();
// If mirrored, flip the swap direction.
int new_index = mirrored ^ right ? old_index + 1 : old_index - 1;
if (new_index < 0 ||
new_index == static_cast<int>(owner_bar_->mini_views().size())) {
return;
}
auto* desks_controller = DesksController::Get();
desks_controller->ReorderDesk(old_index, new_index);
desks_controller->UpdateDesksDefaultNames();
}
void DeskMiniView::OnViewHighlighted() {
UpdateBorderColor();
owner_bar_->ScrollToShowMiniViewIfNecessary(this);
}
void DeskMiniView::OnViewUnhighlighted() {
UpdateBorderColor();
}
void DeskMiniView::ContentsChanged(views::Textfield* sender,
const std::u16string& new_contents) {
DCHECK_EQ(sender, desk_name_view_);
DCHECK(is_desk_name_being_modified_);
if (!desk_)
return;
// Avoid copying new_contents if we don't need to trim it below.
const std::u16string* new_text = &new_contents;
// To avoid potential security and memory issues, we don't allow desk names to
// have an unbounded length. Therefore we trim if needed at kMaxLength UTF-16
// boundary. Note that we don't care about code point boundaries in this case.
std::u16string trimmed_new_contents;
if (new_contents.size() > DeskNameView::kMaxLength) {
trimmed_new_contents = new_contents;
trimmed_new_contents.resize(DeskNameView::kMaxLength);
new_text = &trimmed_new_contents;
desk_name_view_->SetText(trimmed_new_contents);
}
desk_->SetName(
base::CollapseWhitespace(*new_text,
/*trim_sequences_with_line_breaks=*/false),
/*set_by_user=*/true);
Layout();
}
bool DeskMiniView::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
DCHECK_EQ(sender, desk_name_view_);
DCHECK(is_desk_name_being_modified_);
// Pressing enter or escape should blur the focus away from DeskNameView so
// that editing the desk's name ends.
if (key_event.type() != ui::ET_KEY_PRESSED)
return false;
if (key_event.key_code() != ui::VKEY_RETURN &&
key_event.key_code() != ui::VKEY_ESCAPE) {
return false;
}
DeskNameView::CommitChanges(GetWidget());
Shell::Get()
->accessibility_controller()
->TriggerAccessibilityAlertWithMessage(l10n_util::GetStringFUTF8(
IDS_ASH_DESKS_DESK_NAME_COMMIT, desk_->name()));
return true;
}
bool DeskMiniView::HandleMouseEvent(views::Textfield* sender,
const ui::MouseEvent& mouse_event) {
DCHECK_EQ(sender, desk_name_view_);
switch (mouse_event.type()) {
case ui::ET_MOUSE_PRESSED:
// If this is the first mouse press on the DeskNameView, then it's not
// focused yet. OnViewFocused() should not select all text, since it will
// be undone by the mouse release event. Instead we defer it until we get
// the mouse release event.
if (!is_desk_name_being_modified_)
defer_select_all_ = true;
break;
case ui::ET_MOUSE_RELEASED:
if (defer_select_all_) {
defer_select_all_ = false;
// The user may have already clicked and dragged to select some range
// other than all the text. In this case, don't mess with an existing
// selection.
if (!desk_name_view_->HasSelection())
desk_name_view_->SelectAll(false);
return true;
}
break;
default:
break;
}
return false;
}
void DeskMiniView::OnViewFocused(views::View* observed_view) {
DCHECK_EQ(observed_view, desk_name_view_);
is_desk_name_being_modified_ = true;
desk_name_view_->UpdateViewAppearance();
// Set the unelided desk name so that the full name shows up for the user to
// be able to change it.
desk_name_view_->SetText(desk_->name());
if (!defer_select_all_)
desk_name_view_->SelectAll(false);
}
void DeskMiniView::OnViewBlurred(views::View* observed_view) {
DCHECK_EQ(observed_view, desk_name_view_);
is_desk_name_being_modified_ = false;
defer_select_all_ = false;
desk_name_view_->UpdateViewAppearance();
// When committing the name, do not allow an empty desk name. Revert back to
// the default name if the desk is not being removed.
// TODO(afakhry): Make this more robust. What if user renames a previously
// user-modified desk name, say from "code" to "Desk 2", and that desk
// happened to be in the second position. Since the new name matches the
// default one for this position, should we revert it (i.e. consider it
// `set_by_user = false`?
if (!desk_->is_desk_being_removed() && desk_->name().empty()) {
DesksController::Get()->RevertDeskNameToDefault(desk_);
return;
}
OnDeskNameChanged(desk_->name());
// Only when the new desk name has been committed is when we can update the
// desks restore prefs.
desks_restore_util::UpdatePrimaryUserDeskNamesPrefs();
}
bool DeskMiniView::IsPointOnMiniView(const gfx::Point& screen_location) const {
gfx::Point point_in_view = screen_location;
ConvertPointFromScreen(this, &point_in_view);
return HitTestPoint(point_in_view);
}
int DeskMiniView::GetMinWidthForDefaultLayout() const {
const auto& root_size = root_window_->bounds().size();
return GetPreviewWidth(root_size,
DeskPreviewView::GetHeight(root_window_,
/*compact=*/false));
}
bool DeskMiniView::IsDeskNameViewVisibleForTesting() const {
return desk_name_view_->GetVisible();
}
void DeskMiniView::OnCloseButtonPressed() {
auto* controller = DesksController::Get();
if (!controller->CanRemoveDesks())
return;
// Hide the close button so it can no longer be pressed.
close_desk_button_->SetVisible(false);
desk_preview_->OnRemovingDesk();
controller->RemoveDesk(desk_, DesksCreationRemovalSource::kButton);
}
void DeskMiniView::OnDeskPreviewPressed() {
DesksController::Get()->ActivateDesk(desk_,
DesksSwitchSource::kMiniViewButton);
}
void DeskMiniView::LayoutDeskNameView(const gfx::Rect& preview_bounds) {
const int previous_width = desk_name_view_->width();
const gfx::Size desk_name_view_size = desk_name_view_->GetPreferredSize();
const int text_width =
base::ClampToRange(desk_name_view_size.width(), kMinDeskNameViewWidth,
preview_bounds.width());
const int desk_name_view_x =
preview_bounds.x() + (preview_bounds.width() - text_width) / 2;
gfx::Rect desk_name_view_bounds{desk_name_view_x,
preview_bounds.bottom() -
GetPreviewBorderInsets().bottom() +
kLabelPreviewSpacing,
text_width, desk_name_view_size.height()};
desk_name_view_->SetBoundsRect(desk_name_view_bounds);
// A change in the DeskNameView's width might mean the need
// to elide the text differently.
if (previous_width != desk_name_view_bounds.width())
OnDeskNameChanged(desk_->name());
}
} // namespace ash