普通文本  |  857行  |  26.53 KB

// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Draws the view for the balloons.

#include "chrome/browser/chromeos/notifications/notification_panel.h"

#include <algorithm>

#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
#include "chrome/browser/chromeos/notifications/balloon_view.h"
#include "content/common/notification_details.h"
#include "content/common/notification_source.h"
#include "grit/generated_resources.h"
#include "third_party/cros/chromeos_wm_ipc_enums.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "views/background.h"
#include "views/controls/native/native_view_host.h"
#include "views/controls/scroll_view.h"
#include "views/widget/root_view.h"
#include "views/widget/widget_gtk.h"

#define SET_STATE(state) SetState(state, __PRETTY_FUNCTION__)

namespace {
// Minimum and maximum size of balloon content.
const int kBalloonMinWidth = 300;
const int kBalloonMaxWidth = 300;
const int kBalloonMinHeight = 24;
const int kBalloonMaxHeight = 120;

// Maximum height of the notification panel.
// TODO(oshima): Get this from system's metrics.
const int kMaxPanelHeight = 400;

// The duration for a new notification to become stale.
const int kStaleTimeoutInSeconds = 10;

using chromeos::BalloonViewImpl;
using chromeos::NotificationPanel;

#if !defined(NDEBUG)
// A utility function to convert State enum to string.
const char* ToStr(const NotificationPanel::State state) {
  switch (state) {
    case NotificationPanel::FULL:
      return "full";
    case NotificationPanel::KEEP_SIZE:
      return "keep_size";
    case NotificationPanel::STICKY_AND_NEW:
      return "sticky_new";
    case NotificationPanel::MINIMIZED:
      return "minimized";
    case NotificationPanel::CLOSED:
      return "closed";
    default:
      return "unknown";
  }
}
#endif

chromeos::BalloonViewImpl* GetBalloonViewOf(const Balloon* balloon) {
  return static_cast<chromeos::BalloonViewImpl*>(balloon->view());
}

// A WidgetGtk that covers entire ScrollView's viewport. Without this,
// all renderer's native gtk widgets are moved one by one via
// View::VisibleBoundsInRootChanged() notification, which makes
// scrolling not smooth.
class ViewportWidget : public views::WidgetGtk {
 public:
  explicit ViewportWidget(chromeos::NotificationPanel* panel)
      : WidgetGtk(views::WidgetGtk::TYPE_CHILD),
        panel_(panel) {
  }

  void UpdateControl() {
    if (last_point_.get())
      panel_->OnMouseMotion(*last_point_.get());
  }

  // views::WidgetGtk overrides.
  virtual gboolean OnMotionNotify(GtkWidget* widget, GdkEventMotion* event) {
    gboolean result = WidgetGtk::OnMotionNotify(widget, event);
    gdouble x = event->x;
    gdouble y = event->y;

    // The window_contents_' allocation has been moved off the top left
    // corner, so we need to adjust it.
    GtkAllocation alloc = widget->allocation;
    x -= alloc.x;
    y -= alloc.y;

    if (!last_point_.get()) {
      last_point_.reset(new gfx::Point(x, y));
    } else {
      last_point_->set_x(x);
      last_point_->set_y(y);
    }
    panel_->OnMouseMotion(*last_point_.get());
    return result;
  }

  virtual gboolean OnLeaveNotify(GtkWidget* widget, GdkEventCrossing* event) {
    gboolean result = views::WidgetGtk::OnLeaveNotify(widget, event);
    // Leave notify can happen if the mouse moves into the child gdk window.
    // Make sure the mouse is outside of the panel.
    gfx::Point p(event->x_root, event->y_root);
    gfx::Rect bounds = GetWindowScreenBounds();
    if (!bounds.Contains(p)) {
      panel_->OnMouseLeave();
      last_point_.reset();
    }
    return result;
  }

 private:
  chromeos::NotificationPanel* panel_;
  scoped_ptr<gfx::Point> last_point_;
  DISALLOW_COPY_AND_ASSIGN(ViewportWidget);
};

class BalloonSubContainer : public views::View {
 public:
  explicit BalloonSubContainer(int margin)
      : margin_(margin) {
  }

