Home | History | Annotate | Download | only in notifications
      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 
     28 namespace {
     29 // Minimum and maximum size of balloon content.
     30 const int kBalloonMinWidth = 300;
     31 const int kBalloonMaxWidth = 300;
     32 const int kBalloonMinHeight = 24;
     33 const int kBalloonMaxHeight = 120;
     34 
     35 // Maximum height of the notification panel.
     36 // TODO(oshima): Get this from system's metrics.
     37 const int kMaxPanelHeight = 400;
     38 
     39 // The duration for a new notification to become stale.
     40 const int kStaleTimeoutInSeconds = 10;
     41 
     42 using chromeos::BalloonViewImpl;
     43 using chromeos::NotificationPanel;
     44 
     45 #if !defined(NDEBUG)
     46 // A utility function to convert State enum to string.
     47 const 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 
     65 chromeos::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.
     73 class 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 
    126 class 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 
    243 namespace chromeos {
    244 
    245 class 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 
    396 NotificationPanel::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 
    409 NotificationPanel::~NotificationPanel() {
    410   Hide();
    411 }
    412 
    413 ////////////////////////////////////////////////////////////////////////////////
    414 // NottificationPanel public.
    415 
    416 void 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 
    461 void 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 
    489 void 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 
    502 bool NotificationPanel::Update(Balloon* balloon) {
    503   return balloon_container_->Update(balloon);
    504 }
    505 
    506 void 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 
    530 void 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 
    539 void 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 
    565 void 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 
    580 string16 NotificationPanel::GetPanelTitle() {
    581   return string16(l10n_util::GetStringUTF16(IDS_NOTIFICATION_PANEL_TITLE));
    582 }
    583 
    584 SkBitmap NotificationPanel::GetPanelIcon() {
    585   return SkBitmap();
    586 }
    587 
    588 bool NotificationPanel::CanClosePanel() {
    589   return true;
    590 }
    591 
    592 void NotificationPanel::ClosePanel() {
    593   SET_STATE(CLOSED);
    594   UpdatePanel(false);
    595 }
    596 
    597 void NotificationPanel::ActivatePanel() {
    598   if (active_)
    599     active_->Activated();
    600 }
    601 
    602 ////////////////////////////////////////////////////////////////////////////////
    603 // NotificationObserver overrides.
    604 
    605 void 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 
    635 void NotificationPanel::OnMouseLeave() {
    636   SetActiveView(NULL);
    637   if (balloon_container_->GetNotificationCount() == 0)
    638     SET_STATE(CLOSED);
    639   UpdatePanel(true);
    640 }
    641 
    642 void NotificationPanel::OnMouseMotion(const gfx::Point& point) {
    643   SetActiveView(balloon_container_->FindBalloonView(point));
    644   SET_STATE(KEEP_SIZE);
    645 }
    646 
    647 NotificationPanelTester* NotificationPanel::GetTester() {
    648   if (!tester_.get())
    649     tester_.reset(new NotificationPanelTester(this));
    650   return tester_.get();
    651 }
    652 
    653 ////////////////////////////////////////////////////////////////////////////////
    654 // NotificationPanel private.
    655 
    656 void 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 
    669 void NotificationPanel::UnregisterNotification() {
    670   if (panel_controller_.get())
    671     registrar_.Remove(this, NotificationType::PANEL_STATE_CHANGED,
    672                       Source<PanelController>(panel_controller_.get()));
    673 }
    674 
    675 void 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 
    688 void 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 
    731 void 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 
    743 void NotificationPanel::UpdateControl() {
    744   if (container_host_)
    745     static_cast<ViewportWidget*>(container_host_)->UpdateControl();
    746 }
    747 
    748 gfx::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 
    759 gfx::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 
    770 void 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 
    779 void 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 
    794 void 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 
    802 void 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 
    811 int NotificationPanelTester::GetNotificationCount() const {
    812   return panel_->balloon_container_->GetNotificationCount();
    813 }
    814 
    815 int NotificationPanelTester::GetStickyNotificationCount() const {
    816   return panel_->balloon_container_->GetStickyNotificationCount();
    817 }
    818 
    819 int NotificationPanelTester::GetNewNotificationCount() const {
    820   return panel_->balloon_container_->GetNewNotificationCount();
    821 }
    822 
    823 void NotificationPanelTester::SetStaleTimeout(int timeout) {
    824   panel_->stale_timeout_ = timeout;
    825 }
    826 
    827 void NotificationPanelTester::MarkStale(const Notification& notification) {
    828   panel_->MarkStale(notification);
    829 }
    830 
    831 PanelController* NotificationPanelTester::GetPanelController() const {
    832   return panel_->panel_controller_.get();
    833 }
    834 
    835 BalloonViewImpl* NotificationPanelTester::GetBalloonView(
    836     BalloonCollectionImpl* collection,
    837     const Notification& notification) {
    838   Balloon* balloon = collection->FindBalloon(notification);
    839   DCHECK(balloon);
    840   return GetBalloonViewOf(balloon);
    841 }
    842 
    843 bool 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 
    852 bool NotificationPanelTester::IsActive(const BalloonViewImpl* view) const {
    853   return panel_->active_ == view;
    854 }
    855 
    856 }  // namespace chromeos
    857