123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- // Copyright (c) 2013 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/app_list/views/folder_header_view.h"
- #include <algorithm>
- #include <memory>
- #include "ash/app_list/app_list_util.h"
- #include "ash/app_list/model/app_list_folder_item.h"
- #include "ash/app_list/views/app_list_folder_view.h"
- #include "ash/public/cpp/app_list/app_list_color_provider.h"
- #include "ash/public/cpp/app_list/app_list_config.h"
- #include "ash/public/cpp/app_list/app_list_features.h"
- #include "ash/public/cpp/app_list/app_list_switches.h"
- #include "ash/strings/grit/ash_strings.h"
- #include "base/metrics/histogram_macros.h"
- #include "base/strings/utf_string_conversions.h"
- #include "ui/base/cursor/cursor.h"
- #include "ui/base/resource/resource_bundle.h"
- #include "ui/compositor/layer.h"
- #include "ui/gfx/canvas.h"
- #include "ui/gfx/text_elider.h"
- #include "ui/views/background.h"
- #include "ui/views/border.h"
- #include "ui/views/controls/button/image_button.h"
- #include "ui/views/controls/textfield/textfield.h"
- #include "ui/views/focus/focus_manager.h"
- #include "ui/views/painter.h"
- #include "ui/views/view_targeter_delegate.h"
- namespace ash {
- namespace {
- // The max folder name length.
- constexpr int kMaxFolderNameChars = 28;
- // Folder header dimensions. The max header width is based on the width of a
- // folder with 2 items.
- constexpr int kMinFolderHeaderWidth = 24;
- constexpr int kMaxFolderHeaderWidth = 168;
- constexpr int kFolderHeaderHeight = 32;
- // The min width of folder name - ensures the folder name is easily tappable.
- constexpr int kFolderHeaderMinTapWidth = 32;
- // The border radius for folder name.
- constexpr int kFolderNameBorderRadius = 4;
- // The border thickness for folder name.
- constexpr int kFolderNameBorderThickness = 2;
- // The inner padding for folder name.
- constexpr int kFolderNamePadding = 8;
- SkColor GetFolderBackgroundColor(bool is_active) {
- if (!is_active)
- return SK_ColorTRANSPARENT;
- const AppListColorProvider* color_provider = AppListColorProvider::Get();
- return SkColorSetA(color_provider->GetInkDropBaseColor(),
- color_provider->GetInkDropOpacity() * 255);
- }
- } // namespace
- class FolderHeaderView::FolderNameView : public views::Textfield,
- public views::ViewTargeterDelegate {
- public:
- explicit FolderNameView(FolderHeaderView* folder_header_view)
- : folder_header_view_(folder_header_view) {
- DCHECK(folder_header_view_);
- // Make folder name font size 14px.
- SetFontList(
- ui::ResourceBundle::GetSharedInstance().GetFontListWithDelta(2));
- set_placeholder_text_color(
- AppListColorProvider::Get()->GetFolderHintTextColor());
- SetTextColor(AppListColorProvider::Get()->GetFolderTitleTextColor());
- SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
- }
- FolderNameView(const FolderNameView&) = delete;
- FolderNameView& operator=(const FolderNameView&) = delete;
- ~FolderNameView() override = default;
- gfx::Size CalculatePreferredSize() const override {
- return gfx::Size(kMaxFolderHeaderWidth, kFolderHeaderHeight);
- }
- void OnThemeChanged() override {
- Textfield::OnThemeChanged();
- const bool is_active = has_mouse_already_entered_ || HasFocus();
- SetBackground(views::CreateRoundedRectBackground(
- GetFolderBackgroundColor(is_active), kFolderNameBorderRadius,
- kFolderNameBorderThickness));
- AppListColorProvider* color_provider = AppListColorProvider::Get();
- set_placeholder_text_color(color_provider->GetFolderHintTextColor());
- const SkColor text_color = color_provider->GetFolderTitleTextColor();
- SetTextColor(text_color);
- SetSelectionTextColor(text_color);
- SetSelectionBackgroundColor(color_provider->GetFolderNameSelectionColor());
- SetNameViewBorderAndBackground(is_active);
- }
- ui::Cursor GetCursor(const ui::MouseEvent& event) override {
- return ui::mojom::CursorType::kIBeam;
- }
- void SetNameViewBorderAndBackground(bool is_active) {
- SetBorder(views::CreatePaddedBorder(
- views::CreateRoundedRectBorder(
- kFolderNameBorderThickness, kFolderNameBorderRadius,
- AppListColorProvider::Get()->GetFolderNameBorderColor(is_active)),
- gfx::Insets::VH(0, kFolderNamePadding)));
- UpdateBackgroundColor(is_active);
- }
- void OnFocus() override {
- SetNameViewBorderAndBackground(/*is_active=*/true);
- SetText(folder_header_view_->GetFolderName());
- starting_name_ = GetText();
- folder_header_view_->previous_folder_name_ = starting_name_;
- if (!defer_select_all_)
- SelectAll(false);
- Textfield::OnFocus();
- }
- void OnBlur() override {
- SetNameViewBorderAndBackground(/*is_active=*/false);
- // Collapse whitespace when FolderNameView loses focus.
- folder_header_view_->ContentsChanged(
- this, base::CollapseWhitespace(GetText(), false));
- // Ensure folder name is truncated when FolderNameView loses focus.
- SetText(folder_header_view_->GetElidedFolderName());
- // Record metric each time a folder is renamed.
- if (GetText() != starting_name_) {
- if (folder_header_view_->is_tablet_mode()) {
- UMA_HISTOGRAM_COUNTS_100("Apps.AppListFolderNameLength.TabletMode",
- GetText().length());
- } else {
- UMA_HISTOGRAM_COUNTS_100("Apps.AppListFolderNameLength.ClamshellMode",
- GetText().length());
- }
- }
- defer_select_all_ = false;
- Textfield::OnBlur();
- }
- bool DoesMouseEventActuallyIntersect(const ui::MouseEvent& event) {
- // Since hitbox for this view is extended for tap, we need to manually
- // calculate this when checking for mouse events.
- return GetLocalBounds().Contains(event.location());
- }
- bool OnMousePressed(const ui::MouseEvent& event) override {
- // Since hovering changes the background color, only taps should be
- // triggered using the extended event target.
- if (!DoesMouseEventActuallyIntersect(event))
- return false;
- if (!HasFocus())
- defer_select_all_ = true;
- return Textfield::OnMousePressed(event);
- }
- void OnMouseExited(const ui::MouseEvent& event) override {
- if (!HasFocus())
- UpdateBackgroundColor(/*is_active=*/false);
- has_mouse_already_entered_ = false;
- }
- void OnMouseMoved(const ui::MouseEvent& event) override {
- if (DoesMouseEventActuallyIntersect(event) && !has_mouse_already_entered_) {
- // If this is reached, the mouse is entering the view.
- // Recreate border to have custom corner radius.
- UpdateBackgroundColor(/*is_active=*/true);
- has_mouse_already_entered_ = true;
- } else if (!DoesMouseEventActuallyIntersect(event) &&
- has_mouse_already_entered_ && !HasFocus()) {
- // If this is reached, the mouse is exiting the view on its horizontal
- // edges.
- UpdateBackgroundColor(/*is_active=*/false);
- has_mouse_already_entered_ = false;
- }
- }
- void OnMouseReleased(const ui::MouseEvent& event) override {
- if (defer_select_all_) {
- defer_select_all_ = false;
- if (!HasSelection())
- SelectAll(false);
- }
- Textfield::OnMouseReleased(event);
- }
- bool DoesIntersectRect(const views::View* target,
- const gfx::Rect& rect) const override {
- DCHECK_EQ(target, this);
- gfx::Rect textfield_bounds = target->GetLocalBounds();
- // Ensure that the tap target for this view is always at least the view's
- // minimum width.
- int min_width =
- std::max(kFolderHeaderMinTapWidth, textfield_bounds.width());
- int horizontal_padding = -((min_width - textfield_bounds.width()) / 2);
- textfield_bounds.Inset(gfx::Insets::VH(0, horizontal_padding));
- return textfield_bounds.Intersects(rect);
- }
- private:
- void UpdateBackgroundColor(bool is_active) {
- background()->SetNativeControlColor(GetFolderBackgroundColor(is_active));
- SchedulePaint();
- }
- // The parent FolderHeaderView, owns this.
- FolderHeaderView* const folder_header_view_;
- // Name of the folder when FolderNameView is focused, used to track folder
- // rename metric.
- std::u16string starting_name_;
- // If the view is focused via a mouse press event, then selection will be
- // cleared by its mouse release. To address this, defer selecting all
- // until we receive mouse release.
- bool defer_select_all_ = false;
- // Because of this view's custom event target, this view receives mouse enter
- // events in areas where the view isn't actually occupying. To check whether a
- // user has entered/exited this, we must check every mouse move event. This
- // bool tracks whether the mouse has entered the view, avoiding repainting the
- // background on each mouse move event.
- bool has_mouse_already_entered_ = false;
- };
- FolderHeaderView::FolderHeaderView(FolderHeaderViewDelegate* delegate)
- : folder_item_(nullptr),
- folder_name_placeholder_text_(
- ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
- IDS_APP_LIST_FOLDER_NAME_PLACEHOLDER)),
- delegate_(delegate),
- folder_name_visible_(true),
- is_tablet_mode_(false) {
- folder_name_view_ = AddChildView(std::make_unique<FolderNameView>(this));
- folder_name_view_->SetPlaceholderText(folder_name_placeholder_text_);
- folder_name_view_->set_controller(this);
- SetPaintToLayer();
- layer()->SetFillsBoundsOpaquely(false);
- }
- FolderHeaderView::~FolderHeaderView() {
- if (folder_item_)
- folder_item_->RemoveObserver(this);
- }
- void FolderHeaderView::SetFolderItem(AppListFolderItem* folder_item) {
- if (folder_item_)
- folder_item_->RemoveObserver(this);
- folder_item_ = folder_item;
- if (!folder_item_)
- return;
- folder_item_->AddObserver(this);
- folder_name_view_->SetEnabled(folder_item_->folder_type() !=
- AppListFolderItem::FOLDER_TYPE_OEM);
- Update();
- }
- void FolderHeaderView::UpdateFolderNameVisibility(bool visible) {
- folder_name_visible_ = visible;
- Update();
- SchedulePaint();
- }
- void FolderHeaderView::SetTextFocus() {
- folder_name_view_->RequestFocus();
- }
- bool FolderHeaderView::HasTextFocus() const {
- return folder_name_view_->HasFocus();
- }
- void FolderHeaderView::Update() {
- if (!folder_item_)
- return;
- folder_name_view_->SetVisible(folder_name_visible_);
- if (folder_name_visible_) {
- std::u16string elided_folder_name = GetElidedFolderName();
- folder_name_view_->SetText(elided_folder_name);
- UpdateFolderNameAccessibleName();
- }
- Layout();
- }
- void FolderHeaderView::UpdateFolderNameAccessibleName() {
- // Sets |folder_name_view_|'s accessible name to the placeholder text if
- // |folder_name_view_| is blank; otherwise, clear the accessible name, the
- // accessible state's value is set to be folder_name_view_->GetText() by
- // TextField.
- std::u16string accessible_name = folder_name_view_->GetText().empty()
- ? folder_name_placeholder_text_
- : std::u16string();
- folder_name_view_->SetAccessibleName(accessible_name);
- }
- const std::u16string& FolderHeaderView::GetFolderNameForTest() {
- return folder_name_view_->GetText();
- }
- void FolderHeaderView::SetFolderNameForTest(const std::u16string& name) {
- folder_name_view_->SetText(name);
- }
- bool FolderHeaderView::IsFolderNameEnabledForTest() const {
- return folder_name_view_->GetEnabled();
- }
- gfx::Size FolderHeaderView::CalculatePreferredSize() const {
- return gfx::Size(kMaxFolderHeaderWidth,
- folder_name_view_->GetPreferredSize().height());
- }
- const char* FolderHeaderView::GetClassName() const {
- return "FolderHeaderView";
- }
- void FolderHeaderView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
- Update();
- }
- views::Textfield* FolderHeaderView::GetFolderNameViewForTest() const {
- return folder_name_view_;
- }
- int FolderHeaderView::GetMaxFolderNameCharLengthForTest() const {
- return kMaxFolderNameChars;
- }
- std::u16string FolderHeaderView::GetFolderName() const {
- if (!folder_item_)
- return std::u16string();
- return base::UTF8ToUTF16(folder_item_->name());
- }
- std::u16string FolderHeaderView::GetElidedFolderName() const {
- if (!folder_item_)
- return std::u16string();
- // Enforce the maximum folder name length.
- std::u16string folder_name = GetFolderName();
- std::u16string name = folder_name.substr(0, kMaxFolderNameChars);
- // Get maximum text width for fitting into |folder_name_view_|.
- int text_width = std::min(kMaxFolderHeaderWidth, width()) -
- folder_name_view_->GetCaretBounds().width() -
- folder_name_view_->GetInsets().width();
- std::u16string elided_name = gfx::ElideText(
- name, folder_name_view_->GetFontList(), text_width, gfx::ELIDE_TAIL);
- return elided_name;
- }
- void FolderHeaderView::Layout() {
- gfx::Rect rect(GetContentsBounds());
- if (rect.IsEmpty())
- return;
- gfx::Rect text_bounds(rect);
- std::u16string text = folder_name_view_->GetText().empty()
- ? folder_name_placeholder_text_
- : folder_name_view_->GetText();
- int text_width =
- gfx::Canvas::GetStringWidth(text, folder_name_view_->GetFontList()) +
- folder_name_view_->GetCaretBounds().width() +
- folder_name_view_->GetInsets().width();
- text_width = std::min(text_width, kMaxFolderHeaderWidth);
- text_width = std::max(text_width, kMinFolderHeaderWidth);
- text_bounds.set_x(std::max(0, rect.x() + (rect.width() - text_width) / 2));
- text_bounds.set_width(std::min(rect.width(), text_width));
- text_bounds.ClampToCenteredSize(gfx::Size(
- text_bounds.width(), folder_name_view_->GetPreferredSize().height()));
- folder_name_view_->SetBoundsRect(text_bounds);
- }
- void FolderHeaderView::ContentsChanged(views::Textfield* sender,
- const std::u16string& new_contents) {
- // Temporarily remove from observer to ignore data change caused by us.
- if (!folder_item_)
- return;
- folder_item_->RemoveObserver(this);
- // Enforce the maximum folder name length in UI by trimming `new_contents`
- // when it is longer than the max length.
- if (new_contents.length() > kMaxFolderNameChars) {
- std::u16string trimmed_new_contents = new_contents;
- trimmed_new_contents.resize(kMaxFolderNameChars);
- folder_name_view_->SetText(trimmed_new_contents);
- } else {
- delegate_->SetItemName(folder_item_, base::UTF16ToUTF8(new_contents));
- }
- folder_item_->AddObserver(this);
- UpdateFolderNameAccessibleName();
- Layout();
- }
- bool FolderHeaderView::ShouldNameViewClearFocus(const ui::KeyEvent& key_event) {
- return key_event.type() == ui::ET_KEY_PRESSED &&
- (key_event.key_code() == ui::VKEY_RETURN ||
- key_event.key_code() == ui::VKEY_ESCAPE);
- }
- bool FolderHeaderView::HandleKeyEvent(views::Textfield* sender,
- const ui::KeyEvent& key_event) {
- if (ShouldNameViewClearFocus(key_event)) {
- // If the user presses the escape key, we should revert the text in
- // `folder_name_view_`.
- if (key_event.key_code() == ui::VKEY_ESCAPE)
- sender->SetText(previous_folder_name_);
- folder_name_view_->GetFocusManager()->ClearFocus();
- return true;
- }
- if (!IsUnhandledLeftRightKeyEvent(key_event))
- return false;
- return ProcessLeftRightKeyTraversalForTextfield(folder_name_view_, key_event);
- }
- void FolderHeaderView::ItemNameChanged() {
- Update();
- }
- } // namespace ash
|