folder_header_view.cc 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. #include "ash/app_list/views/folder_header_view.h"
  5. #include <algorithm>
  6. #include <memory>
  7. #include "ash/app_list/app_list_util.h"
  8. #include "ash/app_list/model/app_list_folder_item.h"
  9. #include "ash/app_list/views/app_list_folder_view.h"
  10. #include "ash/public/cpp/app_list/app_list_color_provider.h"
  11. #include "ash/public/cpp/app_list/app_list_config.h"
  12. #include "ash/public/cpp/app_list/app_list_features.h"
  13. #include "ash/public/cpp/app_list/app_list_switches.h"
  14. #include "ash/strings/grit/ash_strings.h"
  15. #include "base/metrics/histogram_macros.h"
  16. #include "base/strings/utf_string_conversions.h"
  17. #include "ui/base/cursor/cursor.h"
  18. #include "ui/base/resource/resource_bundle.h"
  19. #include "ui/compositor/layer.h"
  20. #include "ui/gfx/canvas.h"
  21. #include "ui/gfx/text_elider.h"
  22. #include "ui/views/background.h"
  23. #include "ui/views/border.h"
  24. #include "ui/views/controls/button/image_button.h"
  25. #include "ui/views/controls/textfield/textfield.h"
  26. #include "ui/views/focus/focus_manager.h"
  27. #include "ui/views/painter.h"
  28. #include "ui/views/view_targeter_delegate.h"
  29. namespace ash {
  30. namespace {
  31. // The max folder name length.
  32. constexpr int kMaxFolderNameChars = 28;
  33. // Folder header dimensions. The max header width is based on the width of a
  34. // folder with 2 items.
  35. constexpr int kMinFolderHeaderWidth = 24;
  36. constexpr int kMaxFolderHeaderWidth = 168;
  37. constexpr int kFolderHeaderHeight = 32;
  38. // The min width of folder name - ensures the folder name is easily tappable.
  39. constexpr int kFolderHeaderMinTapWidth = 32;
  40. // The border radius for folder name.
  41. constexpr int kFolderNameBorderRadius = 4;
  42. // The border thickness for folder name.
  43. constexpr int kFolderNameBorderThickness = 2;
  44. // The inner padding for folder name.
  45. constexpr int kFolderNamePadding = 8;
  46. SkColor GetFolderBackgroundColor(bool is_active) {
  47. if (!is_active)
  48. return SK_ColorTRANSPARENT;
  49. const AppListColorProvider* color_provider = AppListColorProvider::Get();
  50. return SkColorSetA(color_provider->GetInkDropBaseColor(),
  51. color_provider->GetInkDropOpacity() * 255);
  52. }
  53. } // namespace
  54. class FolderHeaderView::FolderNameView : public views::Textfield,
  55. public views::ViewTargeterDelegate {
  56. public:
  57. explicit FolderNameView(FolderHeaderView* folder_header_view)
  58. : folder_header_view_(folder_header_view) {
  59. DCHECK(folder_header_view_);
  60. // Make folder name font size 14px.
  61. SetFontList(
  62. ui::ResourceBundle::GetSharedInstance().GetFontListWithDelta(2));
  63. set_placeholder_text_color(
  64. AppListColorProvider::Get()->GetFolderHintTextColor());
  65. SetTextColor(AppListColorProvider::Get()->GetFolderTitleTextColor());
  66. SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
  67. }
  68. FolderNameView(const FolderNameView&) = delete;
  69. FolderNameView& operator=(const FolderNameView&) = delete;
  70. ~FolderNameView() override = default;
  71. gfx::Size CalculatePreferredSize() const override {
  72. return gfx::Size(kMaxFolderHeaderWidth, kFolderHeaderHeight);
  73. }
  74. void OnThemeChanged() override {
  75. Textfield::OnThemeChanged();
  76. const bool is_active = has_mouse_already_entered_ || HasFocus();
  77. SetBackground(views::CreateRoundedRectBackground(
  78. GetFolderBackgroundColor(is_active), kFolderNameBorderRadius,
  79. kFolderNameBorderThickness));
  80. AppListColorProvider* color_provider = AppListColorProvider::Get();
  81. set_placeholder_text_color(color_provider->GetFolderHintTextColor());
  82. const SkColor text_color = color_provider->GetFolderTitleTextColor();
  83. SetTextColor(text_color);
  84. SetSelectionTextColor(text_color);
  85. SetSelectionBackgroundColor(color_provider->GetFolderNameSelectionColor());
  86. SetNameViewBorderAndBackground(is_active);
  87. }
  88. ui::Cursor GetCursor(const ui::MouseEvent& event) override {
  89. return ui::mojom::CursorType::kIBeam;
  90. }
  91. void SetNameViewBorderAndBackground(bool is_active) {
  92. SetBorder(views::CreatePaddedBorder(
  93. views::CreateRoundedRectBorder(
  94. kFolderNameBorderThickness, kFolderNameBorderRadius,
  95. AppListColorProvider::Get()->GetFolderNameBorderColor(is_active)),
  96. gfx::Insets::VH(0, kFolderNamePadding)));
  97. UpdateBackgroundColor(is_active);
  98. }
  99. void OnFocus() override {
  100. SetNameViewBorderAndBackground(/*is_active=*/true);
  101. SetText(folder_header_view_->GetFolderName());
  102. starting_name_ = GetText();
  103. folder_header_view_->previous_folder_name_ = starting_name_;
  104. if (!defer_select_all_)
  105. SelectAll(false);
  106. Textfield::OnFocus();
  107. }
  108. void OnBlur() override {
  109. SetNameViewBorderAndBackground(/*is_active=*/false);
  110. // Collapse whitespace when FolderNameView loses focus.
  111. folder_header_view_->ContentsChanged(
  112. this, base::CollapseWhitespace(GetText(), false));
  113. // Ensure folder name is truncated when FolderNameView loses focus.
  114. SetText(folder_header_view_->GetElidedFolderName());
  115. // Record metric each time a folder is renamed.
  116. if (GetText() != starting_name_) {
  117. if (folder_header_view_->is_tablet_mode()) {
  118. UMA_HISTOGRAM_COUNTS_100("Apps.AppListFolderNameLength.TabletMode",
  119. GetText().length());
  120. } else {
  121. UMA_HISTOGRAM_COUNTS_100("Apps.AppListFolderNameLength.ClamshellMode",
  122. GetText().length());
  123. }
  124. }
  125. defer_select_all_ = false;
  126. Textfield::OnBlur();
  127. }
  128. bool DoesMouseEventActuallyIntersect(const ui::MouseEvent& event) {
  129. // Since hitbox for this view is extended for tap, we need to manually
  130. // calculate this when checking for mouse events.
  131. return GetLocalBounds().Contains(event.location());
  132. }
  133. bool OnMousePressed(const ui::MouseEvent& event) override {
  134. // Since hovering changes the background color, only taps should be
  135. // triggered using the extended event target.
  136. if (!DoesMouseEventActuallyIntersect(event))
  137. return false;
  138. if (!HasFocus())
  139. defer_select_all_ = true;
  140. return Textfield::OnMousePressed(event);
  141. }
  142. void OnMouseExited(const ui::MouseEvent& event) override {
  143. if (!HasFocus())
  144. UpdateBackgroundColor(/*is_active=*/false);
  145. has_mouse_already_entered_ = false;
  146. }
  147. void OnMouseMoved(const ui::MouseEvent& event) override {
  148. if (DoesMouseEventActuallyIntersect(event) && !has_mouse_already_entered_) {
  149. // If this is reached, the mouse is entering the view.
  150. // Recreate border to have custom corner radius.
  151. UpdateBackgroundColor(/*is_active=*/true);
  152. has_mouse_already_entered_ = true;
  153. } else if (!DoesMouseEventActuallyIntersect(event) &&
  154. has_mouse_already_entered_ && !HasFocus()) {
  155. // If this is reached, the mouse is exiting the view on its horizontal
  156. // edges.
  157. UpdateBackgroundColor(/*is_active=*/false);
  158. has_mouse_already_entered_ = false;
  159. }
  160. }
  161. void OnMouseReleased(const ui::MouseEvent& event) override {
  162. if (defer_select_all_) {
  163. defer_select_all_ = false;
  164. if (!HasSelection())
  165. SelectAll(false);
  166. }
  167. Textfield::OnMouseReleased(event);
  168. }
  169. bool DoesIntersectRect(const views::View* target,
  170. const gfx::Rect& rect) const override {
  171. DCHECK_EQ(target, this);
  172. gfx::Rect textfield_bounds = target->GetLocalBounds();
  173. // Ensure that the tap target for this view is always at least the view's
  174. // minimum width.
  175. int min_width =
  176. std::max(kFolderHeaderMinTapWidth, textfield_bounds.width());
  177. int horizontal_padding = -((min_width - textfield_bounds.width()) / 2);
  178. textfield_bounds.Inset(gfx::Insets::VH(0, horizontal_padding));
  179. return textfield_bounds.Intersects(rect);
  180. }
  181. private:
  182. void UpdateBackgroundColor(bool is_active) {
  183. background()->SetNativeControlColor(GetFolderBackgroundColor(is_active));
  184. SchedulePaint();
  185. }
  186. // The parent FolderHeaderView, owns this.
  187. FolderHeaderView* const folder_header_view_;
  188. // Name of the folder when FolderNameView is focused, used to track folder
  189. // rename metric.
  190. std::u16string starting_name_;
  191. // If the view is focused via a mouse press event, then selection will be
  192. // cleared by its mouse release. To address this, defer selecting all
  193. // until we receive mouse release.
  194. bool defer_select_all_ = false;
  195. // Because of this view's custom event target, this view receives mouse enter
  196. // events in areas where the view isn't actually occupying. To check whether a
  197. // user has entered/exited this, we must check every mouse move event. This
  198. // bool tracks whether the mouse has entered the view, avoiding repainting the
  199. // background on each mouse move event.
  200. bool has_mouse_already_entered_ = false;
  201. };
  202. FolderHeaderView::FolderHeaderView(FolderHeaderViewDelegate* delegate)
  203. : folder_item_(nullptr),
  204. folder_name_placeholder_text_(
  205. ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
  206. IDS_APP_LIST_FOLDER_NAME_PLACEHOLDER)),
  207. delegate_(delegate),
  208. folder_name_visible_(true),
  209. is_tablet_mode_(false) {
  210. folder_name_view_ = AddChildView(std::make_unique<FolderNameView>(this));
  211. folder_name_view_->SetPlaceholderText(folder_name_placeholder_text_);
  212. folder_name_view_->set_controller(this);
  213. SetPaintToLayer();
  214. layer()->SetFillsBoundsOpaquely(false);
  215. }
  216. FolderHeaderView::~FolderHeaderView() {
  217. if (folder_item_)
  218. folder_item_->RemoveObserver(this);
  219. }
  220. void FolderHeaderView::SetFolderItem(AppListFolderItem* folder_item) {
  221. if (folder_item_)
  222. folder_item_->RemoveObserver(this);
  223. folder_item_ = folder_item;
  224. if (!folder_item_)
  225. return;
  226. folder_item_->AddObserver(this);
  227. folder_name_view_->SetEnabled(folder_item_->folder_type() !=
  228. AppListFolderItem::FOLDER_TYPE_OEM);
  229. Update();
  230. }
  231. void FolderHeaderView::UpdateFolderNameVisibility(bool visible) {
  232. folder_name_visible_ = visible;
  233. Update();
  234. SchedulePaint();
  235. }
  236. void FolderHeaderView::SetTextFocus() {
  237. folder_name_view_->RequestFocus();
  238. }
  239. bool FolderHeaderView::HasTextFocus() const {
  240. return folder_name_view_->HasFocus();
  241. }
  242. void FolderHeaderView::Update() {
  243. if (!folder_item_)
  244. return;
  245. folder_name_view_->SetVisible(folder_name_visible_);
  246. if (folder_name_visible_) {
  247. std::u16string elided_folder_name = GetElidedFolderName();
  248. folder_name_view_->SetText(elided_folder_name);
  249. UpdateFolderNameAccessibleName();
  250. }
  251. Layout();
  252. }
  253. void FolderHeaderView::UpdateFolderNameAccessibleName() {
  254. // Sets |folder_name_view_|'s accessible name to the placeholder text if
  255. // |folder_name_view_| is blank; otherwise, clear the accessible name, the
  256. // accessible state's value is set to be folder_name_view_->GetText() by
  257. // TextField.
  258. std::u16string accessible_name = folder_name_view_->GetText().empty()
  259. ? folder_name_placeholder_text_
  260. : std::u16string();
  261. folder_name_view_->SetAccessibleName(accessible_name);
  262. }
  263. const std::u16string& FolderHeaderView::GetFolderNameForTest() {
  264. return folder_name_view_->GetText();
  265. }
  266. void FolderHeaderView::SetFolderNameForTest(const std::u16string& name) {
  267. folder_name_view_->SetText(name);
  268. }
  269. bool FolderHeaderView::IsFolderNameEnabledForTest() const {
  270. return folder_name_view_->GetEnabled();
  271. }
  272. gfx::Size FolderHeaderView::CalculatePreferredSize() const {
  273. return gfx::Size(kMaxFolderHeaderWidth,
  274. folder_name_view_->GetPreferredSize().height());
  275. }
  276. const char* FolderHeaderView::GetClassName() const {
  277. return "FolderHeaderView";
  278. }
  279. void FolderHeaderView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  280. Update();
  281. }
  282. views::Textfield* FolderHeaderView::GetFolderNameViewForTest() const {
  283. return folder_name_view_;
  284. }
  285. int FolderHeaderView::GetMaxFolderNameCharLengthForTest() const {
  286. return kMaxFolderNameChars;
  287. }
  288. std::u16string FolderHeaderView::GetFolderName() const {
  289. if (!folder_item_)
  290. return std::u16string();
  291. return base::UTF8ToUTF16(folder_item_->name());
  292. }
  293. std::u16string FolderHeaderView::GetElidedFolderName() const {
  294. if (!folder_item_)
  295. return std::u16string();
  296. // Enforce the maximum folder name length.
  297. std::u16string folder_name = GetFolderName();
  298. std::u16string name = folder_name.substr(0, kMaxFolderNameChars);
  299. // Get maximum text width for fitting into |folder_name_view_|.
  300. int text_width = std::min(kMaxFolderHeaderWidth, width()) -
  301. folder_name_view_->GetCaretBounds().width() -
  302. folder_name_view_->GetInsets().width();
  303. std::u16string elided_name = gfx::ElideText(
  304. name, folder_name_view_->GetFontList(), text_width, gfx::ELIDE_TAIL);
  305. return elided_name;
  306. }
  307. void FolderHeaderView::Layout() {
  308. gfx::Rect rect(GetContentsBounds());
  309. if (rect.IsEmpty())
  310. return;
  311. gfx::Rect text_bounds(rect);
  312. std::u16string text = folder_name_view_->GetText().empty()
  313. ? folder_name_placeholder_text_
  314. : folder_name_view_->GetText();
  315. int text_width =
  316. gfx::Canvas::GetStringWidth(text, folder_name_view_->GetFontList()) +
  317. folder_name_view_->GetCaretBounds().width() +
  318. folder_name_view_->GetInsets().width();
  319. text_width = std::min(text_width, kMaxFolderHeaderWidth);
  320. text_width = std::max(text_width, kMinFolderHeaderWidth);
  321. text_bounds.set_x(std::max(0, rect.x() + (rect.width() - text_width) / 2));
  322. text_bounds.set_width(std::min(rect.width(), text_width));
  323. text_bounds.ClampToCenteredSize(gfx::Size(
  324. text_bounds.width(), folder_name_view_->GetPreferredSize().height()));
  325. folder_name_view_->SetBoundsRect(text_bounds);
  326. }
  327. void FolderHeaderView::ContentsChanged(views::Textfield* sender,
  328. const std::u16string& new_contents) {
  329. // Temporarily remove from observer to ignore data change caused by us.
  330. if (!folder_item_)
  331. return;
  332. folder_item_->RemoveObserver(this);
  333. // Enforce the maximum folder name length in UI by trimming `new_contents`
  334. // when it is longer than the max length.
  335. if (new_contents.length() > kMaxFolderNameChars) {
  336. std::u16string trimmed_new_contents = new_contents;
  337. trimmed_new_contents.resize(kMaxFolderNameChars);
  338. folder_name_view_->SetText(trimmed_new_contents);
  339. } else {
  340. delegate_->SetItemName(folder_item_, base::UTF16ToUTF8(new_contents));
  341. }
  342. folder_item_->AddObserver(this);
  343. UpdateFolderNameAccessibleName();
  344. Layout();
  345. }
  346. bool FolderHeaderView::ShouldNameViewClearFocus(const ui::KeyEvent& key_event) {
  347. return key_event.type() == ui::ET_KEY_PRESSED &&
  348. (key_event.key_code() == ui::VKEY_RETURN ||
  349. key_event.key_code() == ui::VKEY_ESCAPE);
  350. }
  351. bool FolderHeaderView::HandleKeyEvent(views::Textfield* sender,
  352. const ui::KeyEvent& key_event) {
  353. if (ShouldNameViewClearFocus(key_event)) {
  354. // If the user presses the escape key, we should revert the text in
  355. // `folder_name_view_`.
  356. if (key_event.key_code() == ui::VKEY_ESCAPE)
  357. sender->SetText(previous_folder_name_);
  358. folder_name_view_->GetFocusManager()->ClearFocus();
  359. return true;
  360. }
  361. if (!IsUnhandledLeftRightKeyEvent(key_event))
  362. return false;
  363. return ProcessLeftRightKeyTraversalForTextfield(folder_name_view_, key_event);
  364. }
  365. void FolderHeaderView::ItemNameChanged() {
  366. Update();
  367. }
  368. } // namespace ash