blob: ca3c21bff2f8bf9ba5e5fd39e9b195e20794aff5 [file] [log] [blame]
// Copyright 2021 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/templates/desks_templates_item_view.h"
#include <string>
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/public/cpp/desk_template.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/close_button.h"
#include "ash/style/pill_button.h"
#include "ash/style/style_util.h"
#include "ash/wm/desks/desk_name_view.h"
#include "ash/wm/desks/desks_textfield.h"
#include "ash/wm/desks/templates/desks_templates_dialog_controller.h"
#include "ash/wm/desks/templates/desks_templates_icon_container.h"
#include "ash/wm/desks/templates/desks_templates_name_view.h"
#include "ash/wm/desks/templates/desks_templates_presenter.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_highlight_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/metadata/view_factory_internal.h"
#include "ui/views/view.h"
#include "ui/views/view_targeter_delegate.h"
namespace ash {
namespace {
// The padding values of the DesksTemplatesItemView.
constexpr int kHorizontalPaddingDp = 24;
constexpr int kVerticalPaddingDp = 16;
// The preferred size of the whole DesksTemplatesItemView.
constexpr gfx::Size kPreferredSize(220, 120);
// The corner radius for the DesksTemplatesItemView.
constexpr int kCornerRadius = 16;
// The margin for the delete button.
constexpr int kDeleteButtonMargin = 8;
// The minimum template name view width.
constexpr int kMinTemplateNameViewWidth = 56;
// The margin between the grid item contents and the card container.
constexpr int kGridItemMargin = 24;
constexpr int kTimeViewHeight = 20;
// The margin for the managed status icon.
constexpr int kManagedStatusIndicatorMargin = 8;
constexpr int kManagedStatusIndicatorSize = 20;
constexpr char kAmPmTimeDateFmtStr[] = "%d:%02d%s, %d-%02d-%02d";
// TODO(richui): This is a placeholder text format. Update this once specs are
// done.
std::u16string GetTimeStr(base::Time timestamp) {
base::Time::Exploded exploded_time;
timestamp.LocalExplode(&exploded_time);
const int noon = 12;
int hour = exploded_time.hour % noon;
if (hour == 0)
hour += noon;
std::string time = base::StringPrintf(
kAmPmTimeDateFmtStr, hour, exploded_time.minute,
(exploded_time.hour >= noon ? "pm" : "am"), exploded_time.year,
exploded_time.month, exploded_time.day_of_month);
return base::UTF8ToUTF16(time);
}
} // namespace
DesksTemplatesItemView::DesksTemplatesItemView(DeskTemplate* desk_template)
: desk_template_(desk_template) {
auto launch_template_callback = base::BindRepeating(
&DesksTemplatesItemView::OnGridItemPressed, base::Unretained(this));
const std::u16string template_name = desk_template_->template_name();
views::View* spacer;
views::BoxLayoutView* card_container;
views::Builder<DesksTemplatesItemView>(this)
.SetPreferredSize(kPreferredSize)
.SetUseDefaultFillLayout(true)
.SetAccessibleName(template_name)
.SetCallback(std::move(launch_template_callback))
.SetBackground(views::CreateRoundedRectBackground(
AshColorProvider::Get()->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::
kControlBackgroundColorInactive),
kCornerRadius))
.AddChildren(
views::Builder<views::BoxLayoutView>()
.CopyAddressTo(&card_container)
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.SetCrossAxisAlignment(
views::BoxLayout::CrossAxisAlignment::kStart)
.SetInsideBorderInsets(
gfx::Insets(kVerticalPaddingDp, kHorizontalPaddingDp))
.AddChildren(
views::Builder<DesksTemplatesNameView>()
.CopyAddressTo(&name_view_)
.SetText(template_name)
.SetAccessibleName(template_name),
views::Builder<views::Label>()
.CopyAddressTo(&time_view_)
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
.SetText(GetTimeStr(desk_template_->created_time()))
.SetPreferredSize(gfx::Size(
kPreferredSize.width() - kGridItemMargin * 2,
kTimeViewHeight)),
views::Builder<views::View>().CopyAddressTo(&spacer),
views::Builder<DesksTemplatesIconContainer>().CopyAddressTo(
&icon_container_view_)),
views::Builder<views::ImageView>()
.CopyAddressTo(&managed_status_indicator_)
.SetPreferredSize(gfx::Size(kManagedStatusIndicatorSize,
kManagedStatusIndicatorSize))
.SetImage(gfx::CreateVectorIcon(
chromeos::kEnterpriseIcon, kManagedStatusIndicatorSize,
AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorSecondary)))
.SetVisible(desk_template->source() ==
DeskTemplateSource::kPolicy),
views::Builder<views::View>().CopyAddressTo(&hover_container_))
.BuildChildren();
// TODO(crbug.com/1267470): Make `PillButton` work with views::Builder.
launch_button_ = hover_container_->AddChildView(std::make_unique<PillButton>(
base::BindRepeating(&DesksTemplatesItemView::OnGridItemPressed,
base::Unretained(this)),
l10n_util::GetStringUTF16(IDS_ASH_DESKS_TEMPLATES_USE_TEMPLATE_BUTTON),
PillButton::Type::kIconless, /*icon=*/nullptr));
delete_button_ = hover_container_->AddChildView(std::make_unique<CloseButton>(
base::BindRepeating(&DesksTemplatesItemView::OnDeleteButtonPressed,
base::Unretained(this)),
CloseButton::Type::kMedium));
name_view_->SetTextAndElideIfNeeded(template_name);
name_view_->set_controller(this);
name_view_observation_.Observe(name_view_);
hover_container_->SetUseDefaultFillLayout(true);
hover_container_->SetVisible(false);
icon_container_view_->PopulateIconContainerFromTemplate(desk_template_);
icon_container_view_->SetVisible(true);
card_container->SetFlexForView(spacer, 1);
StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/false);
views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
kCornerRadius);
views::FocusRing* focus_ring =
StyleUtil::SetUpFocusRingForView(this, kFocusRingHaloInset);
focus_ring->SetHasFocusPredicate([](views::View* view) {
return static_cast<DesksTemplatesItemView*>(view)->IsViewHighlighted();
});
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
}
DesksTemplatesItemView::~DesksTemplatesItemView() {
name_view_observation_.Reset();
}
void DesksTemplatesItemView::UpdateHoverButtonsVisibility(
const gfx::Point& screen_location,
bool is_touch) {
gfx::Point location_in_view = screen_location;
ConvertPointFromScreen(this, &location_in_view);
// For switch access, setting the hover buttons to visible allows users to
// navigate to it.
const bool visible =
!is_template_name_being_modified_ &&
((is_touch && HitTestPoint(location_in_view)) ||
(!is_touch && IsMouseHovered()) ||
Shell::Get()->accessibility_controller()->IsSwitchAccessRunning());
hover_container_->SetVisible(visible);
icon_container_view_->SetVisible(!visible);
}
bool DesksTemplatesItemView::IsTemplateNameBeingModified() const {
return name_view_->HasFocus();
}
void DesksTemplatesItemView::Layout() {
views::View::Layout();
LayoutTemplateNameView();
managed_status_indicator_->SetBoundsRect(
gfx::Rect(name_view_->bounds().width() + kHorizontalPaddingDp +
kManagedStatusIndicatorMargin,
name_view_->y(), kManagedStatusIndicatorSize,
kManagedStatusIndicatorSize));
const gfx::Size delete_button_size = delete_button_->GetPreferredSize();
DCHECK_EQ(delete_button_size.width(), delete_button_size.height());
delete_button_->SetBoundsRect(
gfx::Rect(width() - delete_button_size.width() - kDeleteButtonMargin,
kDeleteButtonMargin, delete_button_size.width(),
delete_button_size.height()));
const gfx::Size launch_button_preferred_size =
launch_button_->CalculatePreferredSize();
launch_button_->SetBoundsRect(gfx::Rect(
{(width() - launch_button_preferred_size.width()) / 2,
height() - launch_button_preferred_size.height() - kVerticalPaddingDp},
launch_button_preferred_size));
}
void DesksTemplatesItemView::OnThemeChanged() {
views::View::OnThemeChanged();
auto* color_provider = AshColorProvider::Get();
const SkColor control_background_color_inactive =
color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kControlBackgroundColorInactive);
GetBackground()->SetNativeControlColor(control_background_color_inactive);
time_view_->SetBackgroundColor(control_background_color_inactive);
time_view_->SetEnabledColor(color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorSecondary));
views::FocusRing::Get(this)->SetColor(color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor));
}
void DesksTemplatesItemView::OnViewFocused(views::View* observed_view) {
// `this` is a button which observes itself. Here we only care about focus on
// `name_view_`.
if (observed_view == this)
return;
DCHECK_EQ(observed_view, name_view_);
is_template_name_being_modified_ = true;
// Assume we should commit the name change unless `HandleKeyEvent` detects the
// user pressed the escape key.
should_commit_name_changes_ = true;
name_view_->UpdateViewAppearance();
// Hide the hover container when we are modifying the template name.
hover_container_->SetVisible(false);
icon_container_view_->SetVisible(true);
// Set the unelided template name so that the full name shows up for the user
// to be able to change it.
name_view_->SetText(desk_template_->template_name());
// Set the Overview highlight to move focus with the `name_view_`.
auto* highlight_controller = Shell::Get()
->overview_controller()
->overview_session()
->highlight_controller();
if (highlight_controller->IsFocusHighlightVisible())
highlight_controller->MoveHighlightToView(name_view_);
if (!defer_select_all_)
name_view_->SelectAll(false);
}
void DesksTemplatesItemView::OnViewBlurred(views::View* observed_view) {
// `this` is a button which observes itself. Here we only care about blur on
// `name_view_`.
if (observed_view == this)
return;
// If we exit overview while the `name_view_` is still focused, the shutdown
// sequence will reset the presenter before `OnViewBlurred` gets called. This
// checks and makes sure that we don't call the presenter while trying to
// shutdown the overview session.
// `overview_session` may also be null as `OnViewBlurred` may be called after
// the owning widget is no longer owned by the session for overview exit
// animation. See https://crbug.com/1281422.
// TODO(richui): Revisit this once the behavior of the template name when
// exiting overview is determined.
OverviewSession* overview_session =
Shell::Get()->overview_controller()->overview_session();
if (!overview_session || overview_session->is_shutting_down())
return;
DCHECK_EQ(observed_view, name_view_);
is_template_name_being_modified_ = false;
defer_select_all_ = false;
name_view_->UpdateViewAppearance();
// Collapse the whitespace for the text first before comparing it or trying to
// commit the name in order to prevent duplicate name issues.
name_view_->SetText(
base::CollapseWhitespace(name_view_->GetText(),
/*trim_sequences_with_line_breaks=*/false));
// When committing the name, do not allow an empty template name. Also, don't
// commit the name changes if the view was blurred from the user pressing the
// escape key (identified by `should_commit_name_changes_`). Revert back to
// the original name.
if (!should_commit_name_changes_ || name_view_->GetText().empty() ||
desk_template_->template_name() == name_view_->GetText()) {
OnTemplateNameChanged(desk_template_->template_name());
return;
}
auto updated_template = desk_template_->Clone();
updated_template->set_template_name(name_view_->GetText());
OnTemplateNameChanged(updated_template->template_name());
// Calling `SaveOrUpdateDeskTemplate` will trigger rebuilding the desks
// templates grid views hierarchy which includes `this`. Use a post task as
// some other `ViewObserver`'s may still be using `this`.
// TODO(crbug.com/1266552): Remove the post task once saving and updating does
// not cause a `this` to be deleted anymore.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(
[](std::unique_ptr<DeskTemplate> desk_template) {
DesksTemplatesPresenter::Get()->SaveOrUpdateDeskTemplate(
/*is_update=*/false, std::move(desk_template));
},
std::move(updated_template)));
}
views::Button::KeyClickAction DesksTemplatesItemView::GetKeyClickActionForEvent(
const ui::KeyEvent& event) {
// Prevents any key events from activating a button click while the template
// name is being modified.
if (is_template_name_being_modified_)
return KeyClickAction::kNone;
return Button::GetKeyClickActionForEvent(event);
}
void DesksTemplatesItemView::ContentsChanged(
views::Textfield* sender,
const std::u16string& new_contents) {
DCHECK_EQ(sender, name_view_);
DCHECK(is_template_name_being_modified_);
// To avoid potential security and memory issues, we don't allow template
// 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.
if (new_contents.size() > DesksTextfield::kMaxLength) {
std::u16string trimmed_new_contents = new_contents;
trimmed_new_contents.resize(DesksTextfield::kMaxLength);
name_view_->SetText(trimmed_new_contents);
}
Layout();
}
bool DesksTemplatesItemView::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
DCHECK_EQ(sender, name_view_);
DCHECK(is_template_name_being_modified_);
// Pressing enter or escape should blur the focus away from `name_view_` so
// that editing the template's name ends. Pressing tab should do the same, but
// is handled in `OverviewSession`.
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;
}
// If the escape key was pressed, `should_commit_name_changes_` is set to
// false so that `OnViewBlurred` knows that it should not change the name of
// the template.
if (key_event.key_code() == ui::VKEY_ESCAPE)
should_commit_name_changes_ = false;
DesksTemplatesNameView::CommitChanges(GetWidget());
return true;
}
bool DesksTemplatesItemView::HandleMouseEvent(
views::Textfield* sender,
const ui::MouseEvent& mouse_event) {
DCHECK_EQ(sender, name_view_);
switch (mouse_event.type()) {
case ui::ET_MOUSE_PRESSED:
// If this is the first mouse press on the `name_view_`, 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_template_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 (!name_view_->HasSelection()) {
name_view_->SelectAll(false);
}
return true;
}
break;
default:
break;
}
return false;
}
views::View* DesksTemplatesItemView::TargetForRect(views::View* root,
const gfx::Rect& rect) {
// With the design of the template card having the textfield within a
// clickable button, as well as having the grid view be a `PreTargetHandler`,
// we needed to make `this` a `ViewTargeterDelegate` for the view event
// targeter in order to allow the `name_view_` to be specifically targeted and
// focused.
if (root == this && name_view_->GetMirroredBounds().Contains(rect))
return name_view_;
return views::ViewTargeterDelegate::TargetForRect(root, rect);
}
void DesksTemplatesItemView::OnDeleteTemplate() {
// Notify the highlight controller that we're going away.
OverviewHighlightController* highlight_controller =
Shell::Get()
->overview_controller()
->overview_session()
->highlight_controller();
DCHECK(highlight_controller);
highlight_controller->OnViewDestroyingOrDisabling(this);
highlight_controller->OnViewDestroyingOrDisabling(name_view_);
DesksTemplatesPresenter::Get()->DeleteEntry(
desk_template_->uuid().AsLowercaseString());
}
void DesksTemplatesItemView::OnDeleteButtonPressed() {
// Show the dialog to confirm the deletion.
auto* dialog_controller = DesksTemplatesDialogController::Get();
dialog_controller->ShowDeleteDialog(
Shell::GetPrimaryRootWindow(), name_view_->GetAccessibleName(),
base::BindOnce(&DesksTemplatesItemView::OnDeleteTemplate,
base::Unretained(this)));
}
void DesksTemplatesItemView::OnGridItemPressed() {
if (is_template_name_being_modified_) {
DesksTemplatesNameView::CommitChanges(GetWidget());
return;
}
DesksTemplatesPresenter::Get()->LaunchDeskTemplate(
desk_template_->uuid().AsLowercaseString());
}
void DesksTemplatesItemView::OnTemplateNameChanged(
const std::u16string& new_name) {
if (is_template_name_being_modified_)
return;
name_view_->SetTextAndElideIfNeeded(new_name);
name_view_->SetAccessibleName(new_name);
SetAccessibleName(new_name);
Layout();
}
void DesksTemplatesItemView::LayoutTemplateNameView() {
const int previous_width = name_view_->width();
const gfx::Size name_view_size = name_view_->GetPreferredSize();
// The item view's width is supposed to be larger than
// `kMinTemplateNameViewWidth`, but it might be not the truth for tests with
// extreme abnormal size of display.
const int min_width =
std::min(kPreferredSize.width(), kMinTemplateNameViewWidth);
// TODO(crbug.com/1264174): Investigate the best way to get this to work with
// the enterprise indicator. Possibly wrap both in a `BoxLayoutView`.
const int max_width = std::max(
kPreferredSize.width() - (kHorizontalPaddingDp * 2) -
(managed_status_indicator_->GetVisible()
? (kManagedStatusIndicatorMargin + kManagedStatusIndicatorSize)
: 0),
kMinTemplateNameViewWidth);
const int text_width =
base::clamp(name_view_size.width(), min_width, max_width);
gfx::Rect name_view_bounds{name_view_->bounds()};
name_view_bounds.set_width(text_width);
name_view_->SetBoundsRect(name_view_bounds);
// A change in the `name_view_`'s width might mean the need to elide the text
// differently.
if (previous_width != name_view_bounds.width())
OnTemplateNameChanged(desk_template_->template_name());
}
views::View* DesksTemplatesItemView::GetView() {
return this;
}
void DesksTemplatesItemView::MaybeActivateHighlightedView() {
OnGridItemPressed();
}
void DesksTemplatesItemView::MaybeCloseHighlightedView() {
OnDeleteButtonPressed();
}
void DesksTemplatesItemView::MaybeSwapHighlightedView(bool right) {}
void DesksTemplatesItemView::OnViewHighlighted() {
views::FocusRing::Get(this)->SchedulePaint();
}
void DesksTemplatesItemView::OnViewUnhighlighted() {
views::FocusRing::Get(this)->SchedulePaint();
}
BEGIN_METADATA(DesksTemplatesItemView, views::Button)
END_METADATA
} // namespace ash