1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/app_list/views/folder_header_view.h"
6 
7 #include <algorithm>
8 #include <memory>
9 
10 #include "ash/app_list/app_list_util.h"
11 #include "ash/app_list/model/app_list_folder_item.h"
12 #include "ash/app_list/views/app_list_folder_view.h"
13 #include "ash/public/cpp/app_list/app_list_color_provider.h"
14 #include "ash/public/cpp/app_list/app_list_config.h"
15 #include "ash/public/cpp/app_list/app_list_features.h"
16 #include "ash/public/cpp/app_list/app_list_switches.h"
17 #include "base/macros.h"
18 #include "base/metrics/histogram_macros.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "ui/base/resource/resource_bundle.h"
21 #include "ui/gfx/canvas.h"
22 #include "ui/gfx/color_palette.h"
23 #include "ui/gfx/text_elider.h"
24 #include "ui/strings/grit/ui_strings.h"
25 #include "ui/views/background.h"
26 #include "ui/views/border.h"
27 #include "ui/views/controls/button/image_button.h"
28 #include "ui/views/controls/textfield/textfield.h"
29 #include "ui/views/native_cursor.h"
30 #include "ui/views/painter.h"
31 #include "ui/views/view_targeter_delegate.h"
32 
33 namespace ash {
34 
35 class FolderHeaderView::FolderNameView : public views::Textfield,
36                                          public views::ViewTargeterDelegate {
37  public:
FolderNameView(FolderHeaderView * folder_header_view)38   explicit FolderNameView(FolderHeaderView* folder_header_view)
39       : folder_header_view_(folder_header_view) {
40     DCHECK(folder_header_view_);
41     // Make folder name font size 14px.
42     SetFontList(
43         ui::ResourceBundle::GetSharedInstance().GetFontListWithDelta(2));
44     set_placeholder_text_color(
45         AppListColorProvider::Get()->GetFolderHintTextColor());
46     SetTextColor(AppListColorProvider::Get()->GetFolderTitleTextColor(
47         gfx::kGoogleGrey700));
48     SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
49   }
50 
51   ~FolderNameView() override = default;
52 
CalculatePreferredSize() const53   gfx::Size CalculatePreferredSize() const override {
54     return gfx::Size(AppListConfig::instance().folder_header_max_width(),
55                      AppListConfig::instance().folder_header_height());
56   }
57 
OnThemeChanged()58   void OnThemeChanged() override {
59     Textfield::OnThemeChanged();
60 
61     const bool is_active = has_mouse_already_entered_ || HasFocus();
62     AppListColorProvider* color_provider = AppListColorProvider::Get();
63     SetBackground(views::CreateRoundedRectBackground(
64         color_provider->GetFolderNameBackgroundColor(is_active),
65         AppListConfig::instance().folder_name_border_radius()));
66 
67     const SkColor text_color =
68         color_provider->GetFolderTitleTextColor(gfx::kGoogleGrey700);
69     SetTextColor(text_color);
70     SetSelectionTextColor(text_color);
71     SetSelectionBackgroundColor(color_provider->GetFolderNameSelectionColor());
72     SetNameViewBorderAndBackground(is_active);
73   }
74 
GetCursor(const ui::MouseEvent & event)75   gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
76     return views::GetNativeIBeamCursor();
77   }
78 
SetNameViewBorderAndBackground(bool is_active)79   void SetNameViewBorderAndBackground(bool is_active) {
80     int horizontal_padding = AppListConfig::instance().folder_name_padding();
81     SetBorder(views::CreatePaddedBorder(
82         views::CreateRoundedRectBorder(
83             AppListConfig::instance().folder_name_border_thickness(),
84             AppListConfig::instance().folder_name_border_radius(),
85             AppListColorProvider::Get()->GetFolderNameBorderColor(is_active)),
86         gfx::Insets(0, horizontal_padding)));
87     UpdateBackgroundColor(is_active);
88   }
89 
OnFocus()90   void OnFocus() override {
91     SetNameViewBorderAndBackground(/*is_active=*/true);
92     SetText(base::UTF8ToUTF16(folder_header_view_->folder_item_->name()));
93     starting_name_ = GetText();
94     folder_header_view_->previous_folder_name_ = starting_name_;
95 
96     if (!defer_select_all_)
97       SelectAll(false);
98 
99     Textfield::OnFocus();
100   }
101 
OnBlur()102   void OnBlur() override {
103     SetNameViewBorderAndBackground(/*is_active=*/false);
104 
105     // Collapse whitespace when FolderNameView loses focus.
106     folder_header_view_->ContentsChanged(
107         this, base::CollapseWhitespace(GetText(), false));
108 
109     // Ensure folder name is truncated when FolderNameView loses focus.
110     SetText(folder_header_view_->GetElidedFolderName(
111         base::UTF8ToUTF16(folder_header_view_->folder_item_->name())));
112 
113     // Record metric each time a folder is renamed.
114     if (GetText() != starting_name_) {
115       if (folder_header_view_->is_tablet_mode()) {
116         UMA_HISTOGRAM_COUNTS_100("Apps.AppListFolderNameLength.TabletMode",
117                                  GetText().length());
118       } else {
119         UMA_HISTOGRAM_COUNTS_100("Apps.AppListFolderNameLength.ClamshellMode",
120                                  GetText().length());
121       }
122     }
123 
124     defer_select_all_ = false;
125 
126     Textfield::OnBlur();
127   }
128 
DoesMouseEventActuallyIntersect(const ui::MouseEvent & event)129   bool DoesMouseEventActuallyIntersect(const ui::MouseEvent& event) {
130     // Since hitbox for this view is extended for tap, we need to manually
131     // calculate this when checking for mouse events.
132     return GetLocalBounds().Contains(event.location());
133   }
134 
OnMousePressed(const ui::MouseEvent & event)135   bool OnMousePressed(const ui::MouseEvent& event) override {
136     // Since hovering changes the background color, only taps should be
137     // triggered using the extended event target.
138     if (!DoesMouseEventActuallyIntersect(event))
139       return false;
140 
141     if (!HasFocus())
142       defer_select_all_ = true;
143 
144     return Textfield::OnMousePressed(event);
145   }
146 
OnMouseExited(const ui::MouseEvent & event)147   void OnMouseExited(const ui::MouseEvent& event) override {
148     if (!HasFocus())
149       UpdateBackgroundColor(/*is_active=*/false);
150 
151     has_mouse_already_entered_ = false;
152   }
153 
OnMouseMoved(const ui::MouseEvent & event)154   void OnMouseMoved(const ui::MouseEvent& event) override {
155     if (DoesMouseEventActuallyIntersect(event) && !has_mouse_already_entered_) {
156       // If this is reached, the mouse is entering the view.
157       // Recreate border to have custom corner radius.
158       UpdateBackgroundColor(/*is_active=*/true);
159       has_mouse_already_entered_ = true;
160     } else if (!DoesMouseEventActuallyIntersect(event) &&
161                has_mouse_already_entered_ && !HasFocus()) {
162       // If this is reached, the mouse is exiting the view on its horizontal
163       // edges.
164       UpdateBackgroundColor(/*is_active=*/false);
165       has_mouse_already_entered_ = false;
166     }
167   }
168 
OnMouseReleased(const ui::MouseEvent & event)169   void OnMouseReleased(const ui::MouseEvent& event) override {
170     if (defer_select_all_) {
171       defer_select_all_ = false;
172 
173       if (!HasSelection())
174         SelectAll(false);
175     }
176 
177     Textfield::OnMouseReleased(event);
178   }
179 
DoesIntersectRect(const views::View * target,const gfx::Rect & rect) const180   bool DoesIntersectRect(const views::View* target,
181                          const gfx::Rect& rect) const override {
182     DCHECK_EQ(target, this);
183     gfx::Rect textfield_bounds = target->GetLocalBounds();
184 
185     // Ensure that the tap target for this view is always at least the view's
186     // minimum width.
187     int min_width =
188         std::max(AppListConfig::instance().folder_header_min_tap_width(),
189                  textfield_bounds.width());
190     int horizontal_padding = -((min_width - textfield_bounds.width()) / 2);
191     textfield_bounds.Inset(gfx::Insets(0, horizontal_padding));
192 
193     return textfield_bounds.Intersects(rect);
194   }
195 
196  private:
UpdateBackgroundColor(bool is_active)197   void UpdateBackgroundColor(bool is_active) {
198     background()->SetNativeControlColor(
199         AppListColorProvider::Get()->GetFolderNameBackgroundColor(is_active));
200     SchedulePaint();
201   }
202 
203   // The parent FolderHeaderView, owns this.
204   FolderHeaderView* folder_header_view_;
205 
206   // Name of the folder when FolderNameView is focused, used to track folder
207   // rename metric.
208   base::string16 starting_name_;
209 
210   // If the view is focused via a mouse press event, then selection will be
211   // cleared by its mouse release. To address this, defer selecting all
212   // until we receive mouse release.
213   bool defer_select_all_ = false;
214 
215   // Because of this view's custom event target, this view receives mouse enter
216   // events in areas where the view isn't actually occupying. To check whether a
217   // user has entered/exited this, we must check every mouse move event. This
218   // bool tracks whether the mouse has entered the view, avoiding repainting the
219   // background on each mouse move event.
220   bool has_mouse_already_entered_ = false;
221 
222   DISALLOW_COPY_AND_ASSIGN(FolderNameView);
223 };
224 
FolderHeaderView(FolderHeaderViewDelegate * delegate)225 FolderHeaderView::FolderHeaderView(FolderHeaderViewDelegate* delegate)
226     : folder_item_(nullptr),
227       folder_name_view_(new FolderNameView(this)),
228       folder_name_placeholder_text_(
229           ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
230               IDS_APP_LIST_FOLDER_NAME_PLACEHOLDER)),
231       delegate_(delegate),
232       folder_name_visible_(true),
233       is_tablet_mode_(false) {
234   folder_name_view_->SetPlaceholderText(folder_name_placeholder_text_);
235   folder_name_view_->set_controller(this);
236   AddChildView(folder_name_view_);
237 
238   SetPaintToLayer();
239   layer()->SetFillsBoundsOpaquely(false);
240 }
241 
~FolderHeaderView()242 FolderHeaderView::~FolderHeaderView() {
243   if (folder_item_)
244     folder_item_->RemoveObserver(this);
245 }
246 
SetFolderItem(AppListFolderItem * folder_item)247 void FolderHeaderView::SetFolderItem(AppListFolderItem* folder_item) {
248   if (folder_item_)
249     folder_item_->RemoveObserver(this);
250 
251   folder_item_ = folder_item;
252   if (!folder_item_)
253     return;
254   folder_item_->AddObserver(this);
255 
256   folder_name_view_->SetEnabled(folder_item_->folder_type() !=
257                                 AppListFolderItem::FOLDER_TYPE_OEM);
258 
259   Update();
260 }
261 
UpdateFolderNameVisibility(bool visible)262 void FolderHeaderView::UpdateFolderNameVisibility(bool visible) {
263   folder_name_visible_ = visible;
264   Update();
265   SchedulePaint();
266 }
267 
OnFolderItemRemoved()268 void FolderHeaderView::OnFolderItemRemoved() {
269   folder_item_ = nullptr;
270 }
271 
SetTextFocus()272 void FolderHeaderView::SetTextFocus() {
273   folder_name_view_->RequestFocus();
274 }
275 
HasTextFocus() const276 bool FolderHeaderView::HasTextFocus() const {
277   return folder_name_view_->HasFocus();
278 }
279 
Update()280 void FolderHeaderView::Update() {
281   if (!folder_item_)
282     return;
283 
284   folder_name_view_->SetVisible(folder_name_visible_);
285   if (folder_name_visible_) {
286     base::string16 folder_name = base::UTF8ToUTF16(folder_item_->name());
287     base::string16 elided_folder_name = GetElidedFolderName(folder_name);
288     folder_name_view_->SetText(elided_folder_name);
289     UpdateFolderNameAccessibleName();
290   }
291 
292   Layout();
293 }
294 
UpdateFolderNameAccessibleName()295 void FolderHeaderView::UpdateFolderNameAccessibleName() {
296   // Sets |folder_name_view_|'s accessible name to the placeholder text if
297   // |folder_name_view_| is blank; otherwise, clear the accessible name, the
298   // accessible state's value is set to be folder_name_view_->GetText() by
299   // TextField.
300   base::string16 accessible_name = folder_name_view_->GetText().empty()
301                                        ? folder_name_placeholder_text_
302                                        : base::string16();
303   folder_name_view_->SetAccessibleName(accessible_name);
304 }
305 
GetFolderNameForTest()306 const base::string16& FolderHeaderView::GetFolderNameForTest() {
307   return folder_name_view_->GetText();
308 }
309 
SetFolderNameForTest(const base::string16 & name)310 void FolderHeaderView::SetFolderNameForTest(const base::string16& name) {
311   folder_name_view_->SetText(name);
312 }
313 
IsFolderNameEnabledForTest() const314 bool FolderHeaderView::IsFolderNameEnabledForTest() const {
315   return folder_name_view_->GetEnabled();
316 }
317 
CalculatePreferredSize() const318 gfx::Size FolderHeaderView::CalculatePreferredSize() const {
319   return gfx::Size(AppListConfig::instance().folder_header_max_width(),
320                    folder_name_view_->GetPreferredSize().height());
321 }
322 
GetClassName() const323 const char* FolderHeaderView::GetClassName() const {
324   return "FolderHeaderView";
325 }
326 
OnBoundsChanged(const gfx::Rect & previous_bounds)327 void FolderHeaderView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
328   Update();
329 }
330 
GetFolderNameViewForTest() const331 views::View* FolderHeaderView::GetFolderNameViewForTest() const {
332   return folder_name_view_;
333 }
334 
GetMaxFolderNameWidth() const335 int FolderHeaderView::GetMaxFolderNameWidth() const {
336   return AppListConfig::instance().folder_header_max_width();
337 }
338 
GetElidedFolderName(const base::string16 & folder_name) const339 base::string16 FolderHeaderView::GetElidedFolderName(
340     const base::string16& folder_name) const {
341   // Enforce the maximum folder name length.
342   base::string16 name =
343       folder_name.substr(0, AppListConfig::instance().max_folder_name_chars());
344 
345   // Get maximum text width for fitting into |folder_name_view_|.
346   int text_width = std::min(GetMaxFolderNameWidth(), width()) -
347                    folder_name_view_->GetCaretBounds().width() -
348                    folder_name_view_->GetInsets().width();
349   base::string16 elided_name = gfx::ElideText(
350       name, folder_name_view_->GetFontList(), text_width, gfx::ELIDE_TAIL);
351   return elided_name;
352 }
353 
Layout()354 void FolderHeaderView::Layout() {
355   gfx::Rect rect(GetContentsBounds());
356   if (rect.IsEmpty())
357     return;
358 
359   gfx::Rect text_bounds(rect);
360 
361   base::string16 text = folder_name_view_->GetText().empty()
362                             ? folder_name_placeholder_text_
363                             : folder_name_view_->GetText();
364   int text_width =
365       gfx::Canvas::GetStringWidth(text, folder_name_view_->GetFontList()) +
366       folder_name_view_->GetCaretBounds().width() +
367       folder_name_view_->GetInsets().width();
368   text_width =
369       std::min(text_width, AppListConfig::instance().folder_header_max_width());
370   text_width =
371       std::max(text_width, AppListConfig::instance().folder_header_min_width());
372   text_bounds.set_x(std::max(0, rect.x() + (rect.width() - text_width) / 2));
373   text_bounds.set_width(std::min(rect.width(), text_width));
374 
375   text_bounds.ClampToCenteredSize(gfx::Size(
376       text_bounds.width(), folder_name_view_->GetPreferredSize().height()));
377   folder_name_view_->SetBoundsRect(text_bounds);
378 }
379 
ContentsChanged(views::Textfield * sender,const base::string16 & new_contents)380 void FolderHeaderView::ContentsChanged(views::Textfield* sender,
381                                        const base::string16& new_contents) {
382   // Temporarily remove from observer to ignore data change caused by us.
383   if (!folder_item_)
384     return;
385 
386   folder_item_->RemoveObserver(this);
387   // Enforce the maximum folder name length in UI.
388   if (new_contents.length() >
389       AppListConfig::instance().max_folder_name_chars()) {
390     folder_name_view_->SetText(previous_folder_name_.value());
391     sender->SetSelectedRange(gfx::Range(previous_cursor_position_.value(),
392                                         previous_cursor_position_.value()));
393   } else {
394     previous_folder_name_ = new_contents;
395     delegate_->SetItemName(folder_item_, base::UTF16ToUTF8(new_contents));
396   }
397 
398   folder_item_->AddObserver(this);
399 
400   UpdateFolderNameAccessibleName();
401 
402   Layout();
403 }
404 
ShouldNameViewClearFocus(const ui::KeyEvent & key_event)405 bool FolderHeaderView::ShouldNameViewClearFocus(const ui::KeyEvent& key_event) {
406   return key_event.type() == ui::ET_KEY_PRESSED &&
407          (key_event.key_code() == ui::VKEY_RETURN ||
408           key_event.key_code() == ui::VKEY_ESCAPE);
409 }
410 
HandleKeyEvent(views::Textfield * sender,const ui::KeyEvent & key_event)411 bool FolderHeaderView::HandleKeyEvent(views::Textfield* sender,
412                                       const ui::KeyEvent& key_event) {
413   if (ShouldNameViewClearFocus(key_event)) {
414     folder_name_view_->GetFocusManager()->ClearFocus();
415     return true;
416   }
417   if (!IsUnhandledLeftRightKeyEvent(key_event))
418     return false;
419   return ProcessLeftRightKeyTraversalForTextfield(folder_name_view_, key_event);
420 }
421 
OnBeforeUserAction(views::Textfield * sender)422 void FolderHeaderView::OnBeforeUserAction(views::Textfield* sender) {
423   previous_cursor_position_ = sender->GetCursorPosition();
424 }
425 
ItemNameChanged()426 void FolderHeaderView::ItemNameChanged() {
427   Update();
428 }
429 
SetPreviousCursorPositionForTest(const size_t cursor_position)430 void FolderHeaderView::SetPreviousCursorPositionForTest(
431     const size_t cursor_position) {
432   previous_cursor_position_ = cursor_position;
433 }
434 
SetPreviousFolderNameForTest(const base::string16 & previous_name)435 void FolderHeaderView::SetPreviousFolderNameForTest(
436     const base::string16& previous_name) {
437   previous_folder_name_ = previous_name;
438 }
439 
440 }  // namespace ash
441