  virtual ~BalloonSubContainer() {}

  // views::View overrides.
  virtual gfx::Size GetPreferredSize() {
    return preferred_size_;
  }

  virtual void Layout() {
    // Layout bottom up
    int height = 0;
    for (int i = child_count() - 1; i >= 0; --i) {
      views::View* child = GetChildViewAt(i);
      child->SetBounds(0, height, child->width(), child->height());
      height += child->height() + margin_;
    }
    SchedulePaint();
  }

  // Updates the bound so that it can show all balloons.
  void UpdateBounds() {
    int height = 0;
    int max_width = 0;
    for (int i = child_count() - 1; i >= 0; --i) {
      views::View* child = GetChildViewAt(i);
      height += child->height() + margin_;
      max_width = std::max(max_width, child->width());
    }
    if (height > 0)
      height -= margin_;
    preferred_size_.set_width(max_width);
    preferred_size_.set_height(height);
    SizeToPreferredSize();
  }

  // Returns the bounds that covers new notifications.
  gfx::Rect GetNewBounds() {
    gfx::Rect rect;
    for (int i = child_count() - 1; i >= 0; --i) {
      BalloonViewImpl* view =
          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
      if (!view->stale()) {
        if (rect.IsEmpty()) {
          rect = view->bounds();
        } else {
          rect = rect.Union(view->bounds());
        }
      }
    }
    return gfx::Rect(x(), y(), rect.width(), rect.height());
  }

  // Returns # of new notifications.
  int GetNewCount() {
    int count = 0;
    for (int i = child_count() - 1; i >= 0; --i) {
      BalloonViewImpl* view =
          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
      if (!view->stale())
        count++;
    }
    return count;
  }

  // Make all notifications stale.
  void MakeAllStale() {
    for (int i = child_count() - 1; i >= 0; --i) {
      BalloonViewImpl* view =
          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
      view->set_stale();
    }
  }

  void DismissAll() {
    for (int i = child_count() - 1; i >= 0; --i) {
      BalloonViewImpl* view =
          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
      view->Close(true);
    }
  }

  BalloonViewImpl* FindBalloonView(const Notification& notification) {
    for (int i = child_count() - 1; i >= 0; --i) {
      BalloonViewImpl* view =
          static_cast<BalloonViewImpl*>(GetChildViewAt(i));
      if (view->IsFor(notification)) {
        return view;
      }
    }
    return NULL;
  }

  BalloonViewImpl* FindBalloonView(const gfx::Point point) {
    gfx::Point copy(point);
    ConvertPointFromWidget(this, &copy);
    for (int i = child_count() - 1; i >= 0; --i) {
      views::View* view = GetChildViewAt(i);
      if (view->bounds().Contains(copy))
        return static_cast<BalloonViewImpl*>(view);
    }
    return NULL;
  }

 private:
  gfx::Size preferred_size_;
  int margin_;

  DISALLOW_COPY_AND_ASSIGN(BalloonSubContainer);
};

}  // namespace

namespace chromeos {

class BalloonContainer : public views::View {
 public:
  explicit BalloonContainer(int margin)
      : margin_(margin),
        sticky_container_(new BalloonSubContainer(margin)),
        non_sticky_container_(new BalloonSubContainer(margin)) {
    AddChildView(sticky_container_);
    AddChildView(non_sticky_container_);
  }
  virtual ~BalloonContainer() {}

  // views::View overrides.
  virtual void Layout() {
    int margin =
        (sticky_container_->child_count() != 0 &&
         non_sticky_container_->child_count() != 0) ?
        margin_ : 0;
    sticky_container_->SetBounds(
        0, 0, width(), sticky_container_->height());
    non_sticky_container_->SetBounds(
        0, sticky_container_->bounds().bottom() + margin,
        width(), non_sticky_container_->height());
  }

  virtual gfx::Size GetPreferredSize() {
    return preferred_size_;
  }

