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