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