  // Returns the size that covers sticky and new notifications.
  gfx::Size GetStickyNewSize() {
    gfx::Rect sticky = sticky_container_->bounds();
    gfx::Rect new_non_sticky = non_sticky_container_->GetNewBounds();
    if (sticky.IsEmpty())
      return new_non_sticky.size();
    if (new_non_sticky.IsEmpty())
      return sticky.size();
    return sticky.Union(new_non_sticky).size();
  }

  // Adds a ballon to the panel.
  void Add(Balloon* balloon) {
    BalloonViewImpl* view = GetBalloonViewOf(balloon);
    GetContainerFor(balloon)->AddChildView(view);
  }

  // Updates the position of the |balloon|.
  bool Update(Balloon* balloon) {
    BalloonViewImpl* view = GetBalloonViewOf(balloon);
    View* container = NULL;
    if (view->parent() == sticky_container_) {
      container = sticky_container_;
    } else if (view->parent() == non_sticky_container_) {
      container = non_sticky_container_;
    }
    if (container) {
      container->RemoveChildView(view);
      container->AddChildView(view);
      return true;
    } else {
      return false;
    }
  }

  // Removes a ballon from the panel.
  BalloonViewImpl* Remove(Balloon* balloon) {
    BalloonViewImpl* view = GetBalloonViewOf(balloon);
    GetContainerFor(balloon)->RemoveChildView(view);
    return view;
  }

  // Returns the number of notifications added to the panel.
  int GetNotificationCount() {
    return sticky_container_->child_count() +
        non_sticky_container_->child_count();
  }

  // Returns the # of new notifications.
  int GetNewNotificationCount() {
    return sticky_container_->GetNewCount() +
        non_sticky_container_->GetNewCount();
  }

  // Returns the # of sticky and new notifications.
  int GetStickyNewNotificationCount() {
    return sticky_container_->child_count() +
        non_sticky_container_->GetNewCount();
  }

  // Returns the # of sticky notifications.
  int GetStickyNotificationCount() {
    return sticky_container_->child_count();
  }

  // Returns true if the |view| is contained in the panel.
  bool HasBalloonView(View* view) {
    return view->parent() == sticky_container_ ||
        view->parent() == non_sticky_container_;
  }

  // Updates the bounds so that all notifications are visible.
  void UpdateBounds() {
    sticky_container_->UpdateBounds();
    non_sticky_container_->UpdateBounds();
    preferred_size_ = sticky_container_->GetPreferredSize();

    gfx::Size non_sticky_size = non_sticky_container_->GetPreferredSize();
    int margin =
        (!preferred_size_.IsEmpty() && !non_sticky_size.IsEmpty()) ?
        margin_ : 0;
    preferred_size_.Enlarge(0, non_sticky_size.height() + margin);
    preferred_size_.set_width(std::max(
        preferred_size_.width(), non_sticky_size.width()));
    SizeToPreferredSize();
  }

  void MakeAllStale() {
    sticky_container_->MakeAllStale();
    non_sticky_container_->MakeAllStale();
  }

  void DismissAllNonSticky() {
    non_sticky_container_->DismissAll();
  }

  BalloonViewImpl* FindBalloonView(const Notification& notification) {
    BalloonViewImpl* view = sticky_container_->FindBalloonView(notification);
    return view ? view : non_sticky_container_->FindBalloonView(notification);
  }

  BalloonViewImpl* FindBalloonView(const gfx::Point& point) {
    BalloonViewImpl* view = sticky_container_->FindBalloonView(point);
    return view ? view : non_sticky_container_->FindBalloonView(point);
  }

 private:
  BalloonSubContainer* GetContainerFor(Balloon* balloon) const {
    BalloonViewImpl* view = GetBalloonViewOf(balloon);
    return view->sticky() ?
        sticky_container_ : non_sticky_container_;
  }

  int margin_;
  // Sticky/non-sticky ballon containers. They're child views and
  // deleted when this container is deleted.
  BalloonSubContainer* sticky_container_;
  BalloonSubContainer* non_sticky_container_;
  gfx::Size preferred_size_;

  DISALLOW_COPY_AND_ASSIGN(BalloonContainer);
};

NotificationPanel::NotificationPanel()
    : balloon_container_(NULL),
      panel_widget_(NULL),
      container_host_(NULL),
      state_(CLOSED),
      task_factory_(this),
      min_bounds_(0, 0, kBalloonMinWidth, kBalloonMinHeight),
      stale_timeout_(1000 * kStaleTimeoutInSeconds),
      active_(NULL),
      scroll_to_(NULL) {
  Init();
}

NotificationPanel::~NotificationPanel() {
  Hide();
}

////////////////////////////////////////////////////////////////////////////////
// NottificationPanel public.

void NotificationPanel::Show() {
  if (!panel_widget_) {
    // TODO(oshima): Using window because Popup widget behaves weird
    // when resizing. This needs to be investigated.
    views::WidgetGtk* widget_gtk =
        new views::WidgetGtk(views::WidgetGtk::TYPE_WINDOW);
    // Enable double buffering because the panel has both pure views
    // control and native controls (scroll bar).
    widget_gtk->EnableDoubleBuffer(true);
    panel_widget_ = widget_gtk;

    gfx::Rect bounds = GetPreferredBounds();
    bounds = bounds.Union(min_bounds_);
    panel_widget_->Init(NULL, bounds);
    // Set minimum bounds so that it can grow freely.
    gtk_widget_set_size_request(GTK_WIDGET(panel_widget_->GetNativeView()),
                                min_bounds_.width(), min_bounds_.height());

    views::NativeViewHost* native = new views::NativeViewHost();
    scroll_view_->SetContents(native);

    panel_widget_->SetContentsView(scroll_view_.get());

    // Add the view port after scroll_view is attached to the panel widget.
    ViewportWidget* widget = new ViewportWidget(this);
    container_host_ = widget;
    container_host_->Init(NULL, gfx::Rect());
    container_host_->SetContentsView(balloon_container_.get());
    // The window_contents_ is onwed by the WidgetGtk. Increase ref count
    // so that window_contents does not get deleted when detached.
    g_object_ref(widget->window_contents());
    native->Attach(widget->window_contents());

    UnregisterNotification();
    panel_controller_.reset(
        new PanelController(this, GTK_WINDOW(panel_widget_->GetNativeView())));
    panel_controller_->Init(false /* don't focus when opened */,
                            gfx::Rect(0, 0, kBalloonMinWidth, 1), 0,
                            WM_IPC_PANEL_USER_RESIZE_VERTICALLY);
    registrar_.Add(this, NotificationType::PANEL_STATE_CHANGED,
                   Source<PanelController>(panel_controller_.get()));
  }
  panel_widget_->Show();
}

void NotificationPanel::Hide() {
  balloon_container_->DismissAllNonSticky();
  if (panel_widget_) {
    container_host_->GetRootView()->RemoveChildView(balloon_container_.get());

    views::NativeViewHost* native =
        static_cast<views::NativeViewHost*>(scroll_view_->GetContents());
    native->Detach();
    scroll_view_->SetContents(NULL);
    container_host_->Hide();
    container_host_->CloseNow();
    container_host_ = NULL;

    UnregisterNotification();
    panel_controller_->Close();
    MessageLoop::current()->DeleteSoon(FROM_HERE, panel_controller_.release());
    // We need to remove & detach the scroll view from hierarchy to
    // avoid GTK deleting child.
    // TODO(oshima): handle this details in WidgetGtk.
    panel_widget_->GetRootView()->RemoveChildView(scroll_view_.get());
    panel_widget_->Close();
    panel_widget_ = NULL;
  }
}

////////////////////////////////////////////////////////////////////////////////
// BalloonCollectionImpl::NotificationUI overrides.

void NotificationPanel::Add(Balloon* balloon) {
  balloon_container_->Add(balloon);
  if (state_ == CLOSED || state_ == MINIMIZED)
    SET_STATE(STICKY_AND_NEW);
  Show();
  // Don't resize the panel yet. The panel will be resized when WebKit tells
  // the size in ResizeNotification.
  UpdatePanel(false);
  UpdateControl();
  StartStaleTimer(balloon);
  scroll_to_ = balloon;
}

bool NotificationPanel::Update(Balloon* balloon) {
  return balloon_container_->Update(balloon);
}

void NotificationPanel::Remove(Balloon* balloon) {
  BalloonViewImpl* view = balloon_container_->Remove(balloon);
  if (view == active_)
    active_ = NULL;
  if (scroll_to_ == balloon)
    scroll_to_ = NULL;

  // TODO(oshima): May be we shouldn't close
  // if the mouse pointer is still on the panel.
  if (balloon_container_->GetNotificationCount() == 0)
    SET_STATE(CLOSED);
  // no change to the state
  if (state_ == KEEP_SIZE) {
    // Just update the content.
    UpdateContainerBounds();
  } else {
    if (state_ != CLOSED &&
        balloon_container_->GetStickyNewNotificationCount() == 0)
      SET_STATE(MINIMIZED);
    UpdatePanel(true);
  }
  UpdateControl();
}

void NotificationPanel::Show(Balloon* balloon) {
  if (state_ == CLOSED || state_ == MINIMIZED)
    SET_STATE(STICKY_AND_NEW);
  Show();
  UpdatePanel(true);
  StartStaleTimer(balloon);
  ScrollBalloonToVisible(balloon);
}

void NotificationPanel::ResizeNotification(
    Balloon* balloon, const gfx::Size& size) {
  // restrict to the min & max sizes
  gfx::Size real_size(
      std::max(kBalloonMinWidth,
               std::min(kBalloonMaxWidth, size.width())),
      std::max(kBalloonMinHeight,
               std::min(kBalloonMaxHeight, size.height())));

  // Don't allow balloons to shrink.  This avoids flickering
  // which sometimes rapidly reports alternating sizes.  Special
  // case for setting the minimum value.
  gfx::Size old_size = balloon->content_size();
  if (real_size.width() > old_size.width() ||
      real_size.height() > old_size.height() ||
      real_size == min_bounds_.size()) {
    balloon->set_content_size(real_size);
    GetBalloonViewOf(balloon)->Layout();
    UpdatePanel(true);
    if (scroll_to_ == balloon) {
      ScrollBalloonToVisible(scroll_to_);
      scroll_to_ = NULL;
    }
  }
}

void NotificationPanel::SetActiveView(BalloonViewImpl* view) {
  // Don't change the active view if it's same notification,
  // or the notification is being closed.
  if (active_ == view || (view && view->closed()))
    return;
  if (active_)
    active_->Deactivated();
  active_ = view;
  if (active_)
    active_->Activated();
}

////////////////////////////////////////////////////////////////////////////////
// PanelController overrides.

string16 NotificationPanel::GetPanelTitle() {
  return string16(l10n_util::GetStringUTF16(IDS_NOTIFICATION_PANEL_TITLE));
}

SkBitmap NotificationPanel::GetPanelIcon() {
  return SkBitmap();
}

bool NotificationPanel::CanClosePanel() {
  return true;
}

void NotificationPanel::ClosePanel() {
  SET_STATE(CLOSED);
  UpdatePanel(false);
}

void NotificationPanel::ActivatePanel() {
  if (active_)
    active_->Activated();
}

////////////////////////////////////////////////////////////////////////////////
// NotificationObserver overrides.

void NotificationPanel::Observe(NotificationType type,
                                const NotificationSource& source,
                                const NotificationDetails& details) {
  DCHECK(type == NotificationType::PANEL_STATE_CHANGED);
  PanelController::State* state =
      reinterpret_cast<PanelController::State*>(details.map_key());
  switch (*state) {
    case PanelController::EXPANDED:
      // Geting expanded in STICKY_AND_NEW or in KEEP_SIZE state means
      // that a new notification is added, so just leave the
      // state. Otherwise, expand to full.
      if (state_ != STICKY_AND_NEW && state_ != KEEP_SIZE)
        SET_STATE(FULL);
      // When the panel is to be expanded, we either show all, or
      // show only sticky/new, depending on the state.
      UpdatePanel(false);
      break;
    case PanelController::MINIMIZED:
      SET_STATE(MINIMIZED);
      // Make all notifications stale when a user minimize the panel.
      balloon_container_->MakeAllStale();
      break;
    case PanelController::INITIAL:
      NOTREACHED() << "Transition to Initial state should not happen";
  }
}

////////////////////////////////////////////////////////////////////////////////
// PanelController public.

void NotificationPanel::OnMouseLeave() {
  SetActiveView(NULL);
  if (balloon_container_->GetNotificationCount() == 0)
    SET_STATE(CLOSED);
  UpdatePanel(true);
}

void NotificationPanel::OnMouseMotion(const gfx::Point& point) {
  SetActiveView(balloon_container_->FindBalloonView(point));
  SET_STATE(KEEP_SIZE);
}

NotificationPanelTester* NotificationPanel::GetTester() {
  if (!tester_.get())
    tester_.reset(new NotificationPanelTester(this));
  return tester_.get();
}

////////////////////////////////////////////////////////////////////////////////
// NotificationPanel private.

void NotificationPanel::Init() {
  DCHECK(!panel_widget_);
  balloon_container_.reset(new BalloonContainer(1));
  balloon_container_->set_parent_owned(false);
  balloon_container_->set_background(
      views::Background::CreateSolidBackground(ResourceBundle::frame_color));

  scroll_view_.reset(new views::ScrollView());
  scroll_view_->set_parent_owned(false);
  scroll_view_->set_background(
      views::Background::CreateSolidBackground(SK_ColorWHITE));
}

void NotificationPanel::UnregisterNotification() {
  if (panel_controller_.get())
    registrar_.Remove(this, NotificationType::PANEL_STATE_CHANGED,
                      Source<PanelController>(panel_controller_.get()));
}

void NotificationPanel::ScrollBalloonToVisible(Balloon* balloon) {
  BalloonViewImpl* view = GetBalloonViewOf(balloon);
  if (!view->closed()) {
    // We can't use View::ScrollRectToVisible because the viewport is not
    // ancestor of the BalloonViewImpl.
    // Use Widget's coordinate which is same as viewport's coordinates.
    gfx::Point p(0, 0);
    views::View::ConvertPointToWidget(view, &p);
    gfx::Rect visible_rect(p.x(), p.y(), view->width(), view->height());
    scroll_view_->ScrollContentsRegionToBeVisible(visible_rect);
  }
}

void NotificationPanel::UpdatePanel(bool update_container_size) {
  if (update_container_size)
    UpdateContainerBounds();
  switch (state_) {
    case KEEP_SIZE: {
      gfx::Rect min_bounds = GetPreferredBounds();
      gfx::Rect panel_bounds = panel_widget_->GetWindowScreenBounds();
      if (min_bounds.height() < panel_bounds.height())
        panel_widget_->SetBounds(min_bounds);
      else if (min_bounds.height() > panel_bounds.height()) {
        // need scroll bar
        int width = balloon_container_->width() +
            scroll_view_->GetScrollBarWidth();
        panel_bounds.set_width(width);
        panel_widget_->SetBounds(panel_bounds);
      }

      // no change.
      break;
    }
    case CLOSED:
      Hide();
      break;
    case MINIMIZED:
      balloon_container_->MakeAllStale();
      if (panel_controller_.get())
        panel_controller_->SetState(PanelController::MINIMIZED);
      break;
    case FULL:
      if (panel_widget_) {
        panel_widget_->SetBounds(GetPreferredBounds());
        panel_controller_->SetState(PanelController::EXPANDED);
      }
      break;
    case STICKY_AND_NEW:
      if (panel_widget_) {
        panel_widget_->SetBounds(GetStickyNewBounds());
        panel_controller_->SetState(PanelController::EXPANDED);
      }
      break;
  }
}

void NotificationPanel::UpdateContainerBounds() {
  balloon_container_->UpdateBounds();
  views::NativeViewHost* native =
      static_cast<views::NativeViewHost*>(scroll_view_->GetContents());
  // Update from WebKit may arrive after the panel is closed/hidden
  // and viewport widget is detached.
  if (native) {
    native->SetBoundsRect(balloon_container_->bounds());
    scroll_view_->Layout();
  }
}

void NotificationPanel::UpdateControl() {
  if (container_host_)
    static_cast<ViewportWidget*>(container_host_)->UpdateControl();
}

gfx::Rect NotificationPanel::GetPreferredBounds() {
  gfx::Size pref_size = balloon_container_->GetPreferredSize();
  int new_height = std::min(pref_size.height(), kMaxPanelHeight);
  int new_width = pref_size.width();
  // Adjust the width to avoid showing a horizontal scroll bar.
  if (new_height != pref_size.height()) {
    new_width += scroll_view_->GetScrollBarWidth();
  }
  return gfx::Rect(0, 0, new_width, new_height).Union(min_bounds_);
}

gfx::Rect NotificationPanel::GetStickyNewBounds() {
  gfx::Size pref_size = balloon_container_->GetPreferredSize();
  gfx::Size sticky_size = balloon_container_->GetStickyNewSize();
  int new_height = std::min(sticky_size.height(), kMaxPanelHeight);
  int new_width = pref_size.width();
  // Adjust the width to avoid showing a horizontal scroll bar.
  if (new_height != pref_size.height())
    new_width += scroll_view_->GetScrollBarWidth();
  return gfx::Rect(0, 0, new_width, new_height).Union(min_bounds_);
}

void NotificationPanel::StartStaleTimer(Balloon* balloon) {
  BalloonViewImpl* view = GetBalloonViewOf(balloon);
  MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      task_factory_.NewRunnableMethod(
          &NotificationPanel::OnStale, view),
      stale_timeout_);
}

void NotificationPanel::OnStale(BalloonViewImpl* view) {
  if (balloon_container_->HasBalloonView(view) && !view->stale()) {
    view->set_stale();
    // don't update panel on stale
    if (state_ == KEEP_SIZE)
      return;
    if (balloon_container_->GetStickyNewNotificationCount() > 0) {
      SET_STATE(STICKY_AND_NEW);
    } else {
      SET_STATE(MINIMIZED);
    }
    UpdatePanel(false);
  }
}

void NotificationPanel::SetState(State new_state, const char* name) {
#if !defined(NDEBUG)
  DVLOG(1) << "state transition " << ToStr(state_) << " >> " << ToStr(new_state)
           << " in " << name;
#endif
  state_ = new_state;
}

void NotificationPanel::MarkStale(const Notification& notification) {
  BalloonViewImpl* view = balloon_container_->FindBalloonView(notification);
  DCHECK(view);
  OnStale(view);
}

////////////////////////////////////////////////////////////////////////////////
// NotificationPanelTester public.

int NotificationPanelTester::GetNotificationCount() const {
  return panel_->balloon_container_->GetNotificationCount();
}

int NotificationPanelTester::GetStickyNotificationCount() const {
  return panel_->balloon_container_->GetStickyNotificationCount();
}

int NotificationPanelTester::GetNewNotificationCount() const {
  return panel_->balloon_container_->GetNewNotificationCount();
}

void NotificationPanelTester::SetStaleTimeout(int timeout) {
  panel_->stale_timeout_ = timeout;
}

void NotificationPanelTester::MarkStale(const Notification& notification) {
  panel_->MarkStale(notification);
}

PanelController* NotificationPanelTester::GetPanelController() const {
  return panel_->panel_controller_.get();
}

BalloonViewImpl* NotificationPanelTester::GetBalloonView(
    BalloonCollectionImpl* collection,
    const Notification& notification) {
  Balloon* balloon = collection->FindBalloon(notification);
  DCHECK(balloon);
  return GetBalloonViewOf(balloon);
}

bool NotificationPanelTester::IsVisible(const BalloonViewImpl* view) const {
  gfx::Rect rect = panel_->scroll_view_->GetVisibleRect();
  gfx::Point origin(0, 0);
  views::View::ConvertPointToView(view, panel_->balloon_container_.get(),
                                  &origin);
  return rect.Contains(gfx::Rect(origin, view->size()));
}


bool NotificationPanelTester::IsActive(const BalloonViewImpl* view) const {
  return panel_->active_ == view;
}

}  // namespace chromeos