1// Copyright (c) 2011 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// Draws the view for the balloons.
6
7#include "chrome/browser/chromeos/notifications/notification_panel.h"
8
9#include <algorithm>
10
11#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
12#include "chrome/browser/chromeos/notifications/balloon_view.h"
13#include "content/common/notification_details.h"
14#include "content/common/notification_source.h"
15#include "grit/generated_resources.h"
16#include "third_party/cros/chromeos_wm_ipc_enums.h"
17#include "ui/base/l10n/l10n_util.h"
18#include "ui/base/resource/resource_bundle.h"
19#include "ui/gfx/canvas.h"
20#include "views/background.h"
21#include "views/controls/native/native_view_host.h"
22#include "views/controls/scroll_view.h"
23#include "views/widget/root_view.h"
24#include "views/widget/widget_gtk.h"
25
26#define SET_STATE(state) SetState(state, __PRETTY_FUNCTION__)
27
28namespace {
29// Minimum and maximum size of balloon content.
30const int kBalloonMinWidth = 300;
31const int kBalloonMaxWidth = 300;
32const int kBalloonMinHeight = 24;
33const int kBalloonMaxHeight = 120;
34
35// Maximum height of the notification panel.
36// TODO(oshima): Get this from system's metrics.
37const int kMaxPanelHeight = 400;
38
39// The duration for a new notification to become stale.
40const int kStaleTimeoutInSeconds = 10;
41
42using chromeos::BalloonViewImpl;
43using chromeos::NotificationPanel;
44
45#if !defined(NDEBUG)
46// A utility function to convert State enum to string.
47const char* ToStr(const NotificationPanel::State state) {
48  switch (state) {
49    case NotificationPanel::FULL:
50      return "full";
51    case NotificationPanel::KEEP_SIZE:
52      return "keep_size";
53    case NotificationPanel::STICKY_AND_NEW:
54      return "sticky_new";
55    case NotificationPanel::MINIMIZED:
56      return "minimized";
57    case NotificationPanel::CLOSED:
58      return "closed";
59    default:
60      return "unknown";
61  }
62}
63#endif
64
65chromeos::BalloonViewImpl* GetBalloonViewOf(const Balloon* balloon) {
66  return static_cast<chromeos::BalloonViewImpl*>(balloon->view());
67}
68
69// A WidgetGtk that covers entire ScrollView's viewport. Without this,
70// all renderer's native gtk widgets are moved one by one via
71// View::VisibleBoundsInRootChanged() notification, which makes
72// scrolling not smooth.
73class ViewportWidget : public views::WidgetGtk {
74 public:
75  explicit ViewportWidget(chromeos::NotificationPanel* panel)
76      : WidgetGtk(views::WidgetGtk::TYPE_CHILD),
77        panel_(panel) {
78  }
79
80  void UpdateControl() {
81    if (last_point_.get())
82      panel_->OnMouseMotion(*last_point_.get());
83  }
84
85  // views::WidgetGtk overrides.
86  virtual gboolean OnMotionNotify(GtkWidget* widget, GdkEventMotion* event) {
87    gboolean result = WidgetGtk::OnMotionNotify(widget, event);
88    gdouble x = event->x;
89    gdouble y = event->y;
90
91    // The window_contents_' allocation has been moved off the top left
92    // corner, so we need to adjust it.
93    GtkAllocation alloc = widget->allocation;
94    x -= alloc.x;
95    y -= alloc.y;
96
97    if (!last_point_.get()) {
98      last_point_.reset(new gfx::Point(x, y));
99    } else {
100      last_point_->set_x(x);
101      last_point_->set_y(y);
102    }
103    panel_->OnMouseMotion(*last_point_.get());
104    return result;
105  }
106
107  virtual gboolean OnLeaveNotify(GtkWidget* widget, GdkEventCrossing* event) {
108    gboolean result = views::WidgetGtk::OnLeaveNotify(widget, event);
109    // Leave notify can happen if the mouse moves into the child gdk window.
110    // Make sure the mouse is outside of the panel.
111    gfx::Point p(event->x_root, event->y_root);
112    gfx::Rect bounds = GetWindowScreenBounds();
113    if (!bounds.Contains(p)) {
114      panel_->OnMouseLeave();
115      last_point_.reset();
116    }
117    return result;
118  }
119
120 private:
121  chromeos::NotificationPanel* panel_;
122  scoped_ptr<gfx::Point> last_point_;
123  DISALLOW_COPY_AND_ASSIGN(ViewportWidget);
124};
125
126class BalloonSubContainer : public views::View {
127 public:
128  explicit BalloonSubContainer(int margin)
129      : margin_(margin) {
130  }
131
132  virtual ~BalloonSubContainer() {}
133
134  // views::View overrides.
135  virtual gfx::Size GetPreferredSize() {
136    return preferred_size_;
137  }
138
139  virtual void Layout() {
140    // Layout bottom up
141    int height = 0;
142    for (int i = child_count() - 1; i >= 0; --i) {
143      views::View* child = GetChildViewAt(i);
144      child->SetBounds(0, height, child->width(), child->height());
145      height += child->height() + margin_;
146    }
147    SchedulePaint();
148  }
149
150  // Updates the bound so that it can show all balloons.
151  void UpdateBounds() {
152    int height = 0;
153    int max_width = 0;
154    for (int i = child_count() - 1; i >= 0; --i) {
155      views::View* child = GetChildViewAt(i);
156      height += child->height() + margin_;
157      max_width = std::max(max_width, child->width());
158    }
159    if (height > 0)
160      height -= margin_;
161    preferred_size_.set_width(max_width);
162    preferred_size_.set_height(height);
163    SizeToPreferredSize();
164  }
165
166  // Returns the bounds that covers new notifications.
167  gfx::Rect GetNewBounds() {
168    gfx::Rect rect;
169    for (int i = child_count() - 1; i >= 0; --i) {
170      BalloonViewImpl* view =
171          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
172      if (!view->stale()) {
173        if (rect.IsEmpty()) {
174          rect = view->bounds();
175        } else {
176          rect = rect.Union(view->bounds());
177        }
178      }
179    }
180    return gfx::Rect(x(), y(), rect.width(), rect.height());
181  }
182
183  // Returns # of new notifications.
184  int GetNewCount() {
185    int count = 0;
186    for (int i = child_count() - 1; i >= 0; --i) {
187      BalloonViewImpl* view =
188          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
189      if (!view->stale())
190        count++;
191    }
192    return count;
193  }
194
195  // Make all notifications stale.
196  void MakeAllStale() {
197    for (int i = child_count() - 1; i >= 0; --i) {
198      BalloonViewImpl* view =
199          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
200      view->set_stale();
201    }
202  }
203
204  void DismissAll() {
205    for (int i = child_count() - 1; i >= 0; --i) {
206      BalloonViewImpl* view =
207          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
208      view->Close(true);
209    }
210  }
211
212  BalloonViewImpl* FindBalloonView(const Notification& notification) {
213    for (int i = child_count() - 1; i >= 0; --i) {
214      BalloonViewImpl* view =
215          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
216      if (view->IsFor(notification)) {
217        return view;
218      }
219    }
220    return NULL;
221  }
222
223  BalloonViewImpl* FindBalloonView(const gfx::Point point) {
224    gfx::Point copy(point);
225    ConvertPointFromWidget(this, &copy);
226    for (int i = child_count() - 1; i >= 0; --i) {
227      views::View* view = GetChildViewAt(i);
228      if (view->bounds().Contains(copy))
229        return static_cast<BalloonViewImpl*>(view);
230    }
231    return NULL;
232  }
233
234 private:
235  gfx::Size preferred_size_;
236  int margin_;
237
238  DISALLOW_COPY_AND_ASSIGN(BalloonSubContainer);
239};
240
241}  // namespace
242
243namespace chromeos {
244
245class BalloonContainer : public views::View {
246 public:
247  explicit BalloonContainer(int margin)
248      : margin_(margin),
249        sticky_container_(new BalloonSubContainer(margin)),
250        non_sticky_container_(new BalloonSubContainer(margin)) {
251    AddChildView(sticky_container_);
252    AddChildView(non_sticky_container_);
253  }
254  virtual ~BalloonContainer() {}
255
256  // views::View overrides.
257  virtual void Layout() {
258    int margin =
259        (sticky_container_->child_count() != 0 &&
260         non_sticky_container_->child_count() != 0) ?
261        margin_ : 0;
262    sticky_container_->SetBounds(
263        0, 0, width(), sticky_container_->height());
264    non_sticky_container_->SetBounds(
265        0, sticky_container_->bounds().bottom() + margin,
266        width(), non_sticky_container_->height());
267  }
268
269  virtual gfx::Size GetPreferredSize() {
270    return preferred_size_;
271  }
272
273  // Returns the size that covers sticky and new notifications.
274  gfx::Size GetStickyNewSize() {
275    gfx::Rect sticky = sticky_container_->bounds();
276    gfx::Rect new_non_sticky = non_sticky_container_->GetNewBounds();
277    if (sticky.IsEmpty())
278      return new_non_sticky.size();
279    if (new_non_sticky.IsEmpty())
280      return sticky.size();
281    return sticky.Union(new_non_sticky).size();
282  }
283
284  // Adds a ballon to the panel.
285  void Add(Balloon* balloon) {
286    BalloonViewImpl* view = GetBalloonViewOf(balloon);
287    GetContainerFor(balloon)->AddChildView(view);
288  }
289
290  // Updates the position of the |balloon|.
291  bool Update(Balloon* balloon) {
292    BalloonViewImpl* view = GetBalloonViewOf(balloon);
293    View* container = NULL;
294    if (view->parent() == sticky_container_) {
295      container = sticky_container_;
296    } else if (view->parent() == non_sticky_container_) {
297      container = non_sticky_container_;
298    }
299    if (container) {
300      container->RemoveChildView(view);
301      container->AddChildView(view);
302      return true;
303    } else {
304      return false;
305    }
306  }
307
308  // Removes a ballon from the panel.
309  BalloonViewImpl* Remove(Balloon* balloon) {
310    BalloonViewImpl* view = GetBalloonViewOf(balloon);
311    GetContainerFor(balloon)->RemoveChildView(view);
312    return view;
313  }
314
315  // Returns the number of notifications added to the panel.
316  int GetNotificationCount() {
317    return sticky_container_->child_count() +
318        non_sticky_container_->child_count();
319  }
320
321  // Returns the # of new notifications.
322  int GetNewNotificationCount() {
323    return sticky_container_->GetNewCount() +
324        non_sticky_container_->GetNewCount();
325  }
326
327  // Returns the # of sticky and new notifications.
328  int GetStickyNewNotificationCount() {
329    return sticky_container_->child_count() +
330        non_sticky_container_->GetNewCount();
331  }
332
333  // Returns the # of sticky notifications.
334  int GetStickyNotificationCount() {
335    return sticky_container_->child_count();
336  }
337
338  // Returns true if the |view| is contained in the panel.
339  bool HasBalloonView(View* view) {
340    return view->parent() == sticky_container_ ||
341        view->parent() == non_sticky_container_;
342  }
343
344  // Updates the bounds so that all notifications are visible.
345  void UpdateBounds() {
346    sticky_container_->UpdateBounds();
347    non_sticky_container_->UpdateBounds();
348    preferred_size_ = sticky_container_->GetPreferredSize();
349
350    gfx::Size non_sticky_size = non_sticky_container_->GetPreferredSize();
351    int margin =
352        (!preferred_size_.IsEmpty() && !non_sticky_size.IsEmpty()) ?
353        margin_ : 0;
354    preferred_size_.Enlarge(0, non_sticky_size.height() + margin);
355    preferred_size_.set_width(std::max(
356        preferred_size_.width(), non_sticky_size.width()));
357    SizeToPreferredSize();
358  }
359
360  void MakeAllStale() {
361    sticky_container_->MakeAllStale();
362    non_sticky_container_->MakeAllStale();
363  }
364
365  void DismissAllNonSticky() {
366    non_sticky_container_->DismissAll();
367  }
368
369  BalloonViewImpl* FindBalloonView(const Notification& notification) {
370    BalloonViewImpl* view = sticky_container_->FindBalloonView(notification);
371    return view ? view : non_sticky_container_->FindBalloonView(notification);
372  }
373
374  BalloonViewImpl* FindBalloonView(const gfx::Point& point) {
375    BalloonViewImpl* view = sticky_container_->FindBalloonView(point);
376    return view ? view : non_sticky_container_->FindBalloonView(point);
377  }
378
379 private:
380  BalloonSubContainer* GetContainerFor(Balloon* balloon) const {
381    BalloonViewImpl* view = GetBalloonViewOf(balloon);
382    return view->sticky() ?
383        sticky_container_ : non_sticky_container_;
384  }
385
386  int margin_;
387  // Sticky/non-sticky ballon containers. They're child views and
388  // deleted when this container is deleted.
389  BalloonSubContainer* sticky_container_;
390  BalloonSubContainer* non_sticky_container_;
391  gfx::Size preferred_size_;
392
393  DISALLOW_COPY_AND_ASSIGN(BalloonContainer);
394};
395
396NotificationPanel::NotificationPanel()
397    : balloon_container_(NULL),
398      panel_widget_(NULL),
399      container_host_(NULL),
400      state_(CLOSED),
401      task_factory_(this),
402      min_bounds_(0, 0, kBalloonMinWidth, kBalloonMinHeight),
403      stale_timeout_(1000 * kStaleTimeoutInSeconds),
404      active_(NULL),
405      scroll_to_(NULL) {
406  Init();
407}
408
409NotificationPanel::~NotificationPanel() {
410  Hide();
411}
412
413////////////////////////////////////////////////////////////////////////////////
414// NottificationPanel public.
415
416void NotificationPanel::Show() {
417  if (!panel_widget_) {
418    // TODO(oshima): Using window because Popup widget behaves weird
419    // when resizing. This needs to be investigated.
420    views::WidgetGtk* widget_gtk =
421        new views::WidgetGtk(views::WidgetGtk::TYPE_WINDOW);
422    // Enable double buffering because the panel has both pure views
423    // control and native controls (scroll bar).
424    widget_gtk->EnableDoubleBuffer(true);
425    panel_widget_ = widget_gtk;
426
427    gfx::Rect bounds = GetPreferredBounds();
428    bounds = bounds.Union(min_bounds_);
429    panel_widget_->Init(NULL, bounds);
430    // Set minimum bounds so that it can grow freely.
431    gtk_widget_set_size_request(GTK_WIDGET(panel_widget_->GetNativeView()),
432                                min_bounds_.width(), min_bounds_.height());
433
434    views::NativeViewHost* native = new views::NativeViewHost();
435    scroll_view_->SetContents(native);
436
437    panel_widget_->SetContentsView(scroll_view_.get());
438
439    // Add the view port after scroll_view is attached to the panel widget.
440    ViewportWidget* widget = new ViewportWidget(this);
441    container_host_ = widget;
442    container_host_->Init(NULL, gfx::Rect());
443    container_host_->SetContentsView(balloon_container_.get());
444    // The window_contents_ is onwed by the WidgetGtk. Increase ref count
445    // so that window_contents does not get deleted when detached.
446    g_object_ref(widget->window_contents());
447    native->Attach(widget->window_contents());
448
449    UnregisterNotification();
450    panel_controller_.reset(
451        new PanelController(this, GTK_WINDOW(panel_widget_->GetNativeView())));
452    panel_controller_->Init(false /* don't focus when opened */,
453                            gfx::Rect(0, 0, kBalloonMinWidth, 1), 0,
454                            WM_IPC_PANEL_USER_RESIZE_VERTICALLY);
455    registrar_.Add(this, NotificationType::PANEL_STATE_CHANGED,
456                   Source<PanelController>(panel_controller_.get()));
457  }
458  panel_widget_->Show();
459}
460
461void NotificationPanel::Hide() {
462  balloon_container_->DismissAllNonSticky();
463  if (panel_widget_) {
464    container_host_->GetRootView()->RemoveChildView(balloon_container_.get());
465
466    views::NativeViewHost* native =
467        static_cast<views::NativeViewHost*>(scroll_view_->GetContents());
468    native->Detach();
469    scroll_view_->SetContents(NULL);
470    container_host_->Hide();
471    container_host_->CloseNow();
472    container_host_ = NULL;
473
474    UnregisterNotification();
475    panel_controller_->Close();
476    MessageLoop::current()->DeleteSoon(FROM_HERE, panel_controller_.release());
477    // We need to remove & detach the scroll view from hierarchy to
478    // avoid GTK deleting child.
479    // TODO(oshima): handle this details in WidgetGtk.
480    panel_widget_->GetRootView()->RemoveChildView(scroll_view_.get());
481    panel_widget_->Close();
482    panel_widget_ = NULL;
483  }
484}
485
486////////////////////////////////////////////////////////////////////////////////
487// BalloonCollectionImpl::NotificationUI overrides.
488
489void NotificationPanel::Add(Balloon* balloon) {
490  balloon_container_->Add(balloon);
491  if (state_ == CLOSED || state_ == MINIMIZED)
492    SET_STATE(STICKY_AND_NEW);
493  Show();
494  // Don't resize the panel yet. The panel will be resized when WebKit tells
495  // the size in ResizeNotification.
496  UpdatePanel(false);
497  UpdateControl();
498  StartStaleTimer(balloon);
499  scroll_to_ = balloon;
500}
501
502bool NotificationPanel::Update(Balloon* balloon) {
503  return balloon_container_->Update(balloon);
504}
505
506void NotificationPanel::Remove(Balloon* balloon) {
507  BalloonViewImpl* view = balloon_container_->Remove(balloon);
508  if (view == active_)
509    active_ = NULL;
510  if (scroll_to_ == balloon)
511    scroll_to_ = NULL;
512
513  // TODO(oshima): May be we shouldn't close
514  // if the mouse pointer is still on the panel.
515  if (balloon_container_->GetNotificationCount() == 0)
516    SET_STATE(CLOSED);
517  // no change to the state
518  if (state_ == KEEP_SIZE) {
519    // Just update the content.
520    UpdateContainerBounds();
521  } else {
522    if (state_ != CLOSED &&
523        balloon_container_->GetStickyNewNotificationCount() == 0)
524      SET_STATE(MINIMIZED);
525    UpdatePanel(true);
526  }
527  UpdateControl();
528}
529
530void NotificationPanel::Show(Balloon* balloon) {
531  if (state_ == CLOSED || state_ == MINIMIZED)
532    SET_STATE(STICKY_AND_NEW);
533  Show();
534  UpdatePanel(true);
535  StartStaleTimer(balloon);
536  ScrollBalloonToVisible(balloon);
537}
538
539void NotificationPanel::ResizeNotification(
540    Balloon* balloon, const gfx::Size& size) {
541  // restrict to the min & max sizes
542  gfx::Size real_size(
543      std::max(kBalloonMinWidth,
544               std::min(kBalloonMaxWidth, size.width())),
545      std::max(kBalloonMinHeight,
546               std::min(kBalloonMaxHeight, size.height())));
547
548  // Don't allow balloons to shrink.  This avoids flickering
549  // which sometimes rapidly reports alternating sizes.  Special
550  // case for setting the minimum value.
551  gfx::Size old_size = balloon->content_size();
552  if (real_size.width() > old_size.width() ||
553      real_size.height() > old_size.height() ||
554      real_size == min_bounds_.size()) {
555    balloon->set_content_size(real_size);
556    GetBalloonViewOf(balloon)->Layout();
557    UpdatePanel(true);
558    if (scroll_to_ == balloon) {
559      ScrollBalloonToVisible(scroll_to_);
560      scroll_to_ = NULL;
561    }
562  }
563}
564
565void NotificationPanel::SetActiveView(BalloonViewImpl* view) {
566  // Don't change the active view if it's same notification,
567  // or the notification is being closed.
568  if (active_ == view || (view && view->closed()))
569    return;
570  if (active_)
571    active_->Deactivated();
572  active_ = view;
573  if (active_)
574    active_->Activated();
575}
576
577////////////////////////////////////////////////////////////////////////////////
578// PanelController overrides.
579
580string16 NotificationPanel::GetPanelTitle() {
581  return string16(l10n_util::GetStringUTF16(IDS_NOTIFICATION_PANEL_TITLE));
582}
583
584SkBitmap NotificationPanel::GetPanelIcon() {
585  return SkBitmap();
586}
587
588bool NotificationPanel::CanClosePanel() {
589  return true;
590}
591
592void NotificationPanel::ClosePanel() {
593  SET_STATE(CLOSED);
594  UpdatePanel(false);
595}
596
597void NotificationPanel::ActivatePanel() {
598  if (active_)
599    active_->Activated();
600}
601
602////////////////////////////////////////////////////////////////////////////////
603// NotificationObserver overrides.
604
605void NotificationPanel::Observe(NotificationType type,
606                                const NotificationSource& source,
607                                const NotificationDetails& details) {
608  DCHECK(type == NotificationType::PANEL_STATE_CHANGED);
609  PanelController::State* state =
610      reinterpret_cast<PanelController::State*>(details.map_key());
611  switch (*state) {
612    case PanelController::EXPANDED:
613      // Geting expanded in STICKY_AND_NEW or in KEEP_SIZE state means
614      // that a new notification is added, so just leave the
615      // state. Otherwise, expand to full.
616      if (state_ != STICKY_AND_NEW && state_ != KEEP_SIZE)
617        SET_STATE(FULL);
618      // When the panel is to be expanded, we either show all, or
619      // show only sticky/new, depending on the state.
620      UpdatePanel(false);
621      break;
622    case PanelController::MINIMIZED:
623      SET_STATE(MINIMIZED);
624      // Make all notifications stale when a user minimize the panel.
625      balloon_container_->MakeAllStale();
626      break;
627    case PanelController::INITIAL:
628      NOTREACHED() << "Transition to Initial state should not happen";
629  }
630}
631
632////////////////////////////////////////////////////////////////////////////////
633// PanelController public.
634
635void NotificationPanel::OnMouseLeave() {
636  SetActiveView(NULL);
637  if (balloon_container_->GetNotificationCount() == 0)
638    SET_STATE(CLOSED);
639  UpdatePanel(true);
640}
641
642void NotificationPanel::OnMouseMotion(const gfx::Point& point) {
643  SetActiveView(balloon_container_->FindBalloonView(point));
644  SET_STATE(KEEP_SIZE);
645}
646
647NotificationPanelTester* NotificationPanel::GetTester() {
648  if (!tester_.get())
649    tester_.reset(new NotificationPanelTester(this));
650  return tester_.get();
651}
652
653////////////////////////////////////////////////////////////////////////////////
654// NotificationPanel private.
655
656void NotificationPanel::Init() {
657  DCHECK(!panel_widget_);
658  balloon_container_.reset(new BalloonContainer(1));
659  balloon_container_->set_parent_owned(false);
660  balloon_container_->set_background(
661      views::Background::CreateSolidBackground(ResourceBundle::frame_color));
662
663  scroll_view_.reset(new views::ScrollView());
664  scroll_view_->set_parent_owned(false);
665  scroll_view_->set_background(
666      views::Background::CreateSolidBackground(SK_ColorWHITE));
667}
668
669void NotificationPanel::UnregisterNotification() {
670  if (panel_controller_.get())
671    registrar_.Remove(this, NotificationType::PANEL_STATE_CHANGED,
672                      Source<PanelController>(panel_controller_.get()));
673}
674
675void NotificationPanel::ScrollBalloonToVisible(Balloon* balloon) {
676  BalloonViewImpl* view = GetBalloonViewOf(balloon);
677  if (!view->closed()) {
678    // We can't use View::ScrollRectToVisible because the viewport is not
679    // ancestor of the BalloonViewImpl.
680    // Use Widget's coordinate which is same as viewport's coordinates.
681    gfx::Point p(0, 0);
682    views::View::ConvertPointToWidget(view, &p);
683    gfx::Rect visible_rect(p.x(), p.y(), view->width(), view->height());
684    scroll_view_->ScrollContentsRegionToBeVisible(visible_rect);
685  }
686}
687
688void NotificationPanel::UpdatePanel(bool update_container_size) {
689  if (update_container_size)
690    UpdateContainerBounds();
691  switch (state_) {
692    case KEEP_SIZE: {
693      gfx::Rect min_bounds = GetPreferredBounds();
694      gfx::Rect panel_bounds = panel_widget_->GetWindowScreenBounds();
695      if (min_bounds.height() < panel_bounds.height())
696        panel_widget_->SetBounds(min_bounds);
697      else if (min_bounds.height() > panel_bounds.height()) {
698        // need scroll bar
699        int width = balloon_container_->width() +
700            scroll_view_->GetScrollBarWidth();
701        panel_bounds.set_width(width);
702        panel_widget_->SetBounds(panel_bounds);
703      }
704
705      // no change.
706      break;
707    }
708    case CLOSED:
709      Hide();
710      break;
711    case MINIMIZED:
712      balloon_container_->MakeAllStale();
713      if (panel_controller_.get())
714        panel_controller_->SetState(PanelController::MINIMIZED);
715      break;
716    case FULL:
717      if (panel_widget_) {
718        panel_widget_->SetBounds(GetPreferredBounds());
719        panel_controller_->SetState(PanelController::EXPANDED);
720      }
721      break;
722    case STICKY_AND_NEW:
723      if (panel_widget_) {
724        panel_widget_->SetBounds(GetStickyNewBounds());
725        panel_controller_->SetState(PanelController::EXPANDED);
726      }
727      break;
728  }
729}
730
731void NotificationPanel::UpdateContainerBounds() {
732  balloon_container_->UpdateBounds();
733  views::NativeViewHost* native =
734      static_cast<views::NativeViewHost*>(scroll_view_->GetContents());
735  // Update from WebKit may arrive after the panel is closed/hidden
736  // and viewport widget is detached.
737  if (native) {
738    native->SetBoundsRect(balloon_container_->bounds());
739    scroll_view_->Layout();
740  }
741}
742
743void NotificationPanel::UpdateControl() {
744  if (container_host_)
745    static_cast<ViewportWidget*>(container_host_)->UpdateControl();
746}
747
748gfx::Rect NotificationPanel::GetPreferredBounds() {
749  gfx::Size pref_size = balloon_container_->GetPreferredSize();
750  int new_height = std::min(pref_size.height(), kMaxPanelHeight);
751  int new_width = pref_size.width();
752  // Adjust the width to avoid showing a horizontal scroll bar.
753  if (new_height != pref_size.height()) {
754    new_width += scroll_view_->GetScrollBarWidth();
755  }
756  return gfx::Rect(0, 0, new_width, new_height).Union(min_bounds_);
757}
758
759gfx::Rect NotificationPanel::GetStickyNewBounds() {
760  gfx::Size pref_size = balloon_container_->GetPreferredSize();
761  gfx::Size sticky_size = balloon_container_->GetStickyNewSize();
762  int new_height = std::min(sticky_size.height(), kMaxPanelHeight);
763  int new_width = pref_size.width();
764  // Adjust the width to avoid showing a horizontal scroll bar.
765  if (new_height != pref_size.height())
766    new_width += scroll_view_->GetScrollBarWidth();
767  return gfx::Rect(0, 0, new_width, new_height).Union(min_bounds_);
768}
769
770void NotificationPanel::StartStaleTimer(Balloon* balloon) {
771  BalloonViewImpl* view = GetBalloonViewOf(balloon);
772  MessageLoop::current()->PostDelayedTask(
773      FROM_HERE,
774      task_factory_.NewRunnableMethod(
775          &NotificationPanel::OnStale, view),
776      stale_timeout_);
777}
778
779void NotificationPanel::OnStale(BalloonViewImpl* view) {
780  if (balloon_container_->HasBalloonView(view) && !view->stale()) {
781    view->set_stale();
782    // don't update panel on stale
783    if (state_ == KEEP_SIZE)
784      return;
785    if (balloon_container_->GetStickyNewNotificationCount() > 0) {
786      SET_STATE(STICKY_AND_NEW);
787    } else {
788      SET_STATE(MINIMIZED);
789    }
790    UpdatePanel(false);
791  }
792}
793
794void NotificationPanel::SetState(State new_state, const char* name) {
795#if !defined(NDEBUG)
796  DVLOG(1) << "state transition " << ToStr(state_) << " >> " << ToStr(new_state)
797           << " in " << name;
798#endif
799  state_ = new_state;
800}
801
802void NotificationPanel::MarkStale(const Notification& notification) {
803  BalloonViewImpl* view = balloon_container_->FindBalloonView(notification);
804  DCHECK(view);
805  OnStale(view);
806}
807
808////////////////////////////////////////////////////////////////////////////////
809// NotificationPanelTester public.
810
811int NotificationPanelTester::GetNotificationCount() const {
812  return panel_->balloon_container_->GetNotificationCount();
813}
814
815int NotificationPanelTester::GetStickyNotificationCount() const {
816  return panel_->balloon_container_->GetStickyNotificationCount();
817}
818
819int NotificationPanelTester::GetNewNotificationCount() const {
820  return panel_->balloon_container_->GetNewNotificationCount();
821}
822
823void NotificationPanelTester::SetStaleTimeout(int timeout) {
824  panel_->stale_timeout_ = timeout;
825}
826
827void NotificationPanelTester::MarkStale(const Notification& notification) {
828  panel_->MarkStale(notification);
829}
830
831PanelController* NotificationPanelTester::GetPanelController() const {
832  return panel_->panel_controller_.get();
833}
834
835BalloonViewImpl* NotificationPanelTester::GetBalloonView(
836    BalloonCollectionImpl* collection,
837    const Notification& notification) {
838  Balloon* balloon = collection->FindBalloon(notification);
839  DCHECK(balloon);
840  return GetBalloonViewOf(balloon);
841}
842
843bool NotificationPanelTester::IsVisible(const BalloonViewImpl* view) const {
844  gfx::Rect rect = panel_->scroll_view_->GetVisibleRect();
845  gfx::Point origin(0, 0);
846  views::View::ConvertPointToView(view, panel_->balloon_container_.get(),
847                                  &origin);
848  return rect.Contains(gfx::Rect(origin, view->size()));
849}
850
851
852bool NotificationPanelTester::IsActive(const BalloonViewImpl* view) const {
853  return panel_->active_ == view;
854}
855
856}  // namespace chromeos
857