// 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, ©); 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