search_result_view.cc 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  1. // Copyright (c) 2012 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/search_result_view.h"
  5. #include <algorithm>
  6. #include <memory>
  7. #include <utility>
  8. #include "ash/app_list/app_list_metrics.h"
  9. #include "ash/app_list/app_list_util.h"
  10. #include "ash/app_list/app_list_view_delegate.h"
  11. #include "ash/app_list/views/app_list_main_view.h"
  12. #include "ash/app_list/views/contents_view.h"
  13. #include "ash/app_list/views/legacy_remove_query_confirmation_dialog.h"
  14. #include "ash/app_list/views/remove_query_confirmation_dialog.h"
  15. #include "ash/app_list/views/search_box_view.h"
  16. #include "ash/app_list/views/search_result_actions_view.h"
  17. #include "ash/app_list/views/search_result_inline_icon_view.h"
  18. #include "ash/app_list/views/search_result_list_view.h"
  19. #include "ash/app_list/views/search_result_page_view.h"
  20. #include "ash/constants/ash_features.h"
  21. #include "ash/public/cpp/app_list/app_list_color_provider.h"
  22. #include "ash/public/cpp/app_list/app_list_config.h"
  23. #include "ash/public/cpp/app_list/app_list_features.h"
  24. #include "ash/public/cpp/app_list/app_list_switches.h"
  25. #include "ash/public/cpp/app_list/app_list_types.h"
  26. #include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
  27. #include "ash/public/cpp/ash_typography.h"
  28. #include "ash/strings/grit/ash_strings.h"
  29. #include "ash/style/ash_color_provider.h"
  30. #include "base/bind.h"
  31. #include "base/i18n/number_formatting.h"
  32. #include "ui/accessibility/ax_node_data.h"
  33. #include "ui/base/l10n/l10n_util.h"
  34. #include "ui/compositor/layer.h"
  35. #include "ui/gfx/canvas.h"
  36. #include "ui/gfx/color_palette.h"
  37. #include "ui/gfx/font.h"
  38. #include "ui/gfx/geometry/skia_conversions.h"
  39. #include "ui/gfx/image/image_skia_operations.h"
  40. #include "ui/gfx/paint_vector_icon.h"
  41. #include "ui/views/accessibility/accessibility_paint_checks.h"
  42. #include "ui/views/accessibility/view_accessibility.h"
  43. #include "ui/views/controls/button/image_button.h"
  44. #include "ui/views/controls/image_view.h"
  45. #include "ui/views/controls/label.h"
  46. #include "ui/views/layout/flex_layout.h"
  47. #include "ui/views/layout/flex_layout_view.h"
  48. #include "ui/views/style/typography.h"
  49. namespace ash {
  50. namespace {
  51. constexpr int kBadgeIconShadowWidth = 1;
  52. constexpr int kPreferredWidth = 640;
  53. constexpr int kMultilineLabelWidth = 544;
  54. constexpr int kClassicViewHeight = 48;
  55. constexpr int kDefaultViewHeight = 40;
  56. constexpr int kDefaultAnswerCardViewHeight = 80;
  57. constexpr int kKeyboardShortcutViewHeight = 64;
  58. constexpr int kPreferredIconViewWidth = 56;
  59. constexpr int kTextTrailPadding = 16;
  60. // Extra margin at the right of the rightmost action icon.
  61. constexpr int kClassicActionButtonRightMargin = 8;
  62. // Extra margin at the right of the rightmost action icon.
  63. constexpr int kDefaultActionButtonRightMargin = 12;
  64. // Text line height in the search result.
  65. constexpr int kPrimaryTextHeight = 20;
  66. constexpr int kAnswerCardDetailsLineHeight = 18;
  67. constexpr int kAnswerCardCardBackgroundCornerRadius = 12;
  68. constexpr int kAnswerCardFocusBarHorizontalOffset = 12;
  69. constexpr int kAnswerCardFocusBarVerticalOffset = 24;
  70. // Corner radius for downloaded image icons.
  71. constexpr int kImageIconCornerRadius = 4;
  72. // The maximum number of lines that can be shown in the details text.
  73. constexpr int kMultiLineLimit = 3;
  74. // Flex layout orders detailing how container views are prioritized.
  75. constexpr int kSeparatorOrder = 1;
  76. constexpr int kRatingOrder = 1;
  77. constexpr int TitleDetailContainerOrder = 1;
  78. constexpr int kTitleDetailsLabelOrderNoElide = 1;
  79. constexpr int kTitleDetailsLabelOrderElide = 2;
  80. // Non-elidable labels are of order 1 to prioritize them.
  81. constexpr int kNonElideLabelOrder = 1;
  82. // Elidable labels are assigned monotonically increasing orders to prioritize
  83. // items that appear close to the front of the TextVector.
  84. constexpr int kElidableLabelOrderStart = 2;
  85. constexpr int kSearchRatingStarPadding = 4;
  86. constexpr int kSearchRatingStarSize = 16;
  87. constexpr int kKeyboardShortcutTopMargin = 6;
  88. constexpr int kAnswerCardBorderMargin = 12;
  89. constexpr gfx::Insets kAnswerCardBorder(kAnswerCardBorderMargin);
  90. // The superscript container has a 3px top margin to shift the text up so the
  91. // it lines up with the text in `big_title_main_text_container_`.
  92. constexpr auto kBigTitleSuperscriptBorder =
  93. gfx::Insets::TLBR(3, 4, 0, kAnswerCardBorderMargin);
  94. // The fraction of total text space allocated to the details label when both the
  95. // title and the details label need to be elided.
  96. constexpr float kDetailsElideRatio = 0.25f;
  97. views::ImageView* SetupChildImageView(views::FlexLayoutView* parent) {
  98. views::ImageView* image_view =
  99. parent->AddChildView(std::make_unique<views::ImageView>());
  100. image_view->GetViewAccessibility().OverrideIsIgnored(true);
  101. image_view->SetCanProcessEventsWithinSubtree(false);
  102. image_view->SetVerticalAlignment(views::ImageView::Alignment::kCenter);
  103. image_view->SetVisible(false);
  104. return image_view;
  105. }
  106. views::Label* SetupChildLabelView(
  107. views::FlexLayoutView* parent,
  108. SearchResultView::SearchResultViewType view_type,
  109. SearchResultView::LabelType label_type,
  110. int flex_order,
  111. bool has_keyboard_shortcut_contents,
  112. bool is_multi_line,
  113. SearchResultTextItem::OverflowBehavior overflow_behavior) {
  114. // Create and setup label.
  115. views::Label* label = parent->AddChildView(std::make_unique<views::Label>());
  116. // Ignore labels for accessibility - the result accessible name is defined on
  117. // the whole result view.
  118. label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  119. label->GetViewAccessibility().OverrideIsIgnored(true);
  120. label->SetBackgroundColor(SK_ColorTRANSPARENT);
  121. label->SetVisible(false);
  122. label->SetElideBehavior(overflow_behavior ==
  123. SearchResultTextItem::OverflowBehavior::kElide
  124. ? gfx::ELIDE_TAIL
  125. : gfx::NO_ELIDE);
  126. label->SetMultiLine(is_multi_line);
  127. if (is_multi_line)
  128. label->SetMaxLines(kMultiLineLimit);
  129. label->SetProperty(
  130. views::kFlexBehaviorKey,
  131. views::FlexSpecification(
  132. overflow_behavior == SearchResultTextItem::OverflowBehavior::kHide
  133. ? views::MinimumFlexSizeRule::kPreferredSnapToZero
  134. : views::MinimumFlexSizeRule::kScaleToMinimum,
  135. views::MaximumFlexSizeRule::kPreferred)
  136. .WithOrder(flex_order));
  137. // Apply label text styling.
  138. ash::AshTextContext text_context;
  139. switch (label_type) {
  140. case SearchResultView::LabelType::kBigTitle:
  141. text_context = CONTEXT_SEARCH_RESULT_BIG_TITLE;
  142. break;
  143. case SearchResultView::LabelType::kBigTitleSuperscript:
  144. // kBigTitleSuperscript labels are top-aligned to support superscripting.
  145. label->SetVerticalAlignment(gfx::ALIGN_TOP);
  146. text_context = CONTEXT_SEARCH_RESULT_BIG_TITLE_SUPERSCRIPT;
  147. break;
  148. case SearchResultView::LabelType::kTitle:
  149. text_context = CONTEXT_SEARCH_RESULT_VIEW;
  150. break;
  151. case SearchResultView::LabelType::kDetails:
  152. // has_keyboard_shortcut_contents forces inline title and details text for
  153. // answer cards so title and details text should use the same context.
  154. if (view_type == SearchResultView::SearchResultViewType::kAnswerCard &&
  155. !has_keyboard_shortcut_contents) {
  156. text_context = CONTEXT_SEARCH_RESULT_VIEW_INLINE_ANSWER_DETAILS;
  157. } else {
  158. text_context = CONTEXT_SEARCH_RESULT_VIEW;
  159. }
  160. break;
  161. case SearchResultView::LabelType::kKeyboardShortcut:
  162. text_context = CONTEXT_SEARCH_RESULT_VIEW;
  163. break;
  164. }
  165. label->SetTextContext(text_context);
  166. switch (view_type) {
  167. case SearchResultView::SearchResultViewType::kClassic:
  168. label->SetTextStyle(STYLE_CLASSIC_LAUNCHER);
  169. break;
  170. case SearchResultView::SearchResultViewType::kDefault:
  171. case SearchResultView::SearchResultViewType::kAnswerCard:
  172. label->SetTextStyle(STYLE_PRODUCTIVITY_LAUNCHER);
  173. }
  174. return label;
  175. }
  176. SearchResultInlineIconView* SetupChildInlineIconView(
  177. views::FlexLayoutView* parent) {
  178. SearchResultInlineIconView* inline_icon_view =
  179. parent->AddChildView(std::make_unique<SearchResultInlineIconView>());
  180. inline_icon_view->SetCanProcessEventsWithinSubtree(false);
  181. inline_icon_view->GetViewAccessibility().OverrideIsIgnored(true);
  182. inline_icon_view->SetVisible(false);
  183. inline_icon_view->SetProperty(
  184. views::kFlexBehaviorKey,
  185. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  186. views::MaximumFlexSizeRule::kPreferred));
  187. return inline_icon_view;
  188. }
  189. } // namespace
  190. // static
  191. const char SearchResultView::kViewClassName[] = "ui/app_list/SearchResultView";
  192. // An ImageView that optionally masks the image into a circle or rectangle with
  193. // rounded corners.
  194. class MaskedImageView : public views::ImageView {
  195. public:
  196. MaskedImageView() = default;
  197. MaskedImageView(const MaskedImageView&) = delete;
  198. MaskedImageView& operator=(const MaskedImageView&) = delete;
  199. void set_shape(SearchResult::IconShape shape) {
  200. if (shape_ == shape)
  201. return;
  202. shape_ = shape;
  203. SchedulePaint();
  204. }
  205. protected:
  206. // views::ImageView:
  207. void OnPaint(gfx::Canvas* canvas) override {
  208. SkPath mask;
  209. const gfx::Rect& bounds = GetImageBounds();
  210. switch (shape_) {
  211. case SearchResult::IconShape::kDefault:
  212. case SearchResult::IconShape::kRectangle:
  213. // Noop.
  214. break;
  215. case SearchResult::IconShape::kRoundedRectangle:
  216. mask.addRoundRect(gfx::RectToSkRect(bounds), kImageIconCornerRadius,
  217. kImageIconCornerRadius);
  218. canvas->ClipPath(mask, true);
  219. break;
  220. case SearchResult::IconShape::kCircle:
  221. // Calculate the radius of the circle based on the minimum of width and
  222. // height in case the icon isn't square.
  223. mask.addCircle(bounds.x() + bounds.width() / 2,
  224. bounds.y() + bounds.height() / 2,
  225. std::min(bounds.width(), bounds.height()) / 2);
  226. canvas->ClipPath(mask, true);
  227. break;
  228. }
  229. ImageView::OnPaint(canvas);
  230. }
  231. private:
  232. SearchResult::IconShape shape_;
  233. };
  234. SearchResultView::LabelAndTag::LabelAndTag(views::Label* label,
  235. SearchResult::Tags tags)
  236. : label_(label), tags_(tags) {}
  237. SearchResultView::LabelAndTag::LabelAndTag(
  238. const SearchResultView::LabelAndTag& other) = default;
  239. SearchResultView::LabelAndTag& SearchResultView::LabelAndTag::operator=(
  240. const SearchResultView::LabelAndTag& other) = default;
  241. SearchResultView::LabelAndTag::~LabelAndTag() = default;
  242. SearchResultView::SearchResultView(
  243. SearchResultListView* list_view,
  244. AppListViewDelegate* view_delegate,
  245. SearchResultPageDialogController* dialog_controller,
  246. SearchResultViewType view_type)
  247. : list_view_(list_view),
  248. view_delegate_(view_delegate),
  249. dialog_controller_(dialog_controller),
  250. view_type_(view_type) {
  251. // Result views are not expected to be focused - while the results UI is shown
  252. // the focus is kept within the `SearchBoxView`, which manages result
  253. // selection state in response to keyboard navigation keys, and forwards
  254. // all relevant key events (e.g. ENTER key for result activation) to search
  255. // result views as needed.
  256. SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
  257. // TODO(crbug.com/1218186): Remove this, this is in place temporarily to be
  258. // able to submit accessibility checks, but this focusable View needs to
  259. // add a name so that the screen reader knows what to announce.
  260. SetProperty(views::kSkipAccessibilityPaintChecks, true);
  261. SetCallback(base::BindRepeating(&SearchResultView::OnButtonPressed,
  262. base::Unretained(this)));
  263. icon_ = AddChildView(std::make_unique<MaskedImageView>());
  264. badge_icon_ = AddChildView(std::make_unique<views::ImageView>());
  265. auto* actions_view =
  266. AddChildView(std::make_unique<SearchResultActionsView>(this));
  267. set_actions_view(actions_view);
  268. icon_->SetCanProcessEventsWithinSubtree(false);
  269. icon_->GetViewAccessibility().OverrideIsIgnored(true);
  270. badge_icon_->SetCanProcessEventsWithinSubtree(false);
  271. badge_icon_->GetViewAccessibility().OverrideIsIgnored(true);
  272. SetNotifyEnterExitOnChild(true);
  273. text_container_ = AddChildView(std::make_unique<views::FlexLayoutView>());
  274. text_container_->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
  275. text_container_->SetOrientation(views::LayoutOrientation::kHorizontal);
  276. text_container_->SetProperty(
  277. views::kFlexBehaviorKey,
  278. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  279. views::MaximumFlexSizeRule::kPreferred));
  280. big_title_container_ =
  281. text_container_->AddChildView(std::make_unique<views::FlexLayoutView>());
  282. big_title_container_->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
  283. big_title_container_->SetOrientation(views::LayoutOrientation::kHorizontal);
  284. big_title_main_text_container_ = big_title_container_->AddChildView(
  285. std::make_unique<views::FlexLayoutView>());
  286. big_title_main_text_container_->SetCrossAxisAlignment(
  287. views::LayoutAlignment::kStretch);
  288. big_title_main_text_container_->SetOrientation(
  289. views::LayoutOrientation::kHorizontal);
  290. big_title_superscript_container_ = big_title_container_->AddChildView(
  291. std::make_unique<views::FlexLayoutView>());
  292. big_title_superscript_container_->SetCrossAxisAlignment(
  293. views::LayoutAlignment::kStretch);
  294. big_title_superscript_container_->SetOrientation(
  295. views::LayoutOrientation::kHorizontal);
  296. body_text_container_ =
  297. text_container_->AddChildView(std::make_unique<views::FlexLayoutView>());
  298. body_text_container_->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
  299. body_text_container_->SetOrientation(views::LayoutOrientation::kVertical);
  300. body_text_container_->SetProperty(
  301. views::kFlexBehaviorKey,
  302. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  303. views::MaximumFlexSizeRule::kPreferred));
  304. title_and_details_container_ = body_text_container_->AddChildView(
  305. std::make_unique<views::FlexLayoutView>());
  306. title_and_details_container_->SetCrossAxisAlignment(
  307. views::LayoutAlignment::kStretch);
  308. title_and_details_container_->SetProperty(
  309. views::kFlexBehaviorKey,
  310. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  311. views::MaximumFlexSizeRule::kUnbounded,
  312. /*adjust_height_for_width=*/true));
  313. SetSearchResultViewType(view_type_);
  314. title_container_ = title_and_details_container_->AddChildView(
  315. std::make_unique<views::FlexLayoutView>());
  316. title_container_->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
  317. title_container_->SetOrientation(views::LayoutOrientation::kHorizontal);
  318. title_container_->SetProperty(
  319. views::kFlexBehaviorKey,
  320. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  321. views::MaximumFlexSizeRule::kPreferred)
  322. .WithOrder(TitleDetailContainerOrder)
  323. .WithWeight(1));
  324. title_container_->SetFlexAllocationOrder(
  325. views::FlexAllocationOrder::kReverse);
  326. result_text_separator_label_ = SetupChildLabelView(
  327. title_and_details_container_, view_type_, LabelType::kDetails,
  328. kSeparatorOrder, has_keyboard_shortcut_contents_,
  329. /*is_multi_line=*/false,
  330. SearchResultTextItem::OverflowBehavior::kNoElide);
  331. result_text_separator_label_->SetText(
  332. l10n_util::GetStringUTF16(IDS_ASH_SEARCH_RESULT_SEPARATOR));
  333. result_text_separator_label_->GetViewAccessibility().OverrideIsIgnored(true);
  334. details_container_ = title_and_details_container_->AddChildView(
  335. std::make_unique<views::FlexLayoutView>());
  336. details_container_->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
  337. details_container_->SetOrientation(views::LayoutOrientation::kHorizontal);
  338. details_container_->SetProperty(
  339. views::kFlexBehaviorKey,
  340. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  341. views::MaximumFlexSizeRule::kUnbounded,
  342. /*adjust_height_for_width=*/true)
  343. .WithOrder(TitleDetailContainerOrder)
  344. .WithWeight(1));
  345. rating_separator_label_ = SetupChildLabelView(
  346. title_and_details_container_, view_type_, LabelType::kDetails,
  347. kSeparatorOrder, has_keyboard_shortcut_contents_,
  348. /*is_multi_line=*/false,
  349. SearchResultTextItem::OverflowBehavior::kNoElide);
  350. rating_separator_label_->SetText(
  351. l10n_util::GetStringUTF16(IDS_ASH_SEARCH_RESULT_SEPARATOR));
  352. rating_separator_label_->GetViewAccessibility().OverrideIsIgnored(true);
  353. rating_ = SetupChildLabelView(
  354. title_and_details_container_, view_type_, LabelType::kDetails,
  355. kRatingOrder, has_keyboard_shortcut_contents_,
  356. /*is_multi_line=*/false,
  357. SearchResultTextItem::OverflowBehavior::kNoElide);
  358. rating_star_ = SetupChildImageView(title_and_details_container_);
  359. rating_star_->SetImage(gfx::CreateVectorIcon(
  360. kBadgeRatingIcon, kSearchRatingStarSize,
  361. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  362. kDeprecatedSearchBoxTextDefaultColor)));
  363. rating_star_->SetBorder(views::CreateEmptyBorder(
  364. gfx::Insets::TLBR(0, kSearchRatingStarPadding, 0, 0)));
  365. keyboard_shortcut_container_ = body_text_container_->AddChildView(
  366. std::make_unique<views::FlexLayoutView>());
  367. keyboard_shortcut_container_->SetCrossAxisAlignment(
  368. views::LayoutAlignment::kStretch);
  369. keyboard_shortcut_container_->SetBorder(views::CreateEmptyBorder(
  370. gfx::Insets::TLBR(kKeyboardShortcutTopMargin, 0, 0, 0)));
  371. }
  372. SearchResultView::~SearchResultView() = default;
  373. void SearchResultView::OnResultChanged() {
  374. OnMetadataChanged();
  375. SchedulePaint();
  376. }
  377. void SearchResultView::SetSearchResultViewType(SearchResultViewType type) {
  378. view_type_ = type;
  379. switch (view_type_) {
  380. case SearchResultViewType::kDefault:
  381. title_and_details_container_->SetOrientation(
  382. views::LayoutOrientation::kHorizontal);
  383. ClearBigTitleContainer();
  384. break;
  385. case SearchResultViewType::kClassic:
  386. title_and_details_container_->SetOrientation(
  387. views::LayoutOrientation::kVertical);
  388. ClearBigTitleContainer();
  389. break;
  390. case SearchResultViewType::kAnswerCard:
  391. title_and_details_container_->SetOrientation(
  392. views::LayoutOrientation::kVertical);
  393. SetBorder(views::CreateEmptyBorder(kAnswerCardBorder));
  394. break;
  395. }
  396. }
  397. void SearchResultView::ClearBigTitleContainer() {
  398. SetBorder(views::CreateEmptyBorder(0));
  399. big_title_main_text_container_->RemoveAllChildViews();
  400. big_title_label_tags_.clear();
  401. big_title_main_text_container_->SetVisible(false);
  402. big_title_superscript_container_->RemoveAllChildViews();
  403. big_title_superscript_label_tags_.clear();
  404. big_title_superscript_container_->SetVisible(false);
  405. big_title_superscript_container_->SetBorder(views::CreateEmptyBorder(0));
  406. }
  407. views::LayoutOrientation SearchResultView::TitleAndDetailsOrientationForTest() {
  408. return title_and_details_container_->GetOrientation();
  409. }
  410. int SearchResultView::PreferredHeight() const {
  411. switch (view_type_) {
  412. case SearchResultViewType::kClassic:
  413. return kClassicViewHeight;
  414. case SearchResultViewType::kDefault:
  415. if (has_keyboard_shortcut_contents_)
  416. return kKeyboardShortcutViewHeight;
  417. return kDefaultViewHeight;
  418. case SearchResultViewType::kAnswerCard:
  419. if (multi_line_label_height_ > 0) {
  420. // kDefaultAnswerCardViewHeight is adjusted to accommodate multi-line
  421. // result's height. The assumed kAnswerCardDetailsLineHeight is replaced
  422. // with the multi-line label's height.
  423. return kDefaultAnswerCardViewHeight + multi_line_label_height_ -
  424. kAnswerCardDetailsLineHeight;
  425. }
  426. return kDefaultAnswerCardViewHeight;
  427. }
  428. }
  429. int SearchResultView::PrimaryTextHeight() const {
  430. switch (view_type_) {
  431. case SearchResultViewType::kClassic:
  432. case SearchResultViewType::kDefault:
  433. case SearchResultViewType::kAnswerCard:
  434. return kPrimaryTextHeight;
  435. }
  436. }
  437. int SearchResultView::SecondaryTextHeight() const {
  438. if (has_keyboard_shortcut_contents_)
  439. return kPrimaryTextHeight;
  440. if (multi_line_label_height_ > 0)
  441. return multi_line_label_height_;
  442. switch (view_type_) {
  443. case SearchResultViewType::kClassic:
  444. case SearchResultViewType::kAnswerCard:
  445. return kAnswerCardDetailsLineHeight;
  446. case SearchResultViewType::kDefault:
  447. return kPrimaryTextHeight;
  448. }
  449. }
  450. int SearchResultView::ActionButtonRightMargin() const {
  451. switch (view_type_) {
  452. case SearchResultViewType::kClassic:
  453. return kClassicActionButtonRightMargin;
  454. case SearchResultViewType::kAnswerCard:
  455. case SearchResultViewType::kDefault:
  456. return kDefaultActionButtonRightMargin;
  457. }
  458. }
  459. // static
  460. int SearchResultView::GetTargetTitleWidth(int total_width,
  461. int separator_width,
  462. int target_details_width) {
  463. // Allocate all remaining space to the title container.
  464. const int target_title_width =
  465. total_width - separator_width - target_details_width;
  466. DCHECK_GT(target_title_width, 0);
  467. return target_title_width;
  468. }
  469. // static
  470. int SearchResultView::GetMinimumDetailsWidth(int total_width,
  471. int details_width,
  472. int details_no_elide_width) {
  473. // Calculate the minimum width for the title and details containers
  474. // assuming both will need eliding.
  475. // We must allocate enough space to show the no_elide text. Otherwise
  476. // show the entire details text up to total_width*kDetailsElideRatio.
  477. const int target_details_width =
  478. std::max(details_no_elide_width,
  479. std::min(static_cast<int>(total_width * kDetailsElideRatio),
  480. details_width));
  481. DCHECK_GT(target_details_width, 0);
  482. return target_details_width;
  483. }
  484. // static
  485. void SearchResultView::SetFlexBehaviorForTextContents(
  486. int total_width,
  487. int separator_width,
  488. int non_elided_details_width,
  489. views::FlexLayoutView* title_container,
  490. views::FlexLayoutView* details_container) {
  491. const int title_width = title_container->GetPreferredSize().width();
  492. const int details_width = details_container->GetPreferredSize().width();
  493. // If the result view has enough space to accommodate text contents at their
  494. // preferred size, we don't need to elide either view. Set their weights to 1
  495. // and order to `kTitleDetailsLabelOrderNoElide`.
  496. if (title_width + details_width + separator_width <= total_width) {
  497. title_container->SetProperty(
  498. views::kFlexBehaviorKey,
  499. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  500. views::MaximumFlexSizeRule::kPreferred)
  501. .WithOrder(kTitleDetailsLabelOrderNoElide)
  502. .WithWeight(1));
  503. details_container->SetProperty(
  504. views::kFlexBehaviorKey,
  505. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  506. views::MaximumFlexSizeRule::kPreferred)
  507. .WithOrder(kTitleDetailsLabelOrderNoElide)
  508. .WithWeight(1));
  509. return;
  510. }
  511. const int min_details_width = GetMinimumDetailsWidth(
  512. total_width, details_width, non_elided_details_width);
  513. // If the result view has enough space to layout details to it's minimum size
  514. // after laying out the title, we should only take away space from the details
  515. // view. We do this by setting the details view to a lower order
  516. // `kTitleDetailsLabelOrderElide`.
  517. if (total_width - separator_width - title_width >= min_details_width) {
  518. title_container->SetProperty(
  519. views::kFlexBehaviorKey,
  520. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  521. views::MaximumFlexSizeRule::kPreferred)
  522. .WithOrder(kTitleDetailsLabelOrderNoElide)
  523. .WithWeight(1));
  524. details_container->SetProperty(
  525. views::kFlexBehaviorKey,
  526. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  527. views::MaximumFlexSizeRule::kPreferred)
  528. .WithOrder(kTitleDetailsLabelOrderElide)
  529. .WithWeight(1));
  530. return;
  531. }
  532. // If excess space needs to be taken from both the title and details view
  533. // to accommodate the minimum details view width, set flex weights to properly
  534. // take away space from the title and details view.
  535. const int target_title_width =
  536. GetTargetTitleWidth(total_width, separator_width, min_details_width);
  537. // Flex weights are set based on the amount of space that needs to be removed
  538. // from an associated view because flex layout *takes space away* based on
  539. // weight when it is unable to accommodate the view's preferred size.
  540. // calculate the number of px we need to take away from the title/details
  541. // text.
  542. const int detail_extra_space = details_width - min_details_width;
  543. const int title_extra_space = title_width - target_title_width;
  544. title_container->SetProperty(
  545. views::kFlexBehaviorKey,
  546. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  547. views::MaximumFlexSizeRule::kPreferred)
  548. .WithOrder(kTitleDetailsLabelOrderElide)
  549. .WithWeight(std::max(title_extra_space, 0)));
  550. details_container->SetProperty(
  551. views::kFlexBehaviorKey,
  552. views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
  553. views::MaximumFlexSizeRule::kPreferred)
  554. .WithOrder(kTitleDetailsLabelOrderElide)
  555. .WithWeight(std::max(detail_extra_space, 0)));
  556. }
  557. std::vector<SearchResultView::LabelAndTag>
  558. SearchResultView::SetupContainerViewForTextVector(
  559. views::FlexLayoutView* parent,
  560. const std::vector<SearchResult::TextItem>& text_vector,
  561. LabelType label_type,
  562. bool has_keyboard_shortcut_contents,
  563. bool is_multi_line) {
  564. std::vector<LabelAndTag> label_tags;
  565. int label_count = 0;
  566. for (auto& span : text_vector) {
  567. switch (span.GetType()) {
  568. case SearchResultTextItemType::kString: {
  569. bool elidable = span.GetOverflowBehavior() !=
  570. SearchResultTextItem::OverflowBehavior::kNoElide;
  571. views::Label* label = SetupChildLabelView(
  572. parent, view_type_, label_type,
  573. !elidable ? kNonElideLabelOrder
  574. : kElidableLabelOrderStart + label_count,
  575. has_keyboard_shortcut_contents,
  576. /*is_multi_line=*/is_multi_line, span.GetOverflowBehavior());
  577. // Elidable label orders are monotonically increasing. Adjust the order
  578. // of the label by the number of labels in this container.
  579. if (elidable) {
  580. ++label_count;
  581. }
  582. if (label_type == LabelType::kDetails) {
  583. // We should only show a separator label when the details container
  584. // has valid contents.
  585. should_show_result_text_separator_label_ =
  586. should_show_result_text_separator_label_ ||
  587. (!span.GetText().empty());
  588. }
  589. label->SetText(span.GetText());
  590. label->SetVisible(true);
  591. if (!elidable) {
  592. // Each search result can have up to one non-elided label in its
  593. // details text.
  594. DCHECK_EQ(label_type, LabelType::kDetails);
  595. non_elided_details_label_width_ = label->GetPreferredSize().width();
  596. }
  597. if (is_multi_line) {
  598. // Each search result can have up to one non-elided label in its
  599. // details text.
  600. DCHECK_EQ(label_type, LabelType::kDetails);
  601. multi_line_label_height_ =
  602. label->GetHeightForWidth(kMultilineLabelWidth);
  603. }
  604. label_tags.push_back(LabelAndTag(label, span.GetTextTags()));
  605. } break;
  606. case SearchResultTextItemType::kIconifiedText: {
  607. SearchResultInlineIconView* iconified_text_view =
  608. SetupChildInlineIconView(parent);
  609. iconified_text_view->SetText(span.GetText());
  610. iconified_text_view->SetVisible(true);
  611. } break;
  612. case SearchResultTextItemType::kIconCode: {
  613. SearchResultInlineIconView* icon_view =
  614. SetupChildInlineIconView(parent);
  615. icon_view->SetIcon(*span.GetIconFromCode());
  616. icon_view->SetVisible(true);
  617. } break;
  618. case SearchResultTextItemType::kCustomImage:
  619. break;
  620. }
  621. }
  622. return label_tags;
  623. }
  624. void SearchResultView::UpdateBadgeIcon() {
  625. if (!result() || result()->badge_icon().IsEmpty()) {
  626. badge_icon_->SetVisible(false);
  627. return;
  628. }
  629. gfx::ImageSkia badge_icon_skia =
  630. result()->badge_icon().Rasterize(GetColorProvider());
  631. if (result()->use_badge_icon_background()) {
  632. badge_icon_skia = CreateIconWithCircleBackground(badge_icon_skia);
  633. }
  634. gfx::ImageSkia resized_badge_icon(
  635. gfx::ImageSkiaOperations::CreateResizedImage(
  636. badge_icon_skia, skia::ImageOperations::RESIZE_BEST,
  637. SharedAppListConfig::instance().search_list_badge_icon_size()));
  638. gfx::ShadowValues shadow_values;
  639. shadow_values.push_back(
  640. gfx::ShadowValue(gfx::Vector2d(0, kBadgeIconShadowWidth), 0,
  641. SkColorSetARGB(0x33, 0, 0, 0)));
  642. shadow_values.push_back(
  643. gfx::ShadowValue(gfx::Vector2d(0, kBadgeIconShadowWidth), 2,
  644. SkColorSetARGB(0x33, 0, 0, 0)));
  645. badge_icon_->SetImage(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
  646. resized_badge_icon, shadow_values));
  647. badge_icon_->SetVisible(true);
  648. }
  649. void SearchResultView::UpdateBigTitleContainer() {
  650. DCHECK_EQ(view_type_, SearchResultViewType::kAnswerCard);
  651. // Big title is only shown for answer card views.
  652. big_title_main_text_container_->RemoveAllChildViews();
  653. big_title_label_tags_.clear();
  654. if (!result() || result()->big_title_text_vector().empty()) {
  655. big_title_main_text_container_->SetVisible(false);
  656. } else {
  657. // Create big title labels from text vector metadata.
  658. big_title_label_tags_ = SetupContainerViewForTextVector(
  659. big_title_main_text_container_, result()->big_title_text_vector(),
  660. LabelType::kBigTitle, has_keyboard_shortcut_contents_,
  661. /*is_multi_line=*/false);
  662. StyleBigTitleContainer();
  663. big_title_main_text_container_->SetVisible(true);
  664. }
  665. }
  666. void SearchResultView::UpdateBigTitleSuperscriptContainer() {
  667. DCHECK_EQ(view_type_, SearchResultViewType::kAnswerCard);
  668. // Big title superscript is only shown for answer card views.
  669. big_title_superscript_container_->RemoveAllChildViews();
  670. big_title_superscript_label_tags_.clear();
  671. if (!result() || result()->big_title_superscript_text_vector().empty()) {
  672. big_title_superscript_container_->SetVisible(false);
  673. } else {
  674. // Create big title superscript labels from text vector metadata.
  675. big_title_superscript_label_tags_ = SetupContainerViewForTextVector(
  676. big_title_superscript_container_,
  677. result()->big_title_superscript_text_vector(),
  678. LabelType::kBigTitleSuperscript, has_keyboard_shortcut_contents_,
  679. /*is_multi_line=*/false);
  680. StyleBigTitleSuperscriptContainer();
  681. big_title_superscript_container_->SetVisible(true);
  682. big_title_superscript_container_->SetBorder(
  683. views::CreateEmptyBorder(kBigTitleSuperscriptBorder));
  684. }
  685. }
  686. void SearchResultView::UpdateTitleContainer() {
  687. title_container_->RemoveAllChildViews();
  688. title_label_tags_.clear();
  689. if (!result() || result()->title_text_vector().empty()) {
  690. // The entire text container should be hidden when there is no title.
  691. text_container_->SetVisible(false);
  692. title_and_details_container_->SetVisible(false);
  693. title_container_->SetVisible(false);
  694. } else {
  695. // Create title labels from text vector metadata.
  696. title_label_tags_ = SetupContainerViewForTextVector(
  697. title_container_, result()->title_text_vector(), LabelType::kTitle,
  698. has_keyboard_shortcut_contents_,
  699. /*is_multi_line=*/false);
  700. StyleTitleContainer();
  701. text_container_->SetVisible(true);
  702. title_and_details_container_->SetVisible(true);
  703. title_container_->SetVisible(true);
  704. }
  705. }
  706. void SearchResultView::UpdateDetailsContainer() {
  707. should_show_result_text_separator_label_ = false;
  708. // Updating the details label should reset `multi_line_label_height_` and
  709. // `non_elided_details_label_width_`.
  710. multi_line_label_height_ = 0;
  711. non_elided_details_label_width_ = 0;
  712. details_container_->RemoveAllChildViews();
  713. details_label_tags_.clear();
  714. if (!result() || result()->details_text_vector().empty()) {
  715. details_container_->SetVisible(false);
  716. result_text_separator_label_->SetVisible(false);
  717. } else {
  718. // Create details labels from text vector metadata.
  719. details_label_tags_ = SetupContainerViewForTextVector(
  720. details_container_, result()->details_text_vector(),
  721. LabelType::kDetails, has_keyboard_shortcut_contents_,
  722. /*is_multi_line=*/result()->multiline_details());
  723. StyleDetailsContainer();
  724. details_container_->SetVisible(true);
  725. switch (view_type_) {
  726. case SearchResultViewType::kDefault:
  727. // Show `separator_label_` when SetupContainerViewForTextVector gets
  728. // valid contents in `result()->details_text_vector()`.
  729. result_text_separator_label_->SetVisible(
  730. should_show_result_text_separator_label_);
  731. break;
  732. case SearchResultViewType::kClassic:
  733. result_text_separator_label_->SetVisible(false);
  734. break;
  735. case SearchResultViewType::kAnswerCard:
  736. // Show `separator_label_` when SetupContainerViewForTextVector gets
  737. // valid contents in `result()->details_text_vector()` and
  738. // `has_keyboard_shortcut_contents_` is set.
  739. result_text_separator_label_->SetVisible(
  740. should_show_result_text_separator_label_ &&
  741. has_keyboard_shortcut_contents_);
  742. }
  743. }
  744. }
  745. void SearchResultView::UpdateKeyboardShortcutContainer() {
  746. keyboard_shortcut_container_->RemoveAllChildViews();
  747. keyboard_shortcut_container_tags_.clear();
  748. DCHECK(view_type_ != SearchResultViewType::kClassic);
  749. if (!app_list_features::IsSearchResultInlineIconEnabled() || !result() ||
  750. result()->keyboard_shortcut_text_vector().empty()) {
  751. keyboard_shortcut_container_->SetVisible(false);
  752. has_keyboard_shortcut_contents_ = false;
  753. // Reset `title_and_details_container_` orientation.
  754. switch (view_type_) {
  755. case SearchResultViewType::kDefault:
  756. title_and_details_container_->SetOrientation(
  757. views::LayoutOrientation::kHorizontal);
  758. break;
  759. case SearchResultViewType::kClassic:
  760. case SearchResultViewType::kAnswerCard:
  761. title_and_details_container_->SetOrientation(
  762. views::LayoutOrientation::kVertical);
  763. break;
  764. }
  765. } else {
  766. has_keyboard_shortcut_contents_ = true;
  767. keyboard_shortcut_container_tags_ = SetupContainerViewForTextVector(
  768. keyboard_shortcut_container_, result()->keyboard_shortcut_text_vector(),
  769. LabelType::kKeyboardShortcut, has_keyboard_shortcut_contents_,
  770. /*is_multi_line=*/false);
  771. StyleKeyboardShortcutContainer();
  772. keyboard_shortcut_container_->SetVisible(true);
  773. // Override `title_and_details_container_` orientation if the keyboard
  774. // shortcut text vector has valid contents.
  775. title_and_details_container_->SetOrientation(
  776. views::LayoutOrientation::kHorizontal);
  777. }
  778. }
  779. void SearchResultView::UpdateRating() {
  780. if (!result() || !result()->rating() || result()->rating() < 0) {
  781. rating_separator_label_->SetVisible(false);
  782. rating_->SetText(std::u16string());
  783. rating_->SetVisible(false);
  784. rating_star_->SetVisible(false);
  785. return;
  786. }
  787. rating_separator_label_->SetVisible(true);
  788. rating_->SetText(base::FormatDouble(result()->rating(), 1));
  789. rating_->SetVisible(true);
  790. rating_star_->SetVisible(true);
  791. }
  792. void SearchResultView::StyleLabel(views::Label* label,
  793. bool is_title_label,
  794. const SearchResult::Tags& tags) {
  795. // Reset font weight styling for label.
  796. label->ApplyBaselineTextStyle();
  797. auto color_tag = SearchResult::Tag::NONE;
  798. for (const auto& tag : tags) {
  799. // Each label only supports one type of color tag. `color_tag` should only
  800. // be set once.
  801. if (tag.styles & SearchResult::Tag::URL) {
  802. DCHECK(color_tag == SearchResult::Tag::NONE ||
  803. color_tag == SearchResult::Tag::URL);
  804. color_tag = SearchResult::Tag::URL;
  805. }
  806. if (tag.styles & SearchResult::Tag::GREEN) {
  807. DCHECK(color_tag == SearchResult::Tag::NONE ||
  808. color_tag == SearchResult::Tag::GREEN);
  809. color_tag = SearchResult::Tag::GREEN;
  810. }
  811. if (tag.styles & SearchResult::Tag::RED) {
  812. DCHECK(color_tag == SearchResult::Tag::NONE ||
  813. color_tag == SearchResult::Tag::RED);
  814. color_tag = SearchResult::Tag::RED;
  815. }
  816. bool has_match_tag = (tag.styles & SearchResult::Tag::MATCH);
  817. if (has_match_tag) {
  818. switch (view_type_) {
  819. case SearchResultViewType::kClassic:
  820. label->SetTextStyleRange(views::style::STYLE_EMPHASIZED, tag.range);
  821. break;
  822. case SearchResultViewType::kDefault:
  823. ABSL_FALLTHROUGH_INTENDED;
  824. case SearchResultViewType::kAnswerCard:
  825. label->SetTextStyleRange(AshTextStyle::STYLE_HIGHLIGHT, tag.range);
  826. break;
  827. }
  828. }
  829. }
  830. switch (color_tag) {
  831. case SearchResult::Tag::NONE:
  832. ABSL_FALLTHROUGH_INTENDED;
  833. case SearchResult::Tag::DIM:
  834. ABSL_FALLTHROUGH_INTENDED;
  835. case SearchResult::Tag::MATCH:
  836. label->SetEnabledColor(
  837. is_title_label
  838. ? AppListColorProvider::Get()->GetSearchBoxTextColor(
  839. kDeprecatedSearchBoxTextDefaultColor)
  840. : AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  841. kDeprecatedSearchBoxTextDefaultColor));
  842. break;
  843. case SearchResult::Tag::URL:
  844. label->SetEnabledColor(AppListColorProvider::Get()->GetTextColorURL());
  845. break;
  846. case SearchResult::Tag::GREEN:
  847. label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
  848. AshColorProvider::ContentLayerType::kTextColorPositive));
  849. break;
  850. case SearchResult::Tag::RED:
  851. label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
  852. AshColorProvider::ContentLayerType::kTextColorAlert));
  853. break;
  854. }
  855. }
  856. void SearchResultView::StyleBigTitleContainer() {
  857. for (auto& span : big_title_label_tags_) {
  858. StyleLabel(span.GetLabel(), true /*is_title_label*/, span.GetTags());
  859. }
  860. }
  861. void SearchResultView::StyleBigTitleSuperscriptContainer() {
  862. for (auto& span : big_title_superscript_label_tags_) {
  863. StyleLabel(span.GetLabel(), true /*is_title_label*/, span.GetTags());
  864. }
  865. }
  866. void SearchResultView::StyleTitleContainer() {
  867. for (auto& span : title_label_tags_) {
  868. StyleLabel(span.GetLabel(), true /*is_title_label*/, span.GetTags());
  869. }
  870. }
  871. void SearchResultView::StyleDetailsContainer() {
  872. for (auto& span : details_label_tags_) {
  873. StyleLabel(span.GetLabel(), false /*is_title_label*/, span.GetTags());
  874. }
  875. }
  876. void SearchResultView::StyleKeyboardShortcutContainer() {
  877. for (auto& span : keyboard_shortcut_container_tags_) {
  878. StyleLabel(span.GetLabel(), false /*is_title_label*/, span.GetTags());
  879. }
  880. }
  881. void SearchResultView::OnQueryRemovalAccepted(bool accepted) {
  882. if (accepted) {
  883. list_view_->SearchResultActionActivated(this,
  884. SearchResultActionType::kRemove);
  885. }
  886. if (confirm_remove_by_long_press_) {
  887. confirm_remove_by_long_press_ = false;
  888. SetSelected(false, absl::nullopt);
  889. }
  890. // Record different dialog action metric depending on productivity launcher
  891. // state - productivity launcher does not show zero-state search results, so
  892. // zero-state specific metric is not suitable. On the other hand, removal
  893. // action outside of zero-state search UI is only allowed if the productivity
  894. // launcher feature is on.
  895. if (features::IsProductivityLauncherEnabled()) {
  896. RecordSearchResultRemovalDialogDecision(
  897. accepted ? SearchResultRemovalConfirmation::kRemovalConfirmed
  898. : SearchResultRemovalConfirmation::kRemovalCanceled);
  899. } else {
  900. RecordZeroStateSearchResultRemovalHistogram(
  901. accepted ? SearchResultRemovalConfirmation::kRemovalConfirmed
  902. : SearchResultRemovalConfirmation::kRemovalCanceled);
  903. }
  904. }
  905. void SearchResultView::OnSelectedResultChanged() {
  906. if (!selected())
  907. actions_view()->HideActions();
  908. }
  909. const char* SearchResultView::GetClassName() const {
  910. return kViewClassName;
  911. }
  912. gfx::Size SearchResultView::CalculatePreferredSize() const {
  913. return gfx::Size(kPreferredWidth, PreferredHeight());
  914. }
  915. void SearchResultView::Layout() {
  916. // TODO(crbug/1311101) add test coverage for search result view layout.
  917. gfx::Rect rect(GetContentsBounds());
  918. if (rect.IsEmpty())
  919. return;
  920. gfx::Rect icon_bounds(rect);
  921. int left_right_padding =
  922. (kPreferredIconViewWidth - icon_->GetImage().width()) / 2;
  923. int top_bottom_padding = (rect.height() - icon_->GetImage().height()) / 2;
  924. icon_bounds.set_width(kPreferredIconViewWidth);
  925. icon_bounds.Inset(gfx::Insets::VH(top_bottom_padding, left_right_padding));
  926. icon_bounds.Intersect(rect);
  927. icon_->SetBoundsRect(icon_bounds);
  928. gfx::Rect badge_icon_bounds;
  929. const int badge_icon_dimension =
  930. SharedAppListConfig::instance().search_list_badge_icon_dimension();
  931. badge_icon_bounds = gfx::Rect(icon_bounds.right() - badge_icon_dimension,
  932. icon_bounds.bottom() - badge_icon_dimension,
  933. badge_icon_dimension, badge_icon_dimension);
  934. badge_icon_bounds.Inset(-kBadgeIconShadowWidth);
  935. badge_icon_bounds.Intersect(rect);
  936. badge_icon_->SetBoundsRect(badge_icon_bounds);
  937. const int max_actions_width =
  938. (rect.right() - ActionButtonRightMargin() - icon_bounds.right()) / 2;
  939. int actions_width =
  940. std::min(max_actions_width, actions_view()->GetPreferredSize().width());
  941. gfx::Rect actions_bounds(rect);
  942. actions_bounds.set_x(rect.right() - ActionButtonRightMargin() -
  943. actions_width);
  944. actions_bounds.set_width(actions_width);
  945. actions_view()->SetBoundsRect(actions_bounds);
  946. gfx::Rect text_bounds(rect);
  947. // Text bounds need to be shifted over by kAnswerCardBorderMargin for answer
  948. // card views to make room for the kAnswerCardBorder.
  949. text_bounds.set_x(kPreferredIconViewWidth +
  950. (view_type_ == SearchResultViewType::kAnswerCard
  951. ? kAnswerCardBorderMargin
  952. : 0));
  953. if (actions_view()->GetVisible()) {
  954. text_bounds.set_width(
  955. rect.width() - kPreferredIconViewWidth - kTextTrailPadding -
  956. actions_view()->bounds().width() -
  957. (actions_view()->children().empty() ? 0 : ActionButtonRightMargin()));
  958. } else {
  959. text_bounds.set_width(rect.width() - kPreferredIconViewWidth -
  960. kTextTrailPadding - ActionButtonRightMargin());
  961. }
  962. if (!title_label_tags_.empty() && !details_label_tags_.empty()) {
  963. switch (view_type_) {
  964. case SearchResultViewType::kDefault: {
  965. // SearchResultView needs additional space when
  966. // `has_keyboard_shortcut_contents_` is set to accommodate the
  967. // `keyboard_shortcut_container_`.
  968. gfx::Size label_size(
  969. text_bounds.width(),
  970. PrimaryTextHeight() +
  971. (has_keyboard_shortcut_contents_
  972. ? kKeyboardShortcutTopMargin + SecondaryTextHeight()
  973. : 0));
  974. gfx::Rect centered_text_bounds(text_bounds);
  975. centered_text_bounds.ClampToCenteredSize(label_size);
  976. text_container_->SetBoundsRect(centered_text_bounds);
  977. SetFlexBehaviorForTextContents(
  978. centered_text_bounds.width(),
  979. result_text_separator_label_->GetPreferredSize().width(),
  980. non_elided_details_label_width_, title_container_,
  981. details_container_);
  982. break;
  983. }
  984. case SearchResultViewType::kClassic:
  985. case SearchResultViewType::kAnswerCard: {
  986. gfx::Size label_size(
  987. text_bounds.width(),
  988. PrimaryTextHeight() + SecondaryTextHeight() +
  989. (has_keyboard_shortcut_contents_ ? kKeyboardShortcutTopMargin
  990. : 0));
  991. gfx::Rect centered_text_bounds(text_bounds);
  992. centered_text_bounds.ClampToCenteredSize(label_size);
  993. text_container_->SetBoundsRect(centered_text_bounds);
  994. }
  995. }
  996. } else if (!title_label_tags_.empty()) {
  997. gfx::Size text_size(text_bounds.width(), PrimaryTextHeight());
  998. gfx::Rect centered_text_bounds(text_bounds);
  999. centered_text_bounds.ClampToCenteredSize(text_size);
  1000. text_container_->SetBoundsRect(centered_text_bounds);
  1001. }
  1002. }
  1003. bool SearchResultView::OnKeyPressed(const ui::KeyEvent& event) {
  1004. // result() could be null when result list is changing.
  1005. if (!result())
  1006. return false;
  1007. switch (event.key_code()) {
  1008. case ui::VKEY_RETURN:
  1009. if (actions_view()->HasSelectedAction()) {
  1010. OnSearchResultActionActivated(static_cast<SearchResultActionType>(
  1011. actions_view()->GetSelectedAction()));
  1012. } else {
  1013. list_view_->SearchResultActivated(this, event.flags(),
  1014. false /* by_button_press */);
  1015. }
  1016. return true;
  1017. case ui::VKEY_DELETE:
  1018. case ui::VKEY_BROWSER_BACK:
  1019. // Allows alt+(back or delete) to trigger the 'remove result' dialog.
  1020. OnSearchResultActionActivated(SearchResultActionType::kRemove);
  1021. return true;
  1022. default:
  1023. return false;
  1024. }
  1025. }
  1026. void SearchResultView::PaintButtonContents(gfx::Canvas* canvas) {
  1027. gfx::Rect rect(GetContentsBounds());
  1028. if (rect.IsEmpty())
  1029. return;
  1030. gfx::Rect content_rect(rect);
  1031. switch (view_type_) {
  1032. case SearchResultViewType::kDefault:
  1033. case SearchResultViewType::kClassic:
  1034. if (selected() && !actions_view()->HasSelectedAction()) {
  1035. canvas->FillRect(
  1036. content_rect,
  1037. AppListColorProvider::Get()->GetSearchResultViewHighlightColor());
  1038. PaintFocusBar(canvas, GetContentsBounds().origin(),
  1039. /*height=*/GetContentsBounds().height());
  1040. }
  1041. break;
  1042. case SearchResultViewType::kAnswerCard: {
  1043. cc::PaintFlags flags;
  1044. flags.setAntiAlias(true);
  1045. flags.setColor(
  1046. AppListColorProvider::Get()->GetSearchResultViewHighlightColor());
  1047. canvas->DrawRoundRect(content_rect,
  1048. kAnswerCardCardBackgroundCornerRadius, flags);
  1049. if (selected()) {
  1050. // Dynamically calculate the height of the answer card focus bar to
  1051. // accommodate different heights for multi-line results.
  1052. PaintFocusBar(canvas,
  1053. gfx::Point(kAnswerCardFocusBarHorizontalOffset,
  1054. kAnswerCardFocusBarVerticalOffset),
  1055. PreferredHeight() -
  1056. kAnswerCardCardBackgroundCornerRadius * 2 -
  1057. kAnswerCardFocusBarVerticalOffset);
  1058. }
  1059. } break;
  1060. }
  1061. }
  1062. void SearchResultView::OnMouseEntered(const ui::MouseEvent& event) {
  1063. actions_view()->UpdateButtonsOnStateChanged();
  1064. }
  1065. void SearchResultView::OnMouseExited(const ui::MouseEvent& event) {
  1066. actions_view()->UpdateButtonsOnStateChanged();
  1067. }
  1068. void SearchResultView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  1069. if (!GetVisible())
  1070. return;
  1071. // Mark the result is a list item in the list of search results.
  1072. // Also avoids an issue with the nested button case(append and remove
  1073. // button are child button of SearchResultView), which is not supported by
  1074. // ChromeVox. see details in crbug.com/924776.
  1075. node_data->role = ax::mojom::Role::kListBoxOption;
  1076. node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  1077. node_data->SetName(GetAccessibleName());
  1078. }
  1079. void SearchResultView::VisibilityChanged(View* starting_from, bool is_visible) {
  1080. NotifyAccessibilityEvent(ax::mojom::Event::kLayoutComplete, true);
  1081. }
  1082. void SearchResultView::OnThemeChanged() {
  1083. if (!big_title_label_tags_.empty())
  1084. StyleBigTitleContainer();
  1085. if (!title_label_tags_.empty())
  1086. StyleTitleContainer();
  1087. if (!details_label_tags_.empty())
  1088. StyleDetailsContainer();
  1089. if (!keyboard_shortcut_container_tags_.empty())
  1090. StyleKeyboardShortcutContainer();
  1091. result_text_separator_label_->SetEnabledColor(
  1092. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  1093. kDeprecatedSearchBoxTextDefaultColor));
  1094. rating_separator_label_->SetEnabledColor(
  1095. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  1096. kDeprecatedSearchBoxTextDefaultColor));
  1097. rating_->SetEnabledColor(
  1098. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  1099. kDeprecatedSearchBoxTextDefaultColor));
  1100. rating_star_->SetImage(gfx::CreateVectorIcon(
  1101. kBadgeRatingIcon, kSearchRatingStarSize,
  1102. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  1103. kDeprecatedSearchBoxTextDefaultColor)));
  1104. views::View::OnThemeChanged();
  1105. }
  1106. void SearchResultView::OnGestureEvent(ui::GestureEvent* event) {
  1107. switch (event->type()) {
  1108. case ui::ET_GESTURE_LONG_PRESS:
  1109. if (actions_view()->IsValidActionIndex(SearchResultActionType::kRemove)) {
  1110. ScrollRectToVisible(GetLocalBounds());
  1111. SetSelected(true, absl::nullopt);
  1112. confirm_remove_by_long_press_ = true;
  1113. event->SetHandled();
  1114. }
  1115. break;
  1116. default:
  1117. break;
  1118. }
  1119. if (!event->handled())
  1120. Button::OnGestureEvent(event);
  1121. }
  1122. void SearchResultView::OnMetadataChanged() {
  1123. if (view_type_ == SearchResultViewType::kAnswerCard) {
  1124. UpdateBigTitleContainer();
  1125. UpdateBigTitleSuperscriptContainer();
  1126. }
  1127. if (view_type_ != SearchResultViewType::kClassic &&
  1128. app_list_features::IsSearchResultInlineIconEnabled()) {
  1129. UpdateKeyboardShortcutContainer();
  1130. }
  1131. UpdateTitleContainer();
  1132. UpdateDetailsContainer();
  1133. UpdateAccessibleName();
  1134. UpdateBadgeIcon();
  1135. UpdateRating();
  1136. // Updates |icon_|.
  1137. // Note: this might leave the view with an old icon. But it is needed to avoid
  1138. // flash when a SearchResult's icon is loaded asynchronously. In this case, it
  1139. // looks nicer to keep the stale icon for a little while on screen instead of
  1140. // clearing it out. It should work correctly as long as the SearchResult does
  1141. // not forget to SetIcon when it's ready.
  1142. if (result() && !result()->icon().icon.isNull()) {
  1143. const SearchResult::IconInfo& icon_info = result()->icon();
  1144. const gfx::ImageSkia& image = icon_info.icon;
  1145. // Calculate the image dimensions. Images could be rectangular, and we
  1146. // should preserve the aspect ratio.
  1147. const size_t dimension = result()->icon().dimension;
  1148. const int max = std::max(image.width(), image.height());
  1149. const bool is_square = image.width() == image.height();
  1150. const int width = is_square ? dimension : dimension * image.width() / max;
  1151. const int height = is_square ? dimension : dimension * image.height() / max;
  1152. SetIconImage(image, icon_, gfx::Size(width, height));
  1153. icon_->set_shape(icon_info.shape);
  1154. }
  1155. // Updates |actions_view()|.
  1156. actions_view()->SetActions(result() ? result()->actions()
  1157. : SearchResult::Actions());
  1158. }
  1159. void SearchResultView::OnButtonPressed(const ui::Event& event) {
  1160. list_view_->SearchResultActivated(this, event.flags(),
  1161. true /* by_button_press */);
  1162. }
  1163. void SearchResultView::SetIconImage(const gfx::ImageSkia& source,
  1164. views::ImageView* const icon,
  1165. const gfx::Size& size) {
  1166. gfx::ImageSkia image(source);
  1167. image = gfx::ImageSkiaOperations::CreateResizedImage(
  1168. source, skia::ImageOperations::RESIZE_BEST, size);
  1169. icon->SetImage(image);
  1170. icon->SetImageSize(size);
  1171. }
  1172. void SearchResultView::OnSearchResultActionActivated(size_t index) {
  1173. // |result()| could be nullptr when result list is changing.
  1174. if (!result())
  1175. return;
  1176. DCHECK_LT(index, result()->actions().size());
  1177. SearchResultActionType button_action = result()->actions()[index].type;
  1178. switch (button_action) {
  1179. case SearchResultActionType::kRemove: {
  1180. // Zero state suggestions are only available when productivity launcher
  1181. // is not enabled, so don't record zero-state metric when the feature is
  1182. // turned on.
  1183. if (!features::IsProductivityLauncherEnabled()) {
  1184. RecordZeroStateSearchResultUserActionHistogram(
  1185. ZeroStateSearchResultUserActionType::kRemoveResult);
  1186. }
  1187. std::unique_ptr<views::WidgetDelegate> dialog;
  1188. if (features::IsProductivityLauncherEnabled()) {
  1189. dialog = std::make_unique<RemoveQueryConfirmationDialog>(
  1190. base::BindOnce(&SearchResultView::OnQueryRemovalAccepted,
  1191. weak_ptr_factory_.GetWeakPtr()),
  1192. result()->title());
  1193. } else {
  1194. dialog = std::make_unique<LegacyRemoveQueryConfirmationDialog>(
  1195. base::BindOnce(&SearchResultView::OnQueryRemovalAccepted,
  1196. weak_ptr_factory_.GetWeakPtr()),
  1197. result()->title());
  1198. }
  1199. dialog_controller_->Show(std::move(dialog));
  1200. break;
  1201. }
  1202. case SearchResultActionType::kAppend:
  1203. // Zero state suggestions are only available when productivity launcher
  1204. // is not enabled, so don't record zero-state metric when the feature is
  1205. // turned on.
  1206. if (!features::IsProductivityLauncherEnabled()) {
  1207. RecordZeroStateSearchResultUserActionHistogram(
  1208. ZeroStateSearchResultUserActionType::kAppendResult);
  1209. }
  1210. list_view_->SearchResultActionActivated(this, button_action);
  1211. break;
  1212. case SearchResultActionType::kSearchResultActionTypeMax:
  1213. NOTREACHED();
  1214. }
  1215. }
  1216. bool SearchResultView::IsSearchResultHoveredOrSelected() {
  1217. return IsMouseHovered() || selected();
  1218. }
  1219. } // namespace ash