1 // Copyright 2017 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 "ui/message_center/views/notification_view_md.h"
6 
7 #include <stddef.h>
8 #include <memory>
9 
10 #include "base/i18n/case_conversion.h"
11 #include "base/metrics/histogram_macros.h"
12 #include "base/strings/string_util.h"
13 #include "components/url_formatter/elide_url.h"
14 #include "ui/base/class_property.h"
15 #include "ui/base/cursor/cursor.h"
16 #include "ui/base/l10n/l10n_util.h"
17 #include "ui/events/base_event_utils.h"
18 #include "ui/events/gesture_detection/gesture_provider_config_helper.h"
19 #include "ui/gfx/canvas.h"
20 #include "ui/gfx/color_palette.h"
21 #include "ui/gfx/geometry/rect.h"
22 #include "ui/gfx/geometry/size.h"
23 #include "ui/gfx/image/image_skia_operations.h"
24 #include "ui/gfx/paint_vector_icon.h"
25 #include "ui/gfx/skia_util.h"
26 #include "ui/gfx/text_constants.h"
27 #include "ui/gfx/text_elider.h"
28 #include "ui/message_center/message_center.h"
29 #include "ui/message_center/public/cpp/message_center_constants.h"
30 #include "ui/message_center/public/cpp/notification.h"
31 #include "ui/message_center/public/cpp/notification_types.h"
32 #include "ui/message_center/vector_icons.h"
33 #include "ui/message_center/views/notification_background_painter.h"
34 #include "ui/message_center/views/notification_control_buttons_view.h"
35 #include "ui/message_center/views/notification_header_view.h"
36 #include "ui/message_center/views/padded_button.h"
37 #include "ui/message_center/views/proportional_image_view.h"
38 #include "ui/strings/grit/ui_strings.h"
39 #include "ui/views/animation/flood_fill_ink_drop_ripple.h"
40 #include "ui/views/animation/ink_drop_highlight.h"
41 #include "ui/views/animation/ink_drop_impl.h"
42 #include "ui/views/animation/ink_drop_mask.h"
43 #include "ui/views/background.h"
44 #include "ui/views/border.h"
45 #include "ui/views/controls/button/image_button.h"
46 #include "ui/views/controls/button/radio_button.h"
47 #include "ui/views/controls/highlight_path_generator.h"
48 #include "ui/views/controls/image_view.h"
49 #include "ui/views/controls/label.h"
50 #include "ui/views/controls/progress_bar.h"
51 #include "ui/views/controls/textfield/textfield.h"
52 #include "ui/views/focus/focus_manager.h"
53 #include "ui/views/layout/box_layout.h"
54 #include "ui/views/layout/fill_layout.h"
55 #include "ui/views/native_cursor.h"
56 #include "ui/views/widget/widget.h"
57 #include "ui/views/widget/widget_delegate.h"
58 
59 namespace message_center {
60 
61 namespace {
62 
63 // Dimensions.
64 constexpr gfx::Insets kContentRowPadding(0, 12, 16, 12);
65 constexpr gfx::Insets kActionsRowPadding(8, 8, 8, 8);
66 constexpr int kActionsRowHorizontalSpacing = 8;
67 constexpr gfx::Insets kActionButtonPadding(0, 12, 0, 12);
68 constexpr gfx::Insets kStatusTextPadding(4, 0, 0, 0);
69 constexpr gfx::Size kActionButtonMinSize(0, 32);
70 // TODO(tetsui): Move |kIconViewSize| to public/cpp/message_center_constants.h
71 // and merge with contradicting |kNotificationIconSize|.
72 constexpr gfx::Size kIconViewSize(36, 36);
73 constexpr gfx::Insets kLargeImageContainerPadding(0, 16, 16, 16);
74 constexpr gfx::Size kLargeImageMinSize(328, 0);
75 constexpr gfx::Size kLargeImageMaxSize(328, 218);
76 constexpr gfx::Insets kLeftContentPadding(2, 4, 0, 4);
77 constexpr gfx::Insets kLeftContentPaddingWithIcon(2, 4, 0, 12);
78 constexpr gfx::Insets kInputTextfieldPadding(16, 16, 16, 0);
79 constexpr gfx::Insets kInputReplyButtonPadding(0, 14, 0, 14);
80 constexpr gfx::Insets kSettingsRowPadding(8, 0, 0, 0);
81 constexpr gfx::Insets kSettingsRadioButtonPadding(14, 18, 14, 18);
82 constexpr gfx::Insets kSettingsButtonRowPadding(8);
83 
84 // Background of inline actions area.
85 constexpr SkColor kActionsRowBackgroundColor = SkColorSetRGB(0xee, 0xee, 0xee);
86 // Ripple ink drop opacity of action buttons.
87 const float kActionButtonInkDropRippleVisibleOpacity = 0.08f;
88 // Highlight (hover) ink drop opacity of action buttons.
89 const float kActionButtonInkDropHighlightVisibleOpacity = 0.08f;
90 // Text color of action button.
91 constexpr SkColor kActionButtonTextColor = gfx::kGoogleBlue600;
92 // Background color of the large image.
93 constexpr SkColor kLargeImageBackgroundColor = SkColorSetRGB(0xf5, 0xf5, 0xf5);
94 // Background color of the inline settings.
95 constexpr SkColor kInlineSettingsBackgroundColor =
96     SkColorSetRGB(0xEE, 0xEE, 0xEE);
97 
98 // Text color and icon color of inline reply area when the textfield is empty.
99 constexpr SkColor kTextfieldPlaceholderTextColorMD =
100     SkColorSetA(SK_ColorWHITE, 0x8A);
101 constexpr SkColor kTextfieldPlaceholderIconColorMD =
102     SkColorSetA(SK_ColorWHITE, 0x60);
103 
104 // The icon size of inline reply input field.
105 constexpr int kInputReplyButtonSize = 20;
106 
107 // Max number of lines for title_view_.
108 constexpr int kMaxLinesForTitleView = 1;
109 // Max number of lines for message_view_.
110 constexpr int kMaxLinesForMessageView = 1;
111 constexpr int kMaxLinesForExpandedMessageView = 4;
112 
113 constexpr int kCompactTitleMessageViewSpacing = 12;
114 
115 constexpr int kProgressBarHeight = 4;
116 
117 constexpr int kMessageViewWidthWithIcon =
118     kNotificationWidth - kIconViewSize.width() -
119     kLeftContentPaddingWithIcon.left() - kLeftContentPaddingWithIcon.right() -
120     kContentRowPadding.left() - kContentRowPadding.right();
121 
122 constexpr int kMessageViewWidth =
123     kNotificationWidth - kLeftContentPadding.left() -
124     kLeftContentPadding.right() - kContentRowPadding.left() -
125     kContentRowPadding.right();
126 
127 const int kMinPixelsPerTitleCharacterMD = 4;
128 
129 // Character limit = pixels per line * line limit / min. pixels per character.
130 constexpr size_t kMessageCharacterLimitMD =
131     kNotificationWidth * kMessageExpandedLineLimit / 3;
132 
133 // The default is 12, so this normally come out to 13.
134 constexpr int kTextFontSizeDelta = 1;
135 
136 // In progress notification, if both the title and the message are long, the
137 // message would be prioritized and the title would be elided.
138 // However, it is not perferable that we completely omit the title, so
139 // the ratio of the message width is limited to this value.
140 constexpr double kProgressNotificationMessageRatio = 0.7;
141 
142 // Line height of title and message views.
143 constexpr int kLineHeightMD = 17;
144 
145 // This key/property allows tagging the textfield with its index.
146 DEFINE_UI_CLASS_PROPERTY_KEY(int, kTextfieldIndexKey, 0U)
147 
148 // FontList for the texts except for the header.
GetTextFontList()149 gfx::FontList GetTextFontList() {
150   gfx::Font default_font;
151   gfx::Font font = default_font.Derive(kTextFontSizeDelta, gfx::Font::NORMAL,
152                                        gfx::Font::Weight::NORMAL);
153   return gfx::FontList(font);
154 }
155 
156 class ClickActivator : public ui::EventHandler {
157  public:
ClickActivator(NotificationViewMD * owner)158   explicit ClickActivator(NotificationViewMD* owner) : owner_(owner) {}
159   ~ClickActivator() override = default;
160 
161  private:
162   // ui::EventHandler
OnEvent(ui::Event * event)163   void OnEvent(ui::Event* event) override {
164     if (event->type() == ui::ET_MOUSE_PRESSED ||
165         event->type() == ui::ET_GESTURE_TAP) {
166       owner_->Activate();
167     }
168   }
169 
170   NotificationViewMD* const owner_;
171 
172   DISALLOW_COPY_AND_ASSIGN(ClickActivator);
173 };
174 
175 // Creates a view responsible for drawing each list notification item's title
176 // and message next to each other within a single column.
CreateItemView(const NotificationItem & item)177 std::unique_ptr<views::View> CreateItemView(const NotificationItem& item) {
178   auto view = std::make_unique<views::View>();
179   view->SetLayoutManager(std::make_unique<views::BoxLayout>(
180       views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), 0));
181 
182   const gfx::FontList font_list = GetTextFontList();
183 
184   auto* title = new views::Label(item.title);
185   title->SetFontList(font_list);
186   title->SetCollapseWhenHidden(true);
187   title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
188   title->SetEnabledColor(kRegularTextColorMD);
189   title->SetBackgroundColor(kNotificationBackgroundColor);
190   title->SetAutoColorReadabilityEnabled(false);
191   view->AddChildView(title);
192 
193   views::Label* message = new views::Label(l10n_util::GetStringFUTF16(
194       IDS_MESSAGE_CENTER_LIST_NOTIFICATION_MESSAGE_WITH_DIVIDER, item.message));
195   message->SetFontList(font_list);
196   message->SetCollapseWhenHidden(true);
197   message->SetHorizontalAlignment(gfx::ALIGN_LEFT);
198   message->SetEnabledColor(kDimTextColorMD);
199   message->SetBackgroundColor(kNotificationBackgroundColor);
200   message->SetAutoColorReadabilityEnabled(false);
201   view->AddChildView(message);
202   return view;
203 }
204 
205 }  // anonymous namespace
206 
207 // CompactTitleMessageView /////////////////////////////////////////////////////
208 
209 CompactTitleMessageView::~CompactTitleMessageView() = default;
210 
GetClassName() const211 const char* CompactTitleMessageView::GetClassName() const {
212   return "CompactTitleMessageView";
213 }
214 
CompactTitleMessageView()215 CompactTitleMessageView::CompactTitleMessageView() {
216   const gfx::FontList& font_list = GetTextFontList();
217 
218   title_ = new views::Label();
219   title_->SetFontList(font_list);
220   title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
221   title_->SetEnabledColor(kRegularTextColorMD);
222   title_->SetBackgroundColor(kNotificationBackgroundColor);
223   AddChildView(title_);
224 
225   message_ = new views::Label();
226   message_->SetFontList(font_list);
227   message_->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
228   message_->SetEnabledColor(kDimTextColorMD);
229   message_->SetBackgroundColor(kNotificationBackgroundColor);
230   AddChildView(message_);
231 }
232 
CalculatePreferredSize() const233 gfx::Size CompactTitleMessageView::CalculatePreferredSize() const {
234   gfx::Size title_size = title_->GetPreferredSize();
235   gfx::Size message_size = message_->GetPreferredSize();
236   return gfx::Size(title_size.width() + message_size.width() +
237                        kCompactTitleMessageViewSpacing,
238                    std::max(title_size.height(), message_size.height()));
239 }
240 
Layout()241 void CompactTitleMessageView::Layout() {
242   // Elides title and message.
243   // * If the message is too long, the message occupies at most
244   //   kProgressNotificationMessageRatio of the width.
245   // * If the title is too long, the full content of the message is shown,
246   //   kCompactTitleMessageViewSpacing is added between them, and the elided
247   //   title is shown.
248   // * If they are short enough, the title is left-aligned and the message is
249   //   right-aligned.
250   const int message_width = std::min(
251       message_->GetPreferredSize().width(),
252       title_->GetPreferredSize().width() > 0
253           ? static_cast<int>(kProgressNotificationMessageRatio * width())
254           : width());
255   const int title_width =
256       std::max(0, width() - message_width - kCompactTitleMessageViewSpacing);
257 
258   title_->SetBounds(0, 0, title_width, height());
259   message_->SetBounds(width() - message_width, 0, message_width, height());
260 }
261 
set_title(const base::string16 & title)262 void CompactTitleMessageView::set_title(const base::string16& title) {
263   title_->SetText(title);
264 }
265 
set_message(const base::string16 & message)266 void CompactTitleMessageView::set_message(const base::string16& message) {
267   message_->SetText(message);
268 }
269 
270 // LargeImageView //////////////////////////////////////////////////////////////
271 
LargeImageView()272 LargeImageView::LargeImageView() {
273   SetBackground(views::CreateSolidBackground(kLargeImageBackgroundColor));
274 }
275 
276 LargeImageView::~LargeImageView() = default;
277 
SetImage(const gfx::ImageSkia & image)278 void LargeImageView::SetImage(const gfx::ImageSkia& image) {
279   image_ = image;
280   gfx::Size preferred_size = GetResizedImageSize();
281   preferred_size.SetToMax(kLargeImageMinSize);
282   preferred_size.SetToMin(kLargeImageMaxSize);
283   SetPreferredSize(preferred_size);
284   SchedulePaint();
285   Layout();
286 }
287 
OnPaint(gfx::Canvas * canvas)288 void LargeImageView::OnPaint(gfx::Canvas* canvas) {
289   views::View::OnPaint(canvas);
290 
291   gfx::Size resized_size = GetResizedImageSize();
292   gfx::Size drawn_size = resized_size;
293   drawn_size.SetToMin(kLargeImageMaxSize);
294   gfx::Rect drawn_bounds = GetContentsBounds();
295   drawn_bounds.ClampToCenteredSize(drawn_size);
296 
297   gfx::ImageSkia resized_image = gfx::ImageSkiaOperations::CreateResizedImage(
298       image_, skia::ImageOperations::RESIZE_BEST, resized_size);
299 
300   // Cut off the overflown part.
301   gfx::ImageSkia drawn_image = gfx::ImageSkiaOperations::ExtractSubset(
302       resized_image, gfx::Rect(drawn_size));
303 
304   canvas->DrawImageInt(drawn_image, drawn_bounds.x(), drawn_bounds.y());
305 }
306 
GetClassName() const307 const char* LargeImageView::GetClassName() const {
308   return "LargeImageView";
309 }
310 
311 // Returns expected size of the image right after resizing.
312 // The GetResizedImageSize().width() <= kLargeImageMaxSize.width() holds, but
313 // GetResizedImageSize().height() may be larger than kLargeImageMaxSize.height()
314 // In this case, the overflown part will be just cutted off from the view.
GetResizedImageSize()315 gfx::Size LargeImageView::GetResizedImageSize() {
316   gfx::Size original_size = image_.size();
317   if (original_size.width() <= kLargeImageMaxSize.width())
318     return image_.size();
319 
320   const double proportion =
321       original_size.height() / static_cast<double>(original_size.width());
322   gfx::Size resized_size;
323   resized_size.SetSize(kLargeImageMaxSize.width(),
324                        kLargeImageMaxSize.width() * proportion);
325   return resized_size;
326 }
327 
328 // NotificationButtonMD ////////////////////////////////////////////////////////
329 
NotificationButtonMD(views::ButtonListener * listener,const base::string16 & label,const base::Optional<base::string16> & placeholder)330 NotificationButtonMD::NotificationButtonMD(
331     views::ButtonListener* listener,
332     const base::string16& label,
333     const base::Optional<base::string16>& placeholder)
334     : views::LabelButton(listener,
335                          base::i18n::ToUpper(label),
336                          views::style::CONTEXT_BUTTON_MD),
337       placeholder_(placeholder) {
338   SetHorizontalAlignment(gfx::ALIGN_CENTER);
339   SetInkDropMode(InkDropMode::ON);
340   set_has_ink_drop_action_on_click(true);
341   set_ink_drop_base_color(SK_ColorBLACK);
342   set_ink_drop_visible_opacity(kActionButtonInkDropRippleVisibleOpacity);
343   SetEnabledTextColors(kActionButtonTextColor);
344   SetBorder(views::CreateEmptyBorder(kActionButtonPadding));
345   SetMinSize(kActionButtonMinSize);
346   SetFocusForPlatform();
347 
348   views::InstallRectHighlightPathGenerator(this);
349 }
350 
351 NotificationButtonMD::~NotificationButtonMD() = default;
352 
SetText(const base::string16 & text)353 void NotificationButtonMD::SetText(const base::string16& text) {
354   views::LabelButton::SetText(base::i18n::ToUpper(text));
355 }
356 
GetClassName() const357 const char* NotificationButtonMD::GetClassName() const {
358   return "NotificationButtonMD";
359 }
360 
361 std::unique_ptr<views::InkDropHighlight>
CreateInkDropHighlight() const362 NotificationButtonMD::CreateInkDropHighlight() const {
363   std::unique_ptr<views::InkDropHighlight> highlight =
364       views::LabelButton::CreateInkDropHighlight();
365   highlight->set_visible_opacity(kActionButtonInkDropHighlightVisibleOpacity);
366   return highlight;
367 }
368 
369 // NotificationInputContainerMD ////////////////////////////////////////////////
370 
NotificationInputContainerMD(NotificationInputDelegate * delegate)371 NotificationInputContainerMD::NotificationInputContainerMD(
372     NotificationInputDelegate* delegate)
373     : delegate_(delegate),
374       ink_drop_container_(new views::InkDropContainerView()),
375       textfield_(new views::Textfield()),
376       button_(new views::ImageButton(this)) {
377   auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
378       views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), 0));
379   SetBackground(views::CreateSolidBackground(kActionsRowBackgroundColor));
380 
381   SetInkDropMode(InkDropMode::ON);
382   set_ink_drop_visible_opacity(1);
383 
384   AddChildView(ink_drop_container_);
385 
386   textfield_->set_controller(this);
387   textfield_->SetTextColor(SK_ColorWHITE);
388   textfield_->SetBackgroundColor(SK_ColorTRANSPARENT);
389   textfield_->set_placeholder_text_color(kTextfieldPlaceholderTextColorMD);
390   textfield_->SetBorder(views::CreateEmptyBorder(kInputTextfieldPadding));
391   AddChildView(textfield_);
392   layout->SetFlexForView(textfield_, 1);
393 
394   button_->SetBorder(views::CreateEmptyBorder(kInputReplyButtonPadding));
395   button_->SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
396   button_->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
397   OnAfterUserAction(textfield_);
398   AddChildView(button_);
399 
400   views::InstallRectHighlightPathGenerator(this);
401 }
402 
403 NotificationInputContainerMD::~NotificationInputContainerMD() = default;
404 
AnimateBackground(const ui::Event & event)405 void NotificationInputContainerMD::AnimateBackground(const ui::Event& event) {
406   // Try to get a located event. This can be NULL if triggered via keyboard.
407   const ui::LocatedEvent* located_event = ui::LocatedEvent::FromIfValid(&event);
408   // Use default animation if location is out of bounds.
409   if (located_event && !View::HitTestPoint(located_event->location()))
410     located_event = nullptr;
411   AnimateInkDrop(views::InkDropState::ACTION_PENDING, located_event);
412 }
413 
AddLayerBeneathView(ui::Layer * layer)414 void NotificationInputContainerMD::AddLayerBeneathView(ui::Layer* layer) {
415   // When a ink drop layer is added it is stacked between the textfield/button
416   // and the parent (|this|). Since the ink drop is opaque, we have to paint the
417   // textfield/button on their own layers in otherwise they remain painted on
418   // |this|'s layer which would be covered by the ink drop.
419   textfield_->SetPaintToLayer();
420   textfield_->layer()->SetFillsBoundsOpaquely(false);
421   button_->SetPaintToLayer();
422   button_->layer()->SetFillsBoundsOpaquely(false);
423   ink_drop_container_->AddLayerBeneathView(layer);
424 }
425 
RemoveLayerBeneathView(ui::Layer * layer)426 void NotificationInputContainerMD::RemoveLayerBeneathView(ui::Layer* layer) {
427   ink_drop_container_->RemoveLayerBeneathView(layer);
428   textfield_->DestroyLayer();
429   button_->DestroyLayer();
430 }
431 
432 std::unique_ptr<views::InkDropRipple>
CreateInkDropRipple() const433 NotificationInputContainerMD::CreateInkDropRipple() const {
434   return std::make_unique<views::FloodFillInkDropRipple>(
435       size(), GetInkDropCenterBasedOnLastEvent(), GetInkDropBaseColor(),
436       ink_drop_visible_opacity());
437 }
438 
GetInkDropBaseColor() const439 SkColor NotificationInputContainerMD::GetInkDropBaseColor() const {
440   return gfx::kGoogleBlue600;
441 }
442 
HandleKeyEvent(views::Textfield * sender,const ui::KeyEvent & event)443 bool NotificationInputContainerMD::HandleKeyEvent(views::Textfield* sender,
444                                                   const ui::KeyEvent& event) {
445   if (event.type() == ui::ET_KEY_PRESSED &&
446       event.key_code() == ui::VKEY_RETURN) {
447     delegate_->OnNotificationInputSubmit(
448         textfield_->GetProperty(kTextfieldIndexKey), textfield_->GetText());
449     textfield_->SetText(base::string16());
450     return true;
451   }
452   return event.type() == ui::ET_KEY_RELEASED;
453 }
454 
OnAfterUserAction(views::Textfield * sender)455 void NotificationInputContainerMD::OnAfterUserAction(views::Textfield* sender) {
456   DCHECK_EQ(sender, textfield_);
457   button_->SetImage(
458       views::Button::STATE_NORMAL,
459       gfx::CreateVectorIcon(kNotificationInlineReplyIcon, kInputReplyButtonSize,
460                             textfield_->GetText().empty()
461                                 ? kTextfieldPlaceholderIconColorMD
462                                 : SK_ColorWHITE));
463 }
464 
ButtonPressed(views::Button * sender,const ui::Event & event)465 void NotificationInputContainerMD::ButtonPressed(views::Button* sender,
466                                                  const ui::Event& event) {
467   if (sender == button_) {
468     delegate_->OnNotificationInputSubmit(
469         textfield_->GetProperty(kTextfieldIndexKey), textfield_->GetText());
470   }
471 }
472 
473 // InlineSettingsRadioButton ///////////////////////////////////////////////////
474 
475 class InlineSettingsRadioButton : public views::RadioButton {
476  public:
InlineSettingsRadioButton(const base::string16 & label_text)477   explicit InlineSettingsRadioButton(const base::string16& label_text)
478       : views::RadioButton(label_text, 1 /* group */) {
479     SetEnabledTextColors(kRegularTextColorMD);
480     label()->SetFontList(GetTextFontList());
481     label()->SetBackgroundColor(kInlineSettingsBackgroundColor);
482     label()->SetSubpixelRenderingEnabled(false);
483   }
484 
485  private:
486   // views::RadioButton:
GetIconImageColor(int icon_state) const487   SkColor GetIconImageColor(int icon_state) const override {
488     return (icon_state & IconState::CHECKED) ? kActionButtonTextColor
489                                              : kRegularTextColorMD;
490   }
491 };
492 
493 // NotificationInkDropImpl /////////////////////////////////////////////////////
494 
495 class NotificationInkDropImpl : public views::InkDropImpl {
496  public:
NotificationInkDropImpl(views::InkDropHostView * ink_drop_host,const gfx::Size & host_size)497   NotificationInkDropImpl(views::InkDropHostView* ink_drop_host,
498                           const gfx::Size& host_size)
499       : views::InkDropImpl(ink_drop_host, host_size) {
500     SetAutoHighlightMode(views::InkDropImpl::AutoHighlightMode::SHOW_ON_RIPPLE);
501   }
502 
HostSizeChanged(const gfx::Size & new_size)503   void HostSizeChanged(const gfx::Size& new_size) override {
504     // Prevent a call to InkDropImpl::HostSizeChanged which recreates the ripple
505     // and stops the currently active animation: http://crbug.com/915222.
506   }
507 };
508 
509 // ////////////////////////////////////////////////////////////
510 // NotificationViewMD
511 // ////////////////////////////////////////////////////////////
512 
513 class NotificationViewMD::NotificationViewMDPathGenerator
514     : public views::HighlightPathGenerator {
515  public:
516   NotificationViewMDPathGenerator() = default;
517   NotificationViewMDPathGenerator(const NotificationViewMDPathGenerator&) =
518       delete;
519   NotificationViewMDPathGenerator& operator=(
520       const NotificationViewMDPathGenerator&) = delete;
521 
522   // views::HighlightPathGenerator:
GetRoundRect(const gfx::RectF & rect)523   base::Optional<RoundRect> GetRoundRect(const gfx::RectF& rect) override {
524     RoundRect round_rect;
525     round_rect.bounds = rect;
526     if (!preferred_size_.IsEmpty())
527       round_rect.bounds.set_size(gfx::SizeF(preferred_size_));
528     round_rect.corner_radius = gfx::RoundedCornersF(
529         top_radius_, top_radius_, bottom_radius_, bottom_radius_);
530     return round_rect;
531   }
532 
set_top_radius(int val)533   void set_top_radius(int val) { top_radius_ = val; }
set_bottom_radius(int val)534   void set_bottom_radius(int val) { bottom_radius_ = val; }
set_preferred_size(const gfx::Size & val)535   void set_preferred_size(const gfx::Size& val) { preferred_size_ = val; }
536 
537  private:
538   int top_radius_ = 0;
539   int bottom_radius_ = 0;
540 
541   // This custom PathGenerator is used for the ink drop clipping bounds. By
542   // setting |preferred_size_| we set the correct clip bounds in
543   // GetRoundRect(). This is needed as the correct bounds for the ink drop are
544   // required before a Layout() on the view is run. See
545   // http://crbug.com/915222.
546   gfx::Size preferred_size_;
547 };
548 
CreateOrUpdateViews(const Notification & notification)549 void NotificationViewMD::CreateOrUpdateViews(const Notification& notification) {
550   left_content_count_ = 0;
551 
552   CreateOrUpdateContextTitleView(notification);
553   CreateOrUpdateTitleView(notification);
554   CreateOrUpdateMessageView(notification);
555   CreateOrUpdateCompactTitleMessageView(notification);
556   CreateOrUpdateProgressBarView(notification);
557   CreateOrUpdateProgressStatusView(notification);
558   CreateOrUpdateListItemViews(notification);
559   CreateOrUpdateIconView(notification);
560   CreateOrUpdateSmallIconView(notification);
561   CreateOrUpdateImageView(notification);
562   CreateOrUpdateInlineSettingsViews(notification);
563   UpdateViewForExpandedState(expanded_);
564   // Should be called at the last because SynthesizeMouseMoveEvent() requires
565   // everything is in the right location when called.
566   CreateOrUpdateActionButtonViews(notification);
567 }
568 
NotificationViewMD(const Notification & notification)569 NotificationViewMD::NotificationViewMD(const Notification& notification)
570     : MessageView(notification),
571       ink_drop_container_(new views::InkDropContainerView()) {
572   SetLayoutManager(std::make_unique<views::BoxLayout>(
573       views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0));
574 
575   set_ink_drop_visible_opacity(1);
576 
577   AddChildView(ink_drop_container_);
578 
579   control_buttons_view_ =
580       std::make_unique<NotificationControlButtonsView>(this);
581   control_buttons_view_->set_owned_by_client();
582 
583   // |header_row_| contains app_icon, app_name, control buttons, etc...
584   header_row_ = new NotificationHeaderView(this);
585   header_row_->AddChildView(control_buttons_view_.get());
586   AddChildView(header_row_);
587 
588   // |content_row_| contains title, message, image, progressbar, etc...
589   content_row_ = new views::View();
590   auto* content_row_layout =
591       content_row_->SetLayoutManager(std::make_unique<views::BoxLayout>(
592           views::BoxLayout::Orientation::kHorizontal, kContentRowPadding, 0));
593   content_row_layout->set_cross_axis_alignment(
594       views::BoxLayout::CrossAxisAlignment::kStart);
595   AddChildView(content_row_);
596 
597   // |left_content_| contains most contents like title, message, etc...
598   left_content_ = new views::View();
599   left_content_->SetLayoutManager(std::make_unique<views::BoxLayout>(
600       views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0));
601   left_content_->SetBorder(views::CreateEmptyBorder(kLeftContentPadding));
602   content_row_->AddChildView(left_content_);
603   content_row_layout->SetFlexForView(left_content_, 1);
604 
605   // |right_content_| contains notification icon and small image.
606   right_content_ = new views::View();
607   right_content_->SetLayoutManager(std::make_unique<views::FillLayout>());
608   content_row_->AddChildView(right_content_);
609 
610   // |action_row_| contains inline action buttons and inline textfield.
611   actions_row_ = new views::View();
612   actions_row_->SetVisible(false);
613   actions_row_->SetLayoutManager(std::make_unique<views::FillLayout>());
614   AddChildView(actions_row_);
615 
616   // |action_buttons_row_| contains inline action buttons.
617   action_buttons_row_ = new views::View();
618   action_buttons_row_->SetLayoutManager(std::make_unique<views::BoxLayout>(
619       views::BoxLayout::Orientation::kHorizontal, kActionsRowPadding,
620       kActionsRowHorizontalSpacing));
621   action_buttons_row_->SetVisible(false);
622   actions_row_->AddChildView(action_buttons_row_);
623 
624   // |inline_reply_| is a container for an inline textfield.
625   inline_reply_ = new NotificationInputContainerMD(this);
626   inline_reply_->SetVisible(false);
627   actions_row_->AddChildView(inline_reply_);
628 
629   CreateOrUpdateViews(notification);
630   UpdateControlButtonsVisibilityWithNotification(notification);
631 
632   set_notify_enter_exit_on_child(true);
633 
634   click_activator_ = std::make_unique<ClickActivator>(this);
635   // Reasons to use pretarget handler instead of OnMousePressed:
636   // - NotificationViewMD::OnMousePresssed would not fire on the inline reply
637   //   textfield click in native notification.
638   // - To make it look similar to ArcNotificationContentView::EventForwarder.
639   AddPreTargetHandler(click_activator_.get());
640 
641   auto highlight_path_generator =
642       std::make_unique<NotificationViewMDPathGenerator>();
643   highlight_path_generator_ = highlight_path_generator.get();
644   views::HighlightPathGenerator::Install(this,
645                                          std::move(highlight_path_generator));
646 
647   UpdateCornerRadius(kNotificationCornerRadius, kNotificationCornerRadius);
648 }
649 
~NotificationViewMD()650 NotificationViewMD::~NotificationViewMD() {
651   RemovePreTargetHandler(click_activator_.get());
652 }
653 
AddLayerBeneathView(ui::Layer * layer)654 void NotificationViewMD::AddLayerBeneathView(ui::Layer* layer) {
655   GetInkDrop()->AddObserver(this);
656   for (auto* child : GetChildrenForLayerAdjustment()) {
657     child->SetPaintToLayer();
658     child->layer()->SetFillsBoundsOpaquely(false);
659   }
660   ink_drop_container_->AddLayerBeneathView(layer);
661 }
662 
RemoveLayerBeneathView(ui::Layer * layer)663 void NotificationViewMD::RemoveLayerBeneathView(ui::Layer* layer) {
664   ink_drop_container_->RemoveLayerBeneathView(layer);
665   for (auto* child : GetChildrenForLayerAdjustment())
666     child->DestroyLayer();
667   GetInkDrop()->RemoveObserver(this);
668 }
669 
Layout()670 void NotificationViewMD::Layout() {
671   MessageView::Layout();
672 
673   // We need to call IsExpandable() at the end of Layout() call, since whether
674   // we should show expand button or not depends on the current view layout.
675   // (e.g. Show expand button when |message_view_| exceeds one line.)
676   header_row_->SetExpandButtonEnabled(IsExpandable());
677   header_row_->Layout();
678 
679   // The notification background is rounded in MessageView::Layout(),
680   // but we also have to round the actions row background here.
681   if (actions_row_->GetVisible()) {
682     constexpr SkScalar kCornerRadius = SkIntToScalar(kNotificationCornerRadius);
683 
684     // Use vertically larger clip path, so that actions row's top coners will
685     // not be rounded.
686     SkPath path;
687     gfx::Rect bounds = actions_row_->GetLocalBounds();
688     bounds.set_y(bounds.y() - bounds.height());
689     bounds.set_height(bounds.height() * 2);
690     path.addRoundRect(gfx::RectToSkRect(bounds), kCornerRadius, kCornerRadius);
691 
692     action_buttons_row_->SetClipPath(path);
693     inline_reply_->SetClipPath(path);
694   }
695 
696   // The animation is needed to run inside of the border.
697   ink_drop_container_->SetBoundsRect(GetLocalBounds());
698 }
699 
OnFocus()700 void NotificationViewMD::OnFocus() {
701   MessageView::OnFocus();
702   ScrollRectToVisible(GetLocalBounds());
703 }
704 
OnMousePressed(const ui::MouseEvent & event)705 bool NotificationViewMD::OnMousePressed(const ui::MouseEvent& event) {
706   last_mouse_pressed_timestamp_ = base::TimeTicks(event.time_stamp());
707   return true;
708 }
709 
OnMouseDragged(const ui::MouseEvent & event)710 bool NotificationViewMD::OnMouseDragged(const ui::MouseEvent& event) {
711   return true;
712 }
713 
OnMouseReleased(const ui::MouseEvent & event)714 void NotificationViewMD::OnMouseReleased(const ui::MouseEvent& event) {
715   if (!event.IsOnlyLeftMouseButton())
716     return;
717 
718   // The mouse has been clicked for a long time.
719   if (ui::EventTimeStampToSeconds(event.time_stamp()) -
720           ui::EventTimeStampToSeconds(last_mouse_pressed_timestamp_) >
721       ui::GetGestureProviderConfig(
722           ui::GestureProviderConfigType::CURRENT_PLATFORM)
723           .gesture_detector_config.longpress_timeout.InSecondsF()) {
724     ToggleInlineSettings(event);
725     return;
726   }
727 
728   // Ignore click of actions row outside action buttons.
729   if (expanded_) {
730     DCHECK(actions_row_);
731     gfx::Point point_in_child = event.location();
732     ConvertPointToTarget(this, actions_row_, &point_in_child);
733     if (actions_row_->HitTestPoint(point_in_child))
734       return;
735   }
736 
737   // Ignore clicks of outside region when inline settings is shown.
738   if (settings_row_ && settings_row_->GetVisible())
739     return;
740 
741   MessageView::OnMouseReleased(event);
742 }
743 
OnMouseEvent(ui::MouseEvent * event)744 void NotificationViewMD::OnMouseEvent(ui::MouseEvent* event) {
745   switch (event->type()) {
746     case ui::ET_MOUSE_ENTERED:
747       UpdateControlButtonsVisibility();
748       break;
749     case ui::ET_MOUSE_EXITED:
750       UpdateControlButtonsVisibility();
751       break;
752     default:
753       break;
754   }
755   View::OnMouseEvent(event);
756 }
757 
OnGestureEvent(ui::GestureEvent * event)758 void NotificationViewMD::OnGestureEvent(ui::GestureEvent* event) {
759   if (event->type() == ui::ET_GESTURE_LONG_TAP) {
760     ToggleInlineSettings(*event);
761     return;
762   }
763   MessageView::OnGestureEvent(event);
764 }
765 
PreferredSizeChanged()766 void NotificationViewMD::PreferredSizeChanged() {
767   highlight_path_generator_->set_preferred_size(GetPreferredSize());
768   MessageView::PreferredSizeChanged();
769 }
770 
UpdateWithNotification(const Notification & notification)771 void NotificationViewMD::UpdateWithNotification(
772     const Notification& notification) {
773   MessageView::UpdateWithNotification(notification);
774   UpdateControlButtonsVisibilityWithNotification(notification);
775 
776   CreateOrUpdateViews(notification);
777   Layout();
778   SchedulePaint();
779 }
780 
781 // TODO(yoshiki): Move this to the parent class (MessageView).
UpdateControlButtonsVisibilityWithNotification(const Notification & notification)782 void NotificationViewMD::UpdateControlButtonsVisibilityWithNotification(
783     const Notification& notification) {
784   control_buttons_view_->ShowSettingsButton(
785       notification.should_show_settings_button());
786   control_buttons_view_->ShowSnoozeButton(
787       notification.should_show_snooze_button());
788   control_buttons_view_->ShowCloseButton(GetMode() != Mode::PINNED);
789   UpdateControlButtonsVisibility();
790 }
791 
ButtonPressed(views::Button * sender,const ui::Event & event)792 void NotificationViewMD::ButtonPressed(views::Button* sender,
793                                        const ui::Event& event) {
794   // Tapping anywhere on |header_row_| can expand the notification, though only
795   // |expand_button| can be focused by TAB.
796   if (sender == header_row_) {
797     if (IsExpandable() && content_row_->GetVisible()) {
798       SetManuallyExpandedOrCollapsed(true);
799       auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
800       ToggleExpanded();
801       // Check |this| is valid before continuing, because ToggleExpanded() might
802       // cause |this| to be deleted.
803       if (!weak_ptr)
804         return;
805       Layout();
806       SchedulePaint();
807     }
808     return;
809   }
810 
811   // See if the button pressed was an action button.
812   for (size_t i = 0; i < action_buttons_.size(); ++i) {
813     if (sender != action_buttons_[i])
814       continue;
815 
816     const base::Optional<base::string16>& placeholder =
817         action_buttons_[i]->placeholder();
818     if (placeholder) {
819       inline_reply_->textfield()->SetProperty(kTextfieldIndexKey,
820                                               static_cast<int>(i));
821       inline_reply_->textfield()->SetPlaceholderText(
822           placeholder->empty()
823               ? l10n_util::GetStringUTF16(
824                     IDS_MESSAGE_CENTER_NOTIFICATION_INLINE_REPLY_PLACEHOLDER)
825               : *placeholder);
826       inline_reply_->AnimateBackground(event);
827       inline_reply_->SetVisible(true);
828       action_buttons_row_->SetVisible(false);
829       // RequestFocus() should be called after SetVisible().
830       inline_reply_->textfield()->RequestFocus();
831       Layout();
832       SchedulePaint();
833     } else {
834       MessageCenter::Get()->ClickOnNotificationButton(notification_id(), i);
835     }
836     return;
837   }
838 
839   if (sender == settings_done_button_) {
840     ToggleInlineSettings(event);
841     return;
842   }
843 }
844 
OnNotificationInputSubmit(size_t index,const base::string16 & text)845 void NotificationViewMD::OnNotificationInputSubmit(size_t index,
846                                                    const base::string16& text) {
847   MessageCenter::Get()->ClickOnNotificationButtonWithReply(notification_id(),
848                                                            index, text);
849 }
850 
CreateOrUpdateContextTitleView(const Notification & notification)851 void NotificationViewMD::CreateOrUpdateContextTitleView(
852     const Notification& notification) {
853   header_row_->SetAccentColor(notification.accent_color() == SK_ColorTRANSPARENT
854                                   ? kNotificationDefaultAccentColor
855                                   : notification.accent_color());
856   header_row_->SetBackgroundColor(kNotificationBackgroundColor);
857   header_row_->SetTimestamp(notification.timestamp());
858   header_row_->SetAppNameElideBehavior(gfx::ELIDE_TAIL);
859   header_row_->SetSummaryText(base::string16());
860 
861   base::string16 app_name;
862   if (notification.UseOriginAsContextMessage()) {
863     app_name = url_formatter::FormatUrlForSecurityDisplay(
864         notification.origin_url(),
865         url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
866     header_row_->SetAppNameElideBehavior(gfx::ELIDE_HEAD);
867   } else if (notification.display_source().empty() &&
868              notification.notifier_id().type ==
869                  NotifierType::SYSTEM_COMPONENT) {
870     app_name = MessageCenter::Get()->GetSystemNotificationAppName();
871   } else if (!notification.context_message().empty()) {
872     app_name = notification.context_message();
873   } else {
874     app_name = notification.display_source();
875   }
876   header_row_->SetAppName(app_name);
877 }
878 
CreateOrUpdateTitleView(const Notification & notification)879 void NotificationViewMD::CreateOrUpdateTitleView(
880     const Notification& notification) {
881   if (notification.title().empty() ||
882       notification.type() == NOTIFICATION_TYPE_PROGRESS) {
883     DCHECK(!title_view_ || left_content_->Contains(title_view_));
884     delete title_view_;
885     title_view_ = nullptr;
886     return;
887   }
888 
889   int title_character_limit =
890       kNotificationWidth * kMaxTitleLines / kMinPixelsPerTitleCharacterMD;
891 
892   base::string16 title = gfx::TruncateString(
893       notification.title(), title_character_limit, gfx::WORD_BREAK);
894   if (!title_view_) {
895     const gfx::FontList& font_list = GetTextFontList();
896 
897     title_view_ = new views::Label(title);
898     title_view_->SetFontList(font_list);
899     title_view_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
900     title_view_->SetEnabledColor(kRegularTextColorMD);
901     title_view_->SetBackgroundColor(kNotificationBackgroundColor);
902     title_view_->SetLineHeight(kLineHeightMD);
903     // TODO(knollr): multiline should not be required, but we need to set the
904     // width of |title_view_| (because of crbug.com/682266), which only works in
905     // multiline mode.
906     title_view_->SetMultiLine(true);
907     title_view_->SetMaxLines(kMaxLinesForTitleView);
908     title_view_->SetAllowCharacterBreak(true);
909     left_content_->AddChildViewAt(title_view_, left_content_count_);
910   } else {
911     title_view_->SetText(title);
912   }
913 
914   left_content_count_++;
915 }
916 
CreateOrUpdateMessageView(const Notification & notification)917 void NotificationViewMD::CreateOrUpdateMessageView(
918     const Notification& notification) {
919   if (notification.type() == NOTIFICATION_TYPE_PROGRESS ||
920       notification.message().empty()) {
921     // Deletion will also remove |message_view_| from its parent.
922     delete message_view_;
923     message_view_ = nullptr;
924     return;
925   }
926 
927   base::string16 text = gfx::TruncateString(
928       notification.message(), kMessageCharacterLimitMD, gfx::WORD_BREAK);
929 
930   if (!message_view_) {
931     const gfx::FontList& font_list = GetTextFontList();
932 
933     message_view_ = new views::Label(text);
934     message_view_->SetFontList(font_list);
935     message_view_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
936     message_view_->SetEnabledColor(kDimTextColorMD);
937     message_view_->SetBackgroundColor(kNotificationBackgroundColor);
938     message_view_->SetLineHeight(kLineHeightMD);
939     message_view_->SetMultiLine(true);
940     message_view_->SetMaxLines(kMaxLinesForMessageView);
941     message_view_->SetAllowCharacterBreak(true);
942     left_content_->AddChildViewAt(message_view_, left_content_count_);
943   } else {
944     message_view_->SetText(text);
945   }
946 
947   message_view_->SetVisible(notification.items().empty());
948   left_content_count_++;
949 }
950 
CreateOrUpdateCompactTitleMessageView(const Notification & notification)951 void NotificationViewMD::CreateOrUpdateCompactTitleMessageView(
952     const Notification& notification) {
953   if (notification.type() != NOTIFICATION_TYPE_PROGRESS) {
954     DCHECK(!compact_title_message_view_ ||
955            left_content_->Contains(compact_title_message_view_));
956     delete compact_title_message_view_;
957     compact_title_message_view_ = nullptr;
958     return;
959   }
960   if (!compact_title_message_view_) {
961     compact_title_message_view_ = new CompactTitleMessageView();
962     left_content_->AddChildViewAt(compact_title_message_view_,
963                                   left_content_count_);
964   }
965 
966   compact_title_message_view_->set_title(notification.title());
967   compact_title_message_view_->set_message(notification.message());
968   left_content_->InvalidateLayout();
969   left_content_count_++;
970 }
971 
CreateOrUpdateProgressBarView(const Notification & notification)972 void NotificationViewMD::CreateOrUpdateProgressBarView(
973     const Notification& notification) {
974   if (notification.type() != NOTIFICATION_TYPE_PROGRESS) {
975     DCHECK(!progress_bar_view_ || left_content_->Contains(progress_bar_view_));
976     delete progress_bar_view_;
977     progress_bar_view_ = nullptr;
978     return;
979   }
980 
981   DCHECK(left_content_);
982 
983   if (!progress_bar_view_) {
984     progress_bar_view_ = new views::ProgressBar(kProgressBarHeight,
985                                                 /* allow_round_corner */ false);
986     progress_bar_view_->SetBorder(
987         views::CreateEmptyBorder(kProgressBarTopPadding, 0, 0, 0));
988     progress_bar_view_->SetForegroundColor(kActionButtonTextColor);
989     left_content_->AddChildViewAt(progress_bar_view_, left_content_count_);
990   }
991 
992   progress_bar_view_->SetValue(notification.progress() / 100.0);
993   progress_bar_view_->SetVisible(notification.items().empty());
994 
995   if (0 <= notification.progress() && notification.progress() <= 100)
996     header_row_->SetProgress(notification.progress());
997 
998   left_content_count_++;
999 }
1000 
CreateOrUpdateProgressStatusView(const Notification & notification)1001 void NotificationViewMD::CreateOrUpdateProgressStatusView(
1002     const Notification& notification) {
1003   if (notification.type() != NOTIFICATION_TYPE_PROGRESS ||
1004       notification.progress_status().empty()) {
1005     if (!status_view_)
1006       return;
1007     DCHECK(left_content_->Contains(status_view_));
1008     delete status_view_;
1009     status_view_ = nullptr;
1010     return;
1011   }
1012 
1013   if (!status_view_) {
1014     const gfx::FontList& font_list = GetTextFontList();
1015     status_view_ = new views::Label();
1016     status_view_->SetFontList(font_list);
1017     status_view_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1018     status_view_->SetEnabledColor(kDimTextColorMD);
1019     status_view_->SetBackgroundColor(kNotificationBackgroundColor);
1020     status_view_->SetBorder(views::CreateEmptyBorder(kStatusTextPadding));
1021     left_content_->AddChildViewAt(status_view_, left_content_count_);
1022   }
1023 
1024   status_view_->SetText(notification.progress_status());
1025   left_content_count_++;
1026 }
1027 
CreateOrUpdateListItemViews(const Notification & notification)1028 void NotificationViewMD::CreateOrUpdateListItemViews(
1029     const Notification& notification) {
1030   for (auto* item_view : item_views_)
1031     delete item_view;
1032   item_views_.clear();
1033 
1034   const std::vector<NotificationItem>& items = notification.items();
1035 
1036   for (size_t i = 0; i < items.size() && i < kMaxLinesForExpandedMessageView;
1037        ++i) {
1038     std::unique_ptr<views::View> item_view = CreateItemView(items[i]);
1039     item_views_.push_back(item_view.get());
1040     left_content_->AddChildViewAt(item_view.release(), left_content_count_++);
1041   }
1042 
1043   list_items_count_ = items.size();
1044 
1045   // Needed when CreateOrUpdateViews is called for update.
1046   if (!item_views_.empty())
1047     left_content_->InvalidateLayout();
1048 }
1049 
CreateOrUpdateIconView(const Notification & notification)1050 void NotificationViewMD::CreateOrUpdateIconView(
1051     const Notification& notification) {
1052   const bool use_image_for_icon = notification.icon().IsEmpty();
1053 
1054   gfx::ImageSkia icon = use_image_for_icon ? notification.image().AsImageSkia()
1055                                            : notification.icon().AsImageSkia();
1056 
1057   if (notification.type() == NOTIFICATION_TYPE_PROGRESS ||
1058       notification.type() == NOTIFICATION_TYPE_MULTIPLE || icon.isNull()) {
1059     DCHECK(!icon_view_ || right_content_->Contains(icon_view_));
1060     delete icon_view_;
1061     icon_view_ = nullptr;
1062     return;
1063   }
1064 
1065   if (!icon_view_) {
1066     icon_view_ = new ProportionalImageView(kIconViewSize);
1067     right_content_->AddChildView(icon_view_);
1068   }
1069 
1070   icon_view_->SetImage(icon, icon.size());
1071 
1072   // Hide the icon on the right side when the notification is expanded.
1073   hide_icon_on_expanded_ = use_image_for_icon;
1074 }
1075 
CreateOrUpdateSmallIconView(const Notification & notification)1076 void NotificationViewMD::CreateOrUpdateSmallIconView(
1077     const Notification& notification) {
1078   // TODO(knollr): figure out if this has a performance impact and
1079   // cache images if so. (crbug.com/768748)
1080   gfx::Image masked_small_icon = notification.GenerateMaskedSmallIcon(
1081       kSmallImageSizeMD, notification.accent_color() == SK_ColorTRANSPARENT
1082                              ? message_center::kNotificationDefaultAccentColor
1083                              : notification.accent_color());
1084 
1085   if (masked_small_icon.IsEmpty()) {
1086     header_row_->ClearAppIcon();
1087   } else {
1088     header_row_->SetAppIcon(masked_small_icon.AsImageSkia());
1089   }
1090 }
1091 
CreateOrUpdateImageView(const Notification & notification)1092 void NotificationViewMD::CreateOrUpdateImageView(
1093     const Notification& notification) {
1094   if (notification.image().IsEmpty()) {
1095     if (image_container_view_) {
1096       DCHECK(Contains(image_container_view_));
1097       delete image_container_view_;
1098       image_container_view_ = nullptr;
1099     }
1100     return;
1101   }
1102 
1103   if (!image_container_view_) {
1104     image_container_view_ = new views::View();
1105     image_container_view_->SetLayoutManager(
1106         std::make_unique<views::FillLayout>());
1107     image_container_view_->SetBorder(
1108         views::CreateEmptyBorder(kLargeImageContainerPadding));
1109     image_container_view_->SetBackground(
1110         views::CreateSolidBackground(kImageBackgroundColor));
1111     image_container_view_->AddChildView(new LargeImageView());
1112 
1113     // Insert the created image container just after the |content_row_|.
1114     AddChildViewAt(image_container_view_, GetIndexOf(content_row_) + 1);
1115   }
1116 
1117   static_cast<LargeImageView*>(image_container_view_->children().front())
1118       ->SetImage(notification.image().AsImageSkia());
1119 }
1120 
CreateOrUpdateActionButtonViews(const Notification & notification)1121 void NotificationViewMD::CreateOrUpdateActionButtonViews(
1122     const Notification& notification) {
1123   const std::vector<ButtonInfo>& buttons = notification.buttons();
1124   bool new_buttons = action_buttons_.size() != buttons.size();
1125 
1126   if (new_buttons || buttons.empty()) {
1127     for (auto* item : action_buttons_)
1128       delete item;
1129     action_buttons_.clear();
1130     actions_row_->SetVisible(expanded_ && !buttons.empty());
1131   }
1132 
1133   DCHECK_EQ(this, actions_row_->parent());
1134 
1135   // Hide inline reply field if it doesn't exist anymore.
1136   if (inline_reply_->GetVisible()) {
1137     const size_t index =
1138         inline_reply_->textfield()->GetProperty(kTextfieldIndexKey);
1139     if (index >= buttons.size() || !buttons[index].placeholder.has_value()) {
1140       action_buttons_row_->SetVisible(true);
1141       inline_reply_->SetVisible(false);
1142     }
1143   }
1144 
1145   for (size_t i = 0; i < buttons.size(); ++i) {
1146     ButtonInfo button_info = buttons[i];
1147     if (new_buttons) {
1148       NotificationButtonMD* button = new NotificationButtonMD(
1149           this, button_info.title, button_info.placeholder);
1150       action_buttons_.push_back(button);
1151       action_buttons_row_->AddChildView(button);
1152     } else {
1153       action_buttons_[i]->SetText(button_info.title);
1154       action_buttons_[i]->set_placeholder(button_info.placeholder);
1155       action_buttons_[i]->SchedulePaint();
1156       action_buttons_[i]->Layout();
1157     }
1158 
1159     // Change action button color to the accent color.
1160     action_buttons_[i]->SetEnabledTextColors(notification.accent_color() ==
1161                                                      SK_ColorTRANSPARENT
1162                                                  ? kActionButtonTextColor
1163                                                  : notification.accent_color());
1164   }
1165 
1166   // Inherit mouse hover state when action button views reset.
1167   // If the view is not expanded, there should be no hover state.
1168   if (new_buttons && expanded_) {
1169     views::Widget* widget = GetWidget();
1170     if (widget) {
1171       // This Layout() is needed because button should be in the right location
1172       // in the view hierarchy when SynthesizeMouseMoveEvent() is called.
1173       Layout();
1174       widget->SetSize(widget->GetContentsView()->GetPreferredSize());
1175       GetWidget()->SynthesizeMouseMoveEvent();
1176     }
1177   }
1178 }
1179 
CreateOrUpdateInlineSettingsViews(const Notification & notification)1180 void NotificationViewMD::CreateOrUpdateInlineSettingsViews(
1181     const Notification& notification) {
1182   if (settings_row_) {
1183     DCHECK_EQ(SettingsButtonHandler::INLINE,
1184               notification.rich_notification_data().settings_button_handler);
1185     return;
1186   }
1187 
1188   if (notification.rich_notification_data().settings_button_handler !=
1189       SettingsButtonHandler::INLINE) {
1190     return;
1191   }
1192 
1193   // |settings_row_| contains inline settings.
1194   settings_row_ = new views::View();
1195   settings_row_->SetLayoutManager(std::make_unique<views::BoxLayout>(
1196       views::BoxLayout::Orientation::kVertical, kSettingsRowPadding, 0));
1197 
1198   int block_notifications_message_id = 0;
1199   switch (notification.notifier_id().type) {
1200     case NotifierType::APPLICATION:
1201     case NotifierType::ARC_APPLICATION:
1202       block_notifications_message_id =
1203           IDS_MESSAGE_CENTER_BLOCK_ALL_NOTIFICATIONS_APP;
1204       break;
1205     case NotifierType::WEB_PAGE:
1206       block_notifications_message_id =
1207           IDS_MESSAGE_CENTER_BLOCK_ALL_NOTIFICATIONS_SITE;
1208       break;
1209     case NotifierType::SYSTEM_COMPONENT:
1210       block_notifications_message_id =
1211           IDS_MESSAGE_CENTER_BLOCK_ALL_NOTIFICATIONS;
1212       break;
1213     case NotifierType::CROSTINI_APPLICATION:
1214       NOTREACHED();
1215       break;
1216   }
1217   DCHECK_NE(block_notifications_message_id, 0);
1218 
1219   block_all_button_ = new InlineSettingsRadioButton(
1220       l10n_util::GetStringUTF16(block_notifications_message_id));
1221   block_all_button_->SetBorder(
1222       views::CreateEmptyBorder(kSettingsRadioButtonPadding));
1223   settings_row_->AddChildView(block_all_button_);
1224 
1225   dont_block_button_ = new InlineSettingsRadioButton(
1226       l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_DONT_BLOCK_NOTIFICATIONS));
1227   dont_block_button_->SetBorder(
1228       views::CreateEmptyBorder(kSettingsRadioButtonPadding));
1229   settings_row_->AddChildView(dont_block_button_);
1230   settings_row_->SetVisible(false);
1231 
1232   settings_done_button_ = new NotificationButtonMD(
1233       this, l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_SETTINGS_DONE),
1234       base::nullopt);
1235   settings_done_button_->SetTextSubpixelRenderingEnabled(false);
1236 
1237   auto* settings_button_row = new views::View;
1238   auto settings_button_layout = std::make_unique<views::BoxLayout>(
1239       views::BoxLayout::Orientation::kHorizontal, kSettingsButtonRowPadding, 0);
1240   settings_button_layout->set_main_axis_alignment(
1241       views::BoxLayout::MainAxisAlignment::kEnd);
1242   settings_button_row->SetLayoutManager(std::move(settings_button_layout));
1243   settings_button_row->AddChildView(settings_done_button_);
1244   settings_row_->AddChildView(settings_button_row);
1245 
1246   AddChildViewAt(settings_row_, GetIndexOf(actions_row_));
1247 }
1248 
IsExpandable()1249 bool NotificationViewMD::IsExpandable() {
1250   // Inline settings can not be expanded.
1251   if (GetMode() == Mode::SETTING)
1252     return false;
1253 
1254   // Expandable if the message exceeds one line.
1255   if (message_view_ && message_view_->GetVisible() &&
1256       message_view_->GetRequiredLines() > 1) {
1257     return true;
1258   }
1259   // Expandable if there is at least one inline action.
1260   if (!action_buttons_row_->children().empty())
1261     return true;
1262 
1263   // Expandable if the notification has image.
1264   if (image_container_view_)
1265     return true;
1266 
1267   // Expandable if there are multiple list items.
1268   if (item_views_.size() > 1)
1269     return true;
1270 
1271   // Expandable if both progress bar and status message exist.
1272   if (status_view_)
1273     return true;
1274 
1275   return false;
1276 }
1277 
ToggleExpanded()1278 void NotificationViewMD::ToggleExpanded() {
1279   SetExpanded(!expanded_);
1280 }
1281 
UpdateViewForExpandedState(bool expanded)1282 void NotificationViewMD::UpdateViewForExpandedState(bool expanded) {
1283   header_row_->SetExpanded(expanded);
1284   if (message_view_) {
1285     message_view_->SetMaxLines(expanded ? kMaxLinesForExpandedMessageView
1286                                         : kMaxLinesForMessageView);
1287   }
1288   if (image_container_view_)
1289     image_container_view_->SetVisible(expanded);
1290 
1291   actions_row_->SetVisible(expanded &&
1292                            !action_buttons_row_->children().empty());
1293   if (!expanded) {
1294     action_buttons_row_->SetVisible(true);
1295     inline_reply_->SetVisible(false);
1296   }
1297 
1298   for (size_t i = kMaxLinesForMessageView; i < item_views_.size(); ++i) {
1299     item_views_[i]->SetVisible(expanded);
1300   }
1301   if (status_view_)
1302     status_view_->SetVisible(expanded);
1303 
1304   int max_items = expanded ? item_views_.size() : kMaxLinesForMessageView;
1305   if (list_items_count_ > max_items)
1306     header_row_->SetOverflowIndicator(list_items_count_ - max_items);
1307   else if (!item_views_.empty())
1308     header_row_->SetSummaryText(base::string16());
1309 
1310   bool has_icon = icon_view_ && (!hide_icon_on_expanded_ || !expanded);
1311   right_content_->SetVisible(has_icon);
1312   left_content_->SetBorder(views::CreateEmptyBorder(
1313       has_icon ? kLeftContentPaddingWithIcon : kLeftContentPadding));
1314 
1315   // TODO(tetsui): Workaround https://crbug.com/682266 by explicitly setting
1316   // the width.
1317   // Ideally, we should fix the original bug, but it seems there's no obvious
1318   // solution for the bug according to https://crbug.com/678337#c7, we should
1319   // ensure that the change won't break any of the users of BoxLayout class.
1320   const int message_view_width =
1321       (has_icon ? kMessageViewWidthWithIcon : kMessageViewWidth) -
1322       GetInsets().width();
1323   if (title_view_)
1324     title_view_->SizeToFit(message_view_width);
1325   if (message_view_)
1326     message_view_->SizeToFit(message_view_width);
1327 
1328   content_row_->InvalidateLayout();
1329 }
1330 
ToggleInlineSettings(const ui::Event & event)1331 void NotificationViewMD::ToggleInlineSettings(const ui::Event& event) {
1332   if (!settings_row_)
1333     return;
1334 
1335   bool inline_settings_visible = !settings_row_->GetVisible();
1336   bool disable_notification =
1337       settings_row_->GetVisible() && block_all_button_->GetChecked();
1338 
1339   settings_row_->SetVisible(inline_settings_visible);
1340   content_row_->SetVisible(!inline_settings_visible);
1341   header_row_->SetDetailViewsVisible(!inline_settings_visible);
1342   header_row_->SetBackgroundColor(inline_settings_visible
1343                                       ? kInlineSettingsBackgroundColor
1344                                       : kNotificationBackgroundColor);
1345 
1346   // Always check "Don't block" when inline settings is shown.
1347   // If it's already blocked, users should not see inline settings.
1348   // Toggling should reset the state.
1349   dont_block_button_->SetChecked(true);
1350 
1351   SetSettingMode(inline_settings_visible);
1352 
1353   // Grab a weak pointer before calling SetExpanded() as it might cause |this|
1354   // to be deleted.
1355   {
1356     auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
1357     SetExpanded(!inline_settings_visible);
1358     if (!weak_ptr)
1359       return;
1360   }
1361 
1362   PreferredSizeChanged();
1363 
1364   if (inline_settings_visible)
1365     AddBackgroundAnimation(event);
1366   else
1367     RemoveBackgroundAnimation();
1368 
1369   Layout();
1370   SchedulePaint();
1371 
1372   // Call DisableNotification() at the end, because |this| can be deleted at any
1373   // point after it's called.
1374   if (disable_notification)
1375     MessageCenter::Get()->DisableNotification(notification_id());
1376 }
1377 
UpdateCornerRadius(int top_radius,int bottom_radius)1378 void NotificationViewMD::UpdateCornerRadius(int top_radius, int bottom_radius) {
1379   MessageView::UpdateCornerRadius(top_radius, bottom_radius);
1380   action_buttons_row_->SetBackground(views::CreateBackgroundFromPainter(
1381       std::make_unique<NotificationBackgroundPainter>(
1382           0, bottom_radius, kActionsRowBackgroundColor)));
1383   highlight_path_generator_->set_top_radius(top_radius);
1384   highlight_path_generator_->set_bottom_radius(bottom_radius);
1385 }
1386 
GetControlButtonsView() const1387 NotificationControlButtonsView* NotificationViewMD::GetControlButtonsView()
1388     const {
1389   return control_buttons_view_.get();
1390 }
1391 
IsExpanded() const1392 bool NotificationViewMD::IsExpanded() const {
1393   return expanded_;
1394 }
1395 
SetExpanded(bool expanded)1396 void NotificationViewMD::SetExpanded(bool expanded) {
1397   if (expanded_ == expanded)
1398     return;
1399   expanded_ = expanded;
1400 
1401   UpdateViewForExpandedState(expanded_);
1402   PreferredSizeChanged();
1403 }
1404 
IsManuallyExpandedOrCollapsed() const1405 bool NotificationViewMD::IsManuallyExpandedOrCollapsed() const {
1406   return manually_expanded_or_collapsed_;
1407 }
1408 
SetManuallyExpandedOrCollapsed(bool value)1409 void NotificationViewMD::SetManuallyExpandedOrCollapsed(bool value) {
1410   manually_expanded_or_collapsed_ = value;
1411 }
1412 
OnSettingsButtonPressed(const ui::Event & event)1413 void NotificationViewMD::OnSettingsButtonPressed(const ui::Event& event) {
1414   for (auto& observer : *observers())
1415     observer.OnSettingsButtonPressed(notification_id());
1416 
1417   if (settings_row_)
1418     ToggleInlineSettings(event);
1419   else
1420     MessageView::OnSettingsButtonPressed(event);
1421 }
1422 
Activate()1423 void NotificationViewMD::Activate() {
1424   GetWidget()->widget_delegate()->SetCanActivate(true);
1425   GetWidget()->Activate();
1426 }
1427 
AddBackgroundAnimation(const ui::Event & event)1428 void NotificationViewMD::AddBackgroundAnimation(const ui::Event& event) {
1429   SetInkDropMode(InkDropMode::ON_NO_GESTURE_HANDLER);
1430   // In case the animation is triggered from keyboard operation.
1431   if (!event.IsLocatedEvent()) {
1432     AnimateInkDrop(views::InkDropState::ACTION_PENDING, nullptr);
1433     return;
1434   }
1435 
1436   // Convert the point of |event| from the coordinate system of
1437   // |control_buttons_view_| to that of NotificationViewMD, create a new
1438   // LocatedEvent which has the new point.
1439   views::View* target = static_cast<views::View*>(event.target());
1440   const gfx::Point& location = event.AsLocatedEvent()->location();
1441   gfx::Point converted_location(location);
1442   View::ConvertPointToTarget(target, this, &converted_location);
1443 
1444   // Use default animation if location is out of bounds.
1445   if (!View::HitTestPoint(converted_location)) {
1446     AnimateInkDrop(views::InkDropState::ACTION_PENDING, nullptr);
1447     return;
1448   }
1449 
1450   std::unique_ptr<ui::Event> cloned_event = ui::Event::Clone(event);
1451   ui::LocatedEvent* cloned_located_event = cloned_event->AsLocatedEvent();
1452   cloned_located_event->set_location(converted_location);
1453   AnimateInkDrop(views::InkDropState::ACTION_PENDING, cloned_located_event);
1454 }
1455 
RemoveBackgroundAnimation()1456 void NotificationViewMD::RemoveBackgroundAnimation() {
1457   SetInkDropMode(InkDropMode::OFF);
1458   AnimateInkDrop(views::InkDropState::HIDDEN, nullptr);
1459 }
1460 
CreateInkDrop()1461 std::unique_ptr<views::InkDrop> NotificationViewMD::CreateInkDrop() {
1462   return std::make_unique<NotificationInkDropImpl>(this, size());
1463 }
1464 
CreateInkDropRipple() const1465 std::unique_ptr<views::InkDropRipple> NotificationViewMD::CreateInkDropRipple()
1466     const {
1467   return std::make_unique<views::FloodFillInkDropRipple>(
1468       GetPreferredSize(), GetInkDropCenterBasedOnLastEvent(),
1469       GetInkDropBaseColor(), ink_drop_visible_opacity());
1470 }
1471 
GetChildrenForLayerAdjustment() const1472 std::vector<views::View*> NotificationViewMD::GetChildrenForLayerAdjustment()
1473     const {
1474   return {header_row_, block_all_button_, dont_block_button_,
1475           settings_done_button_};
1476 }
1477 
CreateInkDropMask() const1478 std::unique_ptr<views::InkDropMask> NotificationViewMD::CreateInkDropMask()
1479     const {
1480   return nullptr;
1481 }
1482 
GetInkDropBaseColor() const1483 SkColor NotificationViewMD::GetInkDropBaseColor() const {
1484   return kInlineSettingsBackgroundColor;
1485 }
1486 
InkDropAnimationStarted()1487 void NotificationViewMD::InkDropAnimationStarted() {
1488   header_row_->SetSubpixelRenderingEnabled(false);
1489 }
1490 
InkDropRippleAnimationEnded(views::InkDropState ink_drop_state)1491 void NotificationViewMD::InkDropRippleAnimationEnded(
1492     views::InkDropState ink_drop_state) {
1493   if (ink_drop_state == views::InkDropState::HIDDEN)
1494     header_row_->SetSubpixelRenderingEnabled(true);
1495 }
1496 
1497 }  // namespace message_center
1498