1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/message_center/views/message_popup_collection.h"
6 
7 #include "base/bind.h"
8 #include "base/stl_util.h"
9 #include "base/threading/thread_task_runner_handle.h"
10 #include "ui/gfx/animation/linear_animation.h"
11 #include "ui/gfx/animation/tween.h"
12 #include "ui/message_center/message_center.h"
13 #include "ui/message_center/public/cpp/message_center_constants.h"
14 #include "ui/message_center/views/message_popup_view.h"
15 
16 namespace message_center {
17 
18 namespace {
19 
20 // Animation duration for FADE_IN and FADE_OUT.
21 constexpr base::TimeDelta kFadeInFadeOutDuration =
22     base::TimeDelta::FromMilliseconds(200);
23 
24 // Animation duration for MOVE_DOWN.
25 constexpr base::TimeDelta kMoveDownDuration =
26     base::TimeDelta::FromMilliseconds(120);
27 
28 }  // namespace
29 
MessagePopupCollection()30 MessagePopupCollection::MessagePopupCollection()
31     : animation_(std::make_unique<gfx::LinearAnimation>(this)),
32       weak_ptr_factory_(this) {
33   MessageCenter::Get()->AddObserver(this);
34 }
35 
~MessagePopupCollection()36 MessagePopupCollection::~MessagePopupCollection() {
37   for (const auto& item : popup_items_)
38     item.popup->Close();
39   MessageCenter::Get()->RemoveObserver(this);
40 }
41 
Update()42 void MessagePopupCollection::Update() {
43   if (is_updating_)
44     return;
45   base::AutoReset<bool> reset(&is_updating_, true);
46 
47   RemoveClosedPopupItems();
48 
49   if (MessageCenter::Get()->IsMessageCenterVisible()) {
50     CloseAllPopupsNow();
51     return;
52   }
53 
54   if (animation_->is_animating()) {
55     UpdateByAnimation();
56     return;
57   }
58 
59   if (state_ != State::IDLE)
60     TransitionFromAnimation();
61 
62   if (state_ == State::IDLE)
63     TransitionToAnimation();
64 
65   UpdatePopupTimers();
66 
67   if (state_ != State::IDLE) {
68     // If not in IDLE state, start animation.
69     animation_->SetDuration(state_ == State::MOVE_DOWN ||
70                                     state_ == State::MOVE_UP_FOR_INVERSE
71                                 ? kMoveDownDuration
72                                 : kFadeInFadeOutDuration);
73     animation_->Start();
74     UpdateByAnimation();
75   }
76 
77   DCHECK(state_ == State::IDLE || animation_->is_animating());
78 }
79 
ResetBounds()80 void MessagePopupCollection::ResetBounds() {
81   if (is_updating_)
82     return;
83   {
84     base::AutoReset<bool> reset(&is_updating_, true);
85 
86     RemoveClosedPopupItems();
87     ResetHotMode();
88     state_ = State::IDLE;
89     animation_->End();
90 
91     CalculateBounds();
92 
93     // Remove popups that are no longer in work area.
94     ClosePopupsOutsideWorkArea();
95 
96     // Reset bounds and opacity of popups.
97     for (auto& item : popup_items_) {
98       item.popup->SetPopupBounds(item.bounds);
99       item.popup->SetOpacity(1.0);
100     }
101   }
102 
103   // Restart animation for FADE_OUT.
104   Update();
105 }
106 
NotifyPopupResized()107 void MessagePopupCollection::NotifyPopupResized() {
108   resize_requested_ = true;
109   Update();
110 }
111 
NotifyPopupClosed(MessagePopupView * popup)112 void MessagePopupCollection::NotifyPopupClosed(MessagePopupView* popup) {
113   for (auto& item : popup_items_) {
114     if (item.popup == popup)
115       item.popup = nullptr;
116   }
117 }
118 
OnNotificationAdded(const std::string & notification_id)119 void MessagePopupCollection::OnNotificationAdded(
120     const std::string& notification_id) {
121   // Should not call MessagePopupCollection::Update here. Because notification
122   // may be removed before animation which is triggered by the previous
123   // operation on MessagePopupCollection ends. As result, when a new
124   // notification with the same ID is created, calling
125   // MessagePopupCollection::Update will not update the popup's content. Then
126   // the new notification popup fails to show. (see https://crbug.com/921402)
127   OnNotificationUpdated(notification_id);
128 }
129 
OnNotificationRemoved(const std::string & notification_id,bool by_user)130 void MessagePopupCollection::OnNotificationRemoved(
131     const std::string& notification_id,
132     bool by_user) {
133   Update();
134 }
135 
OnNotificationUpdated(const std::string & notification_id)136 void MessagePopupCollection::OnNotificationUpdated(
137     const std::string& notification_id) {
138   if (is_updating_)
139     return;
140 
141   // Find Notification object with |notification_id|.
142   const auto& notifications = MessageCenter::Get()->GetPopupNotifications();
143   auto it = notifications.begin();
144   while (it != notifications.end()) {
145     if ((*it)->id() == notification_id)
146       break;
147     ++it;
148   }
149 
150   if (it == notifications.end()) {
151     // If not found, probably |notification_id| is removed from popups by
152     // timeout.
153     Update();
154     return;
155   }
156 
157   {
158     base::AutoReset<bool> reset(&is_updating_, true);
159 
160     RemoveClosedPopupItems();
161 
162     // Update contents of the notification.
163     for (const auto& item : popup_items_) {
164       if (item.id == notification_id) {
165         item.popup->UpdateContents(**it);
166         break;
167       }
168     }
169   }
170 
171   Update();
172 }
173 
OnCenterVisibilityChanged(Visibility visibility)174 void MessagePopupCollection::OnCenterVisibilityChanged(Visibility visibility) {
175   Update();
176 }
177 
OnBlockingStateChanged(NotificationBlocker * blocker)178 void MessagePopupCollection::OnBlockingStateChanged(
179     NotificationBlocker* blocker) {
180   Update();
181 }
182 
AnimationEnded(const gfx::Animation * animation)183 void MessagePopupCollection::AnimationEnded(const gfx::Animation* animation) {
184   Update();
185 }
186 
AnimationProgressed(const gfx::Animation * animation)187 void MessagePopupCollection::AnimationProgressed(
188     const gfx::Animation* animation) {
189   Update();
190 }
191 
AnimationCanceled(const gfx::Animation * animation)192 void MessagePopupCollection::AnimationCanceled(
193     const gfx::Animation* animation) {
194   Update();
195 }
196 
GetPopupViewForNotificationID(const std::string & notification_id)197 MessagePopupView* MessagePopupCollection::GetPopupViewForNotificationID(
198     const std::string& notification_id) {
199   for (const auto& item : popup_items_) {
200     if (item.id == notification_id)
201       return item.popup;
202   }
203   return nullptr;
204 }
205 
CreatePopup(const Notification & notification)206 MessagePopupView* MessagePopupCollection::CreatePopup(
207     const Notification& notification) {
208   return new MessagePopupView(notification, this);
209 }
210 
RestartPopupTimers()211 void MessagePopupCollection::RestartPopupTimers() {
212   MessageCenter::Get()->RestartPopupTimers();
213 }
214 
PausePopupTimers()215 void MessagePopupCollection::PausePopupTimers() {
216   MessageCenter::Get()->PausePopupTimers();
217 }
218 
TransitionFromAnimation()219 void MessagePopupCollection::TransitionFromAnimation() {
220   DCHECK_NE(state_, State::IDLE);
221   DCHECK(!animation_->is_animating());
222 
223   // The animation of type |state_| is now finished.
224   UpdateByAnimation();
225 
226   // If FADE_OUT animation is finished, remove the animated popup.
227   if (state_ == State::FADE_OUT)
228     CloseAnimatingPopups();
229 
230   if (state_ == State::FADE_IN || state_ == State::MOVE_DOWN ||
231       (state_ == State::FADE_OUT && popup_items_.empty())) {
232     // If the animation is finished, transition to IDLE.
233     state_ = State::IDLE;
234   } else if (state_ == State::FADE_OUT && !popup_items_.empty()) {
235     if ((HasAddedPopup() && CollapseAllPopups()) || !inverse_) {
236       // If FADE_OUT animation is finished and we still have remaining popups,
237       // we have to MOVE_DOWN them.
238       // If we're going to add a new popup after this MOVE_DOWN, do the collapse
239       // animation at the same time. Otherwise it will take another MOVE_DOWN.
240       state_ = State::MOVE_DOWN;
241       MoveDownPopups();
242     } else {
243       // If there's no collapsable popups and |inverse_| is on, there's nothing
244       // to do after FADE_OUT.
245       state_ = State::IDLE;
246     }
247   } else if (state_ == State::MOVE_UP_FOR_INVERSE) {
248     for (auto& item : popup_items_)
249       item.is_animating = item.will_fade_in;
250     state_ = State::FADE_IN;
251   }
252 }
253 
TransitionToAnimation()254 void MessagePopupCollection::TransitionToAnimation() {
255   DCHECK_EQ(state_, State::IDLE);
256   DCHECK(!animation_->is_animating());
257 
258   if (HasRemovedPopup()) {
259     MarkRemovedPopup();
260 
261     // Start hot mode to allow a user to continually close many notifications.
262     StartHotMode();
263 
264     if (CloseTransparentPopups()) {
265       // If the popup is already transparent, skip FADE_OUT.
266       state_ = State::MOVE_DOWN;
267       MoveDownPopups();
268     } else {
269       state_ = State::FADE_OUT;
270     }
271     return;
272   }
273 
274   if (HasAddedPopup()) {
275     if (CollapseAllPopups()) {
276       // If we had existing popups that weren't collapsed, first show collapsing
277       // animation.
278       state_ = State::MOVE_DOWN;
279       MoveDownPopups();
280       return;
281     } else if (AddPopup()) {
282       // A popup is actually added.
283       if (inverse_ && popup_items_.size() > 1) {
284         // If |inverse_| is on and there are existing notifications that have to
285         // be moved up (existing ones + new one, so > 1), transition to
286         // MOVE_UP_FOR_INVERSE.
287         state_ = State::MOVE_UP_FOR_INVERSE;
288       } else {
289         // Show FADE_IN animation.
290         state_ = State::FADE_IN;
291       }
292       return;
293     }
294   }
295 
296   if (resize_requested_) {
297     // Resize is requested e.g. a user manually expanded notification.
298     resize_requested_ = false;
299     state_ = State::MOVE_DOWN;
300     MoveDownPopups();
301 
302     // This function may be called by a child MessageView when a notification is
303     // expanded by the user.  Deleting the pop-up should be delayed so we are
304     // out of the child view's call stack. See crbug.com/957033.
305     base::ThreadTaskRunnerHandle::Get()->PostTask(
306         FROM_HERE,
307         base::BindOnce(&MessagePopupCollection::ClosePopupsOutsideWorkArea,
308                        weak_ptr_factory_.GetWeakPtr()));
309     return;
310   }
311 
312   if (!IsAnyPopupHovered() && is_hot_) {
313     // Reset hot mode and animate to the normal positions.
314     state_ = State::MOVE_DOWN;
315     ResetHotMode();
316     MoveDownPopups();
317   }
318 }
319 
UpdatePopupTimers()320 void MessagePopupCollection::UpdatePopupTimers() {
321   if (state_ == State::IDLE) {
322     if (IsAnyPopupHovered() || IsAnyPopupActive()) {
323       // If any popup is hovered or activated, pause popup timer.
324       PausePopupTimers();
325     } else {
326       // If in IDLE state, restart popup timer.
327       RestartPopupTimers();
328     }
329   } else {
330     // If not in IDLE state, pause popup timer.
331     PausePopupTimers();
332   }
333 }
334 
CalculateBounds()335 void MessagePopupCollection::CalculateBounds() {
336   int base = GetBaseline();
337   for (size_t i = 0; i < popup_items_.size(); ++i) {
338     gfx::Size preferred_size(
339         kNotificationWidth,
340         GetPopupItem(i)->popup->GetHeightForWidth(kNotificationWidth));
341 
342     // Align the top of i-th popup to |hot_top_|.
343     if (is_hot_ && hot_index_ == i) {
344       base = hot_top_;
345       if (!IsTopDown())
346         base += preferred_size.height();
347     }
348 
349     int origin_x = GetToastOriginX(gfx::Rect(preferred_size));
350 
351     int origin_y = base;
352     if (!IsTopDown())
353       origin_y -= preferred_size.height();
354 
355     GetPopupItem(i)->start_bounds = GetPopupItem(i)->bounds;
356     GetPopupItem(i)->bounds =
357         gfx::Rect(gfx::Point(origin_x, origin_y), preferred_size);
358 
359     const int delta = preferred_size.height() + kMarginBetweenPopups;
360     if (IsTopDown())
361       base += delta;
362     else
363       base -= delta;
364   }
365 }
366 
UpdateByAnimation()367 void MessagePopupCollection::UpdateByAnimation() {
368   DCHECK_NE(state_, State::IDLE);
369 
370   for (auto& item : popup_items_) {
371     if (!item.is_animating)
372       continue;
373 
374     double value = gfx::Tween::CalculateValue(
375         state_ == State::FADE_OUT ? gfx::Tween::EASE_IN : gfx::Tween::EASE_OUT,
376         animation_->GetCurrentValue());
377 
378     if (state_ == State::FADE_IN)
379       item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 0.0f, 1.0f));
380     else if (state_ == State::FADE_OUT)
381       item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 1.0f, 0.0f));
382 
383     if (state_ == State::FADE_IN || state_ == State::MOVE_DOWN ||
384         state_ == State::MOVE_UP_FOR_INVERSE) {
385       item.popup->SetPopupBounds(
386           gfx::Tween::RectValueBetween(value, item.start_bounds, item.bounds));
387     }
388   }
389 }
390 
AddPopup()391 bool MessagePopupCollection::AddPopup() {
392   std::set<std::string> existing_ids;
393   for (const auto& item : popup_items_)
394     existing_ids.insert(item.id);
395 
396   auto notifications = MessageCenter::Get()->GetPopupNotifications();
397   Notification* new_notification = nullptr;
398   // Reverse iterating because notifications are in reverse chronological order.
399   for (auto it = notifications.rbegin(); it != notifications.rend(); ++it) {
400     // Disables popup of custom notification on non-primary displays, since
401     // currently custom notification supports only on one display at the same
402     // time.
403     // TODO(yoshiki): Support custom popup notification on multiple display
404     // (https://crbug.com/715370).
405     if (!IsPrimaryDisplayForNotification() &&
406         (*it)->type() == NOTIFICATION_TYPE_CUSTOM) {
407       continue;
408     }
409 
410     if (!existing_ids.count((*it)->id())) {
411       new_notification = *it;
412       break;
413     }
414   }
415 
416   if (!new_notification)
417     return false;
418 
419   // Reset animation flags of existing popups.
420   for (auto& item : popup_items_) {
421     item.is_animating = false;
422     item.will_fade_in = false;
423   }
424 
425   {
426     PopupItem item;
427     item.id = new_notification->id();
428     item.is_animating = true;
429     item.popup = CreatePopup(*new_notification);
430 
431     if (IsNextEdgeOutsideWorkArea(item)) {
432       item.popup->Close();
433       return false;
434     }
435 
436     item.popup->Show();
437     popup_items_.push_back(item);
438     NotifyPopupAdded(item.popup);
439   }
440 
441   // There are existing notifications that have to be moved up (existing ones +
442   // new one, so > 1).
443   if (inverse_ && popup_items_.size() > 1) {
444     for (auto& item : popup_items_) {
445       item.will_fade_in = item.is_animating;
446       item.is_animating = !item.is_animating;
447     }
448   }
449 
450   MessageCenter::Get()->DisplayedNotification(new_notification->id(),
451                                               DISPLAY_SOURCE_POPUP);
452 
453   CalculateBounds();
454 
455   auto& item = popup_items_.back();
456   item.start_bounds = item.bounds;
457   item.start_bounds +=
458       gfx::Vector2d((IsFromLeft() ? -1 : 1) * item.bounds.width(), 0);
459   return true;
460 }
461 
MarkRemovedPopup()462 void MessagePopupCollection::MarkRemovedPopup() {
463   std::set<std::string> existing_ids;
464   for (Notification* notification :
465        MessageCenter::Get()->GetPopupNotifications()) {
466     existing_ids.insert(notification->id());
467   }
468 
469   for (auto& item : popup_items_)
470     item.is_animating = !existing_ids.count(item.id);
471 }
472 
MoveDownPopups()473 void MessagePopupCollection::MoveDownPopups() {
474   CalculateBounds();
475   for (auto& item : popup_items_)
476     item.is_animating = true;
477 }
478 
GetNextEdge(const PopupItem & item) const479 int MessagePopupCollection::GetNextEdge(const PopupItem& item) const {
480   const int delta =
481       item.popup->GetHeightForWidth(kNotificationWidth) + kMarginBetweenPopups;
482 
483   int base = 0;
484   if (popup_items_.empty()) {
485     base = GetBaseline();
486   } else if (inverse_) {
487     base = IsTopDown() ? popup_items_.front().bounds.bottom()
488                        : popup_items_.front().bounds.y();
489   } else {
490     base = IsTopDown() ? popup_items_.back().bounds.bottom()
491                        : popup_items_.back().bounds.y();
492   }
493 
494   return IsTopDown() ? base + delta : base - delta;
495 }
496 
IsNextEdgeOutsideWorkArea(const PopupItem & item) const497 bool MessagePopupCollection::IsNextEdgeOutsideWorkArea(
498     const PopupItem& item) const {
499   const int next_edge = GetNextEdge(item);
500   const gfx::Rect work_area = GetWorkArea();
501   return IsTopDown() ? next_edge > work_area.bottom()
502                      : next_edge < work_area.y();
503 }
504 
StartHotMode()505 void MessagePopupCollection::StartHotMode() {
506   for (size_t i = 0; i < popup_items_.size(); ++i) {
507     if (GetPopupItem(i)->is_animating && GetPopupItem(i)->popup->is_hovered()) {
508       is_hot_ = true;
509       hot_index_ = i;
510       hot_top_ = GetPopupItem(i)->bounds.y();
511       break;
512     }
513   }
514 }
515 
ResetHotMode()516 void MessagePopupCollection::ResetHotMode() {
517   is_hot_ = false;
518   hot_index_ = 0;
519   hot_top_ = 0;
520 }
521 
CloseAnimatingPopups()522 void MessagePopupCollection::CloseAnimatingPopups() {
523   for (auto& item : popup_items_) {
524     if (!item.is_animating)
525       continue;
526     item.popup->Close();
527   }
528   RemoveClosedPopupItems();
529 }
530 
CloseTransparentPopups()531 bool MessagePopupCollection::CloseTransparentPopups() {
532   bool removed = false;
533   for (auto& item : popup_items_) {
534     if (item.popup->GetOpacity() > 0.0)
535       continue;
536     item.popup->Close();
537     removed = true;
538   }
539   RemoveClosedPopupItems();
540   return removed;
541 }
542 
ClosePopupsOutsideWorkArea()543 void MessagePopupCollection::ClosePopupsOutsideWorkArea() {
544   const gfx::Rect work_area = GetWorkArea();
545   for (auto& item : popup_items_) {
546     if (work_area.Contains(item.bounds))
547       continue;
548     item.popup->Close();
549   }
550   RemoveClosedPopupItems();
551 }
552 
RemoveClosedPopupItems()553 void MessagePopupCollection::RemoveClosedPopupItems() {
554   base::EraseIf(popup_items_, [](const auto& item) { return !item.popup; });
555 }
556 
CloseAllPopupsNow()557 void MessagePopupCollection::CloseAllPopupsNow() {
558   for (auto& item : popup_items_)
559     item.is_animating = true;
560   CloseAnimatingPopups();
561 
562   ResetHotMode();
563   state_ = State::IDLE;
564   animation_->End();
565 }
566 
CollapseAllPopups()567 bool MessagePopupCollection::CollapseAllPopups() {
568   bool changed = false;
569   for (auto& item : popup_items_) {
570     int old_height = item.popup->GetHeightForWidth(kNotificationWidth);
571 
572     item.popup->AutoCollapse();
573 
574     int new_height = item.popup->GetHeightForWidth(kNotificationWidth);
575     if (old_height != new_height)
576       changed = true;
577   }
578 
579   resize_requested_ = false;
580   return changed;
581 }
582 
HasAddedPopup() const583 bool MessagePopupCollection::HasAddedPopup() const {
584   std::set<std::string> existing_ids;
585   for (const auto& item : popup_items_)
586     existing_ids.insert(item.id);
587 
588   for (Notification* notification :
589        MessageCenter::Get()->GetPopupNotifications()) {
590     if (!existing_ids.count(notification->id()))
591       return true;
592   }
593   return false;
594 }
595 
HasRemovedPopup() const596 bool MessagePopupCollection::HasRemovedPopup() const {
597   std::set<std::string> existing_ids;
598   for (Notification* notification :
599        MessageCenter::Get()->GetPopupNotifications()) {
600     existing_ids.insert(notification->id());
601   }
602 
603   for (const auto& item : popup_items_) {
604     if (!existing_ids.count(item.id))
605       return true;
606   }
607   return false;
608 }
609 
IsAnyPopupHovered() const610 bool MessagePopupCollection::IsAnyPopupHovered() const {
611   for (const auto& item : popup_items_) {
612     if (item.popup->is_hovered())
613       return true;
614   }
615   return false;
616 }
617 
IsAnyPopupActive() const618 bool MessagePopupCollection::IsAnyPopupActive() const {
619   for (const auto& item : popup_items_) {
620     if (item.popup->is_active())
621       return true;
622   }
623   return false;
624 }
625 
GetPopupItem(size_t index_from_top)626 MessagePopupCollection::PopupItem* MessagePopupCollection::GetPopupItem(
627     size_t index_from_top) {
628   DCHECK_LT(index_from_top, popup_items_.size());
629   return &popup_items_[inverse_ ? popup_items_.size() - index_from_top - 1
630                                 : index_from_top];
631 }
632 
633 }  // namespace message_center
634