summaryrefslogtreecommitdiffstats
path: root/chrome/browser/chromeos/notifications
diff options
context:
space:
mode:
authorBen Murdoch <benm@google.com>2010-07-29 17:14:53 +0100
committerBen Murdoch <benm@google.com>2010-08-04 14:29:45 +0100
commitc407dc5cd9bdc5668497f21b26b09d988ab439de (patch)
tree7eaf8707c0309516bdb042ad976feedaf72b0bb1 /chrome/browser/chromeos/notifications
parent0998b1cdac5733f299c12d88bc31ef9c8035b8fa (diff)
downloadexternal_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.zip
external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.gz
external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.bz2
Merge Chromium src@r53293
Change-Id: Ia79acf8670f385cee48c45b0a75371d8e950af34
Diffstat (limited to 'chrome/browser/chromeos/notifications')
-rw-r--r--chrome/browser/chromeos/notifications/balloon_collection_impl.cc183
-rw-r--r--chrome/browser/chromeos/notifications/balloon_collection_impl.h130
-rw-r--r--chrome/browser/chromeos/notifications/balloon_view.cc344
-rw-r--r--chrome/browser/chromeos/notifications/balloon_view.h131
-rw-r--r--chrome/browser/chromeos/notifications/desktop_notifications_unittest.cc273
-rw-r--r--chrome/browser/chromeos/notifications/desktop_notifications_unittest.h113
-rw-r--r--chrome/browser/chromeos/notifications/notification_browsertest.cc549
-rw-r--r--chrome/browser/chromeos/notifications/notification_panel.cc871
-rw-r--r--chrome/browser/chromeos/notifications/notification_panel.h245
-rw-r--r--chrome/browser/chromeos/notifications/system_notification.cc70
-rw-r--r--chrome/browser/chromeos/notifications/system_notification.h78
-rw-r--r--chrome/browser/chromeos/notifications/system_notification_factory.cc21
-rw-r--r--chrome/browser/chromeos/notifications/system_notification_factory.h29
13 files changed, 3037 insertions, 0 deletions
diff --git a/chrome/browser/chromeos/notifications/balloon_collection_impl.cc b/chrome/browser/chromeos/notifications/balloon_collection_impl.cc
new file mode 100644
index 0000000..31a6ef7
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/balloon_collection_impl.cc
@@ -0,0 +1,183 @@
+// Copyright (c) 2010 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.
+
+#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
+
+#include <algorithm>
+
+#include "base/logging.h"
+#include "base/stl_util-inl.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/chromeos/notifications/balloon_view.h"
+#include "chrome/browser/chromeos/notifications/notification_panel.h"
+#include "chrome/browser/notifications/balloon.h"
+#include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/window_sizer.h"
+#include "chrome/common/notification_service.h"
+#include "gfx/rect.h"
+#include "gfx/size.h"
+
+namespace {
+
+// Margin from the edge of the work area
+const int kVerticalEdgeMargin = 5;
+const int kHorizontalEdgeMargin = 5;
+
+class NotificationMatcher {
+ public:
+ explicit NotificationMatcher(const Notification& notification)
+ : notification_(notification) {}
+ bool operator()(const Balloon* b) const {
+ return notification_.IsSame(b->notification());
+ }
+ private:
+ Notification notification_;
+};
+
+} // namespace
+
+namespace chromeos {
+
+BalloonCollectionImpl::BalloonCollectionImpl()
+ : notification_ui_(new NotificationPanel()) {
+ registrar_.Add(this, NotificationType::BROWSER_CLOSED,
+ NotificationService::AllSources());
+}
+
+BalloonCollectionImpl::~BalloonCollectionImpl() {
+ Shutdown();
+}
+
+void BalloonCollectionImpl::Add(const Notification& notification,
+ Profile* profile) {
+ Balloon* new_balloon = MakeBalloon(notification, profile);
+ balloons_.push_back(new_balloon);
+ new_balloon->Show();
+ notification_ui_->Add(new_balloon);
+
+ // There may be no listener in a unit test.
+ if (space_change_listener_)
+ space_change_listener_->OnBalloonSpaceChanged();
+}
+
+void BalloonCollectionImpl::AddSystemNotification(
+ const Notification& notification,
+ Profile* profile,
+ bool sticky,
+ bool control) {
+ // TODO(oshima): We need to modify BallonCollection/MakeBalloon pattern
+ // in order to add unit tests for system notification.
+ Balloon* new_balloon = new Balloon(notification, profile, this);
+ new_balloon->set_view(
+ new chromeos::BalloonViewImpl(sticky, control));
+ balloons_.push_back(new_balloon);
+ new_balloon->Show();
+ notification_ui_->Add(new_balloon);
+
+ // There may be no listener in a unit test.
+ if (space_change_listener_)
+ space_change_listener_->OnBalloonSpaceChanged();
+}
+
+bool BalloonCollectionImpl::UpdateNotification(
+ const Notification& notification) {
+ Balloons::iterator iter = FindBalloon(notification);
+ if (iter == balloons_.end())
+ return false;
+ Balloon* balloon = *iter;
+ balloon->Update(notification);
+ notification_ui_->Update(balloon);
+ return true;
+}
+
+bool BalloonCollectionImpl::UpdateAndShowNotification(
+ const Notification& notification) {
+ Balloons::iterator iter = FindBalloon(notification);
+ if (iter == balloons_.end())
+ return false;
+ Balloon* balloon = *iter;
+ balloon->Update(notification);
+ bool updated = notification_ui_->Update(balloon);
+ DCHECK(updated);
+ notification_ui_->Show(balloon);
+ return true;
+}
+
+bool BalloonCollectionImpl::Remove(const Notification& notification) {
+ Balloons::iterator iter = FindBalloon(notification);
+ if (iter != balloons_.end()) {
+ // Balloon.CloseByScript() will cause OnBalloonClosed() to be called on
+ // this object, which will remove it from the collection and free it.
+ (*iter)->CloseByScript();
+ return true;
+ }
+ return false;
+}
+
+bool BalloonCollectionImpl::HasSpace() const {
+ return true;
+}
+
+void BalloonCollectionImpl::ResizeBalloon(Balloon* balloon,
+ const gfx::Size& size) {
+ notification_ui_->ResizeNotification(balloon, size);
+}
+
+void BalloonCollectionImpl::OnBalloonClosed(Balloon* source) {
+ // We want to free the balloon when finished.
+ scoped_ptr<Balloon> closed(source);
+
+ notification_ui_->Remove(source);
+
+ Balloons::iterator iter = FindBalloon(source->notification());
+ if (iter != balloons_.end()) {
+ balloons_.erase(iter);
+ }
+ // There may be no listener in a unit test.
+ if (space_change_listener_)
+ space_change_listener_->OnBalloonSpaceChanged();
+}
+
+void BalloonCollectionImpl::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK(type == NotificationType::BROWSER_CLOSED);
+ bool app_closing = *Details<bool>(details).ptr();
+ // When exitting, we need to shutdown all renderers in
+ // BalloonViewImpl before IO thread gets deleted in the
+ // BrowserProcessImpl's destructor. See http://crbug.com/40810
+ // for details.
+ if(app_closing)
+ Shutdown();
+}
+
+void BalloonCollectionImpl::Shutdown() {
+ // We need to remove the panel first because deleting
+ // views that are not owned by parent will not remove
+ // themselves from the parent.
+ DLOG(INFO) << "Shutting down notification UI";
+ notification_ui_.reset();
+ STLDeleteElements(&balloons_);
+}
+
+Balloon* BalloonCollectionImpl::MakeBalloon(const Notification& notification,
+ Profile* profile) {
+ Balloon* new_balloon = new Balloon(notification, profile, this);
+ new_balloon->set_view(new chromeos::BalloonViewImpl(false, true));
+ return new_balloon;
+}
+
+std::deque<Balloon*>::iterator BalloonCollectionImpl::FindBalloon(
+ const Notification& notification) {
+ return std::find_if(balloons_.begin(),
+ balloons_.end(),
+ NotificationMatcher(notification));
+}
+
+} // namespace chromeos
+
+// static
+BalloonCollection* BalloonCollection::Create() {
+ return new chromeos::BalloonCollectionImpl();
+}
diff --git a/chrome/browser/chromeos/notifications/balloon_collection_impl.h b/chrome/browser/chromeos/notifications/balloon_collection_impl.h
new file mode 100644
index 0000000..7ae7763
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/balloon_collection_impl.h
@@ -0,0 +1,130 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_BALLOON_COLLECTION_IMPL_H_
+#define CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_BALLOON_COLLECTION_IMPL_H_
+
+#include <deque>
+
+#include "base/basictypes.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/notifications/balloon_collection.h"
+#include "chrome/common/notification_registrar.h"
+#include "gfx/point.h"
+#include "gfx/rect.h"
+
+namespace gfx {
+class Size;
+} // namespace gfx
+
+namespace chromeos {
+
+class BalloonViewImpl;
+
+// A balloon collection represents a set of notification balloons being
+// shown in the chromeos notification panel. Unlike other platforms,
+// chromeos shows the all notifications in the notification panel, and
+// this class does not manage the location of balloons.
+class BalloonCollectionImpl : public BalloonCollection,
+ public NotificationObserver {
+ public:
+ // An interface to display balloons on the screen.
+ // This is used for unit tests to inject a mock ui implementation.
+ class NotificationUI {
+ public:
+ NotificationUI() {}
+ virtual ~NotificationUI() {}
+
+ // Add, remove, resize and show the balloon.
+ virtual void Add(Balloon* balloon) = 0;
+ virtual bool Update(Balloon* balloon) = 0;
+ virtual void Remove(Balloon* balloon) = 0;
+ virtual void Show(Balloon* balloon) = 0;
+
+ // Resize notification from webkit.
+ virtual void ResizeNotification(Balloon* balloon,
+ const gfx::Size& size) = 0;
+
+ // Sets the active view.
+ virtual void SetActiveView(BalloonViewImpl* view) = 0;
+ private:
+ DISALLOW_COPY_AND_ASSIGN(NotificationUI);
+ };
+
+ BalloonCollectionImpl();
+ virtual ~BalloonCollectionImpl();
+
+ // BalloonCollectionInterface overrides
+ virtual void Add(const Notification& notification,
+ Profile* profile);
+ virtual bool Remove(const Notification& notification);
+ virtual bool HasSpace() const;
+ virtual void ResizeBalloon(Balloon* balloon, const gfx::Size& size);
+ virtual void DisplayChanged() {}
+ virtual void OnBalloonClosed(Balloon* source);
+ virtual const Balloons& GetActiveBalloons() { return balloons_; }
+
+ // NotificationObserver overrides:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Adds new system notification.
+ // |sticky| is used to indicate that the notification
+ // is sticky and cannot be dismissed by a user. |controls| turns on/off
+ // info label and option/dismiss buttons.
+ void AddSystemNotification(const Notification& notification,
+ Profile* profile, bool sticky, bool controls);
+
+ // Updates the notification's content. It uses
+ // NotificationDelegate::id() to check the equality of notifications.
+ // Returns true if the notification has been updated. False if
+ // no corresponding notification is found. This will not change the
+ // visibility of the notification.
+ bool UpdateNotification(const Notification& notification);
+
+ // Updates and shows the notification. It will open the notification panel
+ // if it's closed or minimized, and scroll the viewport so that
+ // the updated notification is visible.
+ bool UpdateAndShowNotification(const Notification& notification);
+
+ // Injects notification ui. Used to inject a mock implementation in tests.
+ void set_notification_ui(NotificationUI* ui) {
+ notification_ui_.reset(ui);
+ }
+
+ NotificationUI* notification_ui() {
+ return notification_ui_.get();
+ }
+
+ protected:
+ // Creates a new balloon. Overridable by unit tests. The caller is
+ // responsible for freeing the pointer returned.
+ virtual Balloon* MakeBalloon(const Notification& notification,
+ Profile* profile);
+
+ private:
+ friend class NotificationPanelTester;
+
+ // Shutdown the notification ui.
+ void Shutdown();
+
+ // The number of balloons being displayed.
+ int count() const { return balloons_.size(); }
+
+ Balloons::iterator FindBalloon(const Notification& notification);
+
+ // Queue of active balloons.
+ Balloons balloons_;
+
+ scoped_ptr<NotificationUI> notification_ui_;
+
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(BalloonCollectionImpl);
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_BALLOON_COLLECTION_IMPL_H_
diff --git a/chrome/browser/chromeos/notifications/balloon_view.cc b/chrome/browser/chromeos/notifications/balloon_view.cc
new file mode 100644
index 0000000..6ee8043
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/balloon_view.cc
@@ -0,0 +1,344 @@
+// Copyright (c) 2010 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.
+
+#include "chrome/browser/chromeos/notifications/balloon_view.h"
+
+#include <vector>
+
+#include "app/l10n_util.h"
+#include "app/menus/simple_menu_model.h"
+#include "app/resource_bundle.h"
+#include "base/message_loop.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/chromeos/notifications/notification_panel.h"
+#include "chrome/browser/notifications/balloon.h"
+#include "chrome/browser/notifications/desktop_notification_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/renderer_host/render_widget_host_view.h"
+#include "chrome/browser/views/notifications/balloon_view_host.h"
+#include "chrome/common/notification_details.h"
+#include "chrome/common/notification_source.h"
+#include "chrome/common/notification_type.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "views/background.h"
+#include "views/controls/button/button.h"
+#include "views/controls/button/image_button.h"
+#include "views/controls/button/menu_button.h"
+#include "views/controls/label.h"
+#include "views/controls/menu/menu_2.h"
+#include "views/controls/menu/view_menu_delegate.h"
+#include "views/widget/root_view.h"
+#include "views/widget/widget_gtk.h"
+
+namespace {
+// Menu commands
+const int kNoopCommand = 0;
+const int kRevokePermissionCommand = 1;
+
+// Vertical margin between close button and menu button.
+const int kControlButtonsMargin = 6;
+
+// Top, Right margin for notification control view.
+const int kControlViewTopMargin = 4;
+const int kControlViewRightMargin = 6;
+} // namespace
+
+namespace chromeos {
+
+// NotificationControlView has close and menu buttons and
+// overlays on top of renderer view.
+class NotificationControlView : public views::View,
+ public views::ViewMenuDelegate,
+ public menus::SimpleMenuModel::Delegate,
+ public views::ButtonListener {
+ public:
+ explicit NotificationControlView(BalloonViewImpl* view)
+ : balloon_view_(view),
+ close_button_(NULL),
+ options_menu_contents_(NULL),
+ options_menu_menu_(NULL),
+ options_menu_button_(NULL) {
+ // TODO(oshima): make background transparent.
+ set_background(views::Background::CreateSolidBackground(SK_ColorWHITE));
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+
+ SkBitmap* close_button_n = rb.GetBitmapNamed(IDR_TAB_CLOSE);
+ SkBitmap* close_button_m = rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK);
+ SkBitmap* close_button_h = rb.GetBitmapNamed(IDR_TAB_CLOSE_H);
+ SkBitmap* close_button_p = rb.GetBitmapNamed(IDR_TAB_CLOSE_P);
+
+ close_button_ = new views::ImageButton(this);
+ close_button_->SetImage(views::CustomButton::BS_NORMAL, close_button_n);
+ close_button_->SetImage(views::CustomButton::BS_HOT, close_button_h);
+ close_button_->SetImage(views::CustomButton::BS_PUSHED, close_button_p);
+ close_button_->SetBackground(
+ SK_ColorBLACK, close_button_n, close_button_m);
+
+ AddChildView(close_button_);
+
+ options_menu_button_
+ = new views::MenuButton(NULL, std::wstring(), this, false);
+ options_menu_button_->SetFont(rb.GetFont(ResourceBundle::SmallFont));
+ options_menu_button_->SetIcon(*rb.GetBitmapNamed(IDR_NOTIFICATION_MENU));
+ options_menu_button_->set_border(NULL);
+
+ options_menu_button_->set_icon_placement(views::TextButton::ICON_ON_RIGHT);
+ AddChildView(options_menu_button_);
+
+ // The control view will never be resized, so just layout once.
+ gfx::Size options_size = options_menu_button_->GetPreferredSize();
+ gfx::Size button_size = close_button_->GetPreferredSize();
+
+ int height = std::max(options_size.height(), button_size.height());
+ options_menu_button_->SetBounds(
+ 0, (height - options_size.height()) / 2,
+ options_size.width(), options_size.height());
+
+ close_button_->SetBounds(
+ options_size.width() + kControlButtonsMargin,
+ (height - button_size.height()) / 2,
+ button_size.width(), button_size.height());
+
+ SizeToPreferredSize();
+ }
+
+ virtual gfx::Size GetPreferredSize() {
+ gfx::Rect total_bounds =
+ close_button_->bounds().Union(options_menu_button_->bounds());
+ return total_bounds.size();
+ }
+
+ // views::ViewMenuDelegate implements.
+ virtual void RunMenu(views::View* source, const gfx::Point& pt) {
+ CreateOptionsMenu();
+ options_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT);
+ }
+
+ // views::ButtonListener implements.
+ virtual void ButtonPressed(views::Button* sender, const views::Event&) {
+ balloon_view_->Close(true);
+ }
+
+ // menus::SimpleMenuModel::Delegate impglements.
+ virtual bool IsCommandIdChecked(int /* command_id */) const {
+ // Nothing in the menu is checked.
+ return false;
+ }
+
+ virtual bool IsCommandIdEnabled(int /* command_id */) const {
+ // All the menu options are always enabled.
+ return true;
+ }
+
+ virtual bool GetAcceleratorForCommandId(
+ int /* command_id */, menus::Accelerator* /* accelerator */) {
+ // Currently no accelerators.
+ return false;
+ }
+
+ virtual void ExecuteCommand(int command_id) {
+ switch (command_id) {
+ case kRevokePermissionCommand:
+ balloon_view_->DenyPermission();
+ default:
+ NOTIMPLEMENTED();
+ }
+ }
+
+ private:
+ void CreateOptionsMenu() {
+ if (options_menu_contents_.get())
+ return;
+ const string16 source_label_text = WideToUTF16Hack(l10n_util::GetStringF(
+ IDS_NOTIFICATION_BALLOON_SOURCE_LABEL,
+ balloon_view_->balloon_->notification().display_source()));
+ const string16 label_text = WideToUTF16Hack(l10n_util::GetStringF(
+ IDS_NOTIFICATION_BALLOON_REVOKE_MESSAGE,
+ balloon_view_->balloon_->notification().display_source()));
+
+ options_menu_contents_.reset(new menus::SimpleMenuModel(this));
+ // TODO(oshima): Showing the source info in the menu for now.
+ // Figure out where to show the source info.
+ options_menu_contents_->AddItem(kNoopCommand, source_label_text);
+ options_menu_contents_->AddItem(kRevokePermissionCommand, label_text);
+
+ options_menu_menu_.reset(new views::Menu2(options_menu_contents_.get()));
+ }
+
+ BalloonViewImpl* balloon_view_;
+
+ views::ImageButton* close_button_;
+
+ // The options menu.
+ scoped_ptr<menus::SimpleMenuModel> options_menu_contents_;
+ scoped_ptr<views::Menu2> options_menu_menu_;
+ views::MenuButton* options_menu_button_;
+
+ DISALLOW_COPY_AND_ASSIGN(NotificationControlView);
+};
+
+BalloonViewImpl::BalloonViewImpl(bool sticky, bool controls)
+ : balloon_(NULL),
+ html_contents_(NULL),
+ method_factory_(this),
+ stale_(false),
+ sticky_(sticky),
+ controls_(controls),
+ closed_(false) {
+ // This object is not to be deleted by the views hierarchy,
+ // as it is owned by the balloon.
+ set_parent_owned(false);
+}
+
+BalloonViewImpl::~BalloonViewImpl() {
+ if (control_view_host_.get()) {
+ control_view_host_->CloseNow();
+ }
+ if (html_contents_) {
+ html_contents_->Shutdown();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// BallonViewImpl, BalloonView implementation.
+
+void BalloonViewImpl::Show(Balloon* balloon) {
+ balloon_ = balloon;
+
+ html_contents_ = new BalloonViewHost(balloon);
+ AddChildView(html_contents_->view());
+ notification_registrar_.Add(this,
+ NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon));
+}
+
+void BalloonViewImpl::Update() {
+ stale_ = false;
+ if (html_contents_->render_view_host())
+ html_contents_->render_view_host()->NavigateToURL(
+ balloon_->notification().content_url());
+}
+
+void BalloonViewImpl::Close(bool by_user) {
+ closed_ = true;
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ method_factory_.NewRunnableMethod(
+ &BalloonViewImpl::DelayedClose, by_user));
+}
+
+gfx::Size BalloonViewImpl::GetSize() const {
+ // Not used. The layout is managed by the Panel.
+ return gfx::Size(0, 0);
+}
+
+BalloonHost* BalloonViewImpl::GetHost() const {
+ return html_contents_;
+}
+
+void BalloonViewImpl::RepositionToBalloon() {
+ // Not used. The layout is managed by the Panel.
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// views::View interface overrides.
+
+void BalloonViewImpl::Layout() {
+ gfx::Size size = balloon_->content_size();
+
+ SetBounds(x(), y(), size.width(), size.height());
+
+ html_contents_->view()->SetBounds(0, 0, size.width(), size.height());
+ if (html_contents_->render_view_host()) {
+ RenderWidgetHostView* view = html_contents_->render_view_host()->view();
+ if (view)
+ view->SetSize(size);
+ }
+}
+
+void BalloonViewImpl::ViewHierarchyChanged(
+ bool is_add, View* parent, View* child) {
+ if (is_add && GetWidget() && !control_view_host_.get() && controls_) {
+ control_view_host_.reset(
+ new views::WidgetGtk(views::WidgetGtk::TYPE_CHILD));
+ control_view_host_->EnableDoubleBuffer(true);
+ control_view_host_->Init(GetParentNativeView(), gfx::Rect());
+ NotificationControlView* control = new NotificationControlView(this);
+ control_view_host_->set_delete_on_destroy(false);
+ control_view_host_->SetContentsView(control);
+ }
+ if (!is_add && this == child && control_view_host_.get() && controls_) {
+ control_view_host_.release()->CloseNow();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// NotificationObserver overrides.
+
+void BalloonViewImpl::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type != NotificationType::NOTIFY_BALLOON_DISCONNECTED) {
+ NOTREACHED();
+ return;
+ }
+
+ // If the renderer process attached to this balloon is disconnected
+ // (e.g., because of a crash), we want to close the balloon.
+ notification_registrar_.Remove(this,
+ NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon_));
+ Close(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// BalloonViewImpl public.
+
+bool BalloonViewImpl::IsFor(const Notification& notification) const {
+ return balloon_->notification().IsSame(notification);
+}
+
+void BalloonViewImpl::Activated() {
+ if (!control_view_host_.get())
+ return;
+
+ // Get the size of Control View.
+ gfx::Size size =
+ control_view_host_->GetRootView()->GetChildViewAt(0)->GetPreferredSize();
+ control_view_host_->Show();
+ control_view_host_->SetBounds(
+ gfx::Rect(width() - size.width() - kControlViewRightMargin,
+ kControlViewTopMargin,
+ size.width(), size.height()));
+}
+
+void BalloonViewImpl::Deactivated() {
+ if (control_view_host_.get()) {
+ control_view_host_->Hide();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// BalloonViewImpl private.
+
+void BalloonViewImpl::DelayedClose(bool by_user) {
+ html_contents_->Shutdown();
+ html_contents_ = NULL;
+ balloon_->OnClose(by_user);
+}
+
+void BalloonViewImpl::DenyPermission() {
+ DesktopNotificationService* service =
+ balloon_->profile()->GetDesktopNotificationService();
+ service->DenyPermission(balloon_->notification().origin_url());
+}
+
+gfx::NativeView BalloonViewImpl::GetParentNativeView() {
+ RenderWidgetHostView* view = html_contents_->render_view_host()->view();
+ DCHECK(view);
+ return view->GetNativeView();
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/notifications/balloon_view.h b/chrome/browser/chromeos/notifications/balloon_view.h
new file mode 100644
index 0000000..c6032e6
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/balloon_view.h
@@ -0,0 +1,131 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_BALLOON_VIEW_H_
+#define CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_BALLOON_VIEW_H_
+
+#include "base/basictypes.h"
+#include "base/scoped_ptr.h"
+#include "base/task.h"
+#include "chrome/browser/notifications/balloon.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "gfx/path.h"
+#include "gfx/point.h"
+#include "gfx/rect.h"
+#include "gfx/size.h"
+#include "views/view.h"
+
+namespace views {
+class Menu2;
+class MenuButton;
+class MouseEvent;
+class TextButton;
+class WidgetGtk;
+} // namespace views
+
+class BalloonViewHost;
+class Notification;
+class NotificationDetails;
+class NotificationSource;
+
+namespace chromeos {
+
+class NotificationControlView;
+
+// A balloon view is the UI component for a notification panel.
+class BalloonViewImpl : public BalloonView,
+ public views::View,
+ public NotificationObserver {
+ public:
+ BalloonViewImpl(bool sticky, bool controls);
+ ~BalloonViewImpl();
+
+ // views::View interface.
+ virtual void Layout();
+ virtual void ViewHierarchyChanged(bool is_add, View* parent, View* child);
+
+ // BalloonView interface.
+ virtual void Show(Balloon* balloon);
+ virtual void Update();
+ virtual void Close(bool by_user);
+ virtual void RepositionToBalloon();
+ gfx::Size GetSize() const;
+ virtual BalloonHost* GetHost() const;
+
+ // True if the notification is stale. False if the notification is new.
+ bool stale() const { return stale_; }
+
+ // Makes the notification stale.
+ void set_stale() { stale_ = true; }
+
+ // True if the notification is sticky.
+ bool sticky() const { return sticky_; }
+
+ // True if the notification is being closed.
+ bool closed() const { return closed_; }
+
+ // True if the balloon is for the given |notification|.
+ bool IsFor(const Notification& notification) const;
+
+ // Called when the notification becomes active (mouse is on).
+ void Activated();
+
+ // Called when the notification becomes inactive.
+ void Deactivated();
+
+ private:
+ friend class NotificationControlView;
+
+ // views::View interface.
+ virtual gfx::Size GetPreferredSize() {
+ return gfx::Size(1000, 1000);
+ }
+
+ // NotificationObserver interface.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Initializes the options menu.
+ void CreateOptionsMenu();
+
+ // Do the delayed close work.
+ void DelayedClose(bool by_user);
+
+ // Denies the permission to show the ballooon from its source.
+ void DenyPermission();
+
+ // Returns the renderer's native view.
+ gfx::NativeView GetParentNativeView();
+
+ // Non-owned pointer to the balloon which owns this object.
+ Balloon* balloon_;
+
+ // The renderer of the HTML contents. Pointer owned by the views hierarchy.
+ BalloonViewHost* html_contents_;
+
+ // The following factory is used to call methods at a later time.
+ ScopedRunnableMethodFactory<BalloonViewImpl> method_factory_;
+
+ // A widget for ControlView.
+ scoped_ptr<views::WidgetGtk> control_view_host_;
+
+ bool stale_;
+ NotificationRegistrar notification_registrar_;
+ // A sticky flag. A sticky notification cannot be dismissed by a user.
+ bool sticky_;
+ // True if a notification should have info/option/dismiss label/buttons.
+ bool controls_;
+ // True if the notification is being closed.
+ bool closed_;
+
+ DISALLOW_COPY_AND_ASSIGN(BalloonViewImpl);
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_BALLOON_VIEW_H_
diff --git a/chrome/browser/chromeos/notifications/desktop_notifications_unittest.cc b/chrome/browser/chromeos/notifications/desktop_notifications_unittest.cc
new file mode 100644
index 0000000..cbdf0e4
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/desktop_notifications_unittest.cc
@@ -0,0 +1,273 @@
+// Copyright (c) 2010 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.
+
+#include "chrome/browser/chromeos/notifications/desktop_notifications_unittest.h"
+
+namespace chromeos {
+
+// static
+std::string DesktopNotificationsTest::log_output_;
+
+class MockNotificationUI : public BalloonCollectionImpl::NotificationUI {
+ public:
+ virtual void Add(Balloon* balloon) {}
+ virtual bool Update(Balloon* balloon) { return false; }
+ virtual void Remove(Balloon* balloon) {}
+ virtual void Show(Balloon* balloon) {}
+ virtual void ResizeNotification(Balloon* balloon,
+ const gfx::Size& size) {}
+ virtual void SetActiveView(BalloonViewImpl* view) {}
+};
+
+MockBalloonCollection::MockBalloonCollection()
+ : log_proxy_(new LoggingNotificationProxy()) {
+ set_notification_ui(new MockNotificationUI());
+}
+
+void MockBalloonCollection::Add(const Notification& notification,
+ Profile* profile) {
+ // Swap in the logging proxy for the purpose of logging calls that
+ // would be made into javascript, then pass this down to the
+ // balloon collection.
+ Notification test_notification(notification.origin_url(),
+ notification.content_url(),
+ notification.display_source(),
+ string16(), /* replace_id */
+ log_proxy_.get());
+ BalloonCollectionImpl::Add(test_notification, profile);
+}
+
+bool MockBalloonCollection::Remove(const Notification& notification) {
+ Notification test_notification(notification.origin_url(),
+ notification.content_url(),
+ notification.display_source(),
+ string16(), /* replace_id */
+ log_proxy_.get());
+ return BalloonCollectionImpl::Remove(test_notification);
+}
+
+Balloon* MockBalloonCollection::MakeBalloon(const Notification& notification,
+ Profile* profile) {
+ // Start with a normal balloon but mock out the view.
+ Balloon* balloon = BalloonCollectionImpl::MakeBalloon(notification, profile);
+ balloon->set_view(new MockBalloonView(balloon));
+ balloons_.insert(balloon);
+ return balloon;
+}
+
+void MockBalloonCollection::OnBalloonClosed(Balloon* source) {
+ balloons_.erase(source);
+ BalloonCollectionImpl::OnBalloonClosed(source);
+}
+
+int MockBalloonCollection::UppermostVerticalPosition() {
+ int min = 0;
+ std::set<Balloon*>::iterator iter;
+ for (iter = balloons_.begin(); iter != balloons_.end(); ++iter) {
+ int pos = (*iter)->GetPosition().y();
+ if (iter == balloons_.begin() || pos < min)
+ min = pos;
+ }
+ return min;
+}
+
+DesktopNotificationsTest::DesktopNotificationsTest()
+ : ui_thread_(ChromeThread::UI, &message_loop_) {
+}
+
+DesktopNotificationsTest::~DesktopNotificationsTest() {
+}
+
+void DesktopNotificationsTest::SetUp() {
+ profile_.reset(new TestingProfile());
+ balloon_collection_ = new MockBalloonCollection();
+ ui_manager_.reset(new NotificationUIManager());
+ ui_manager_->Initialize(balloon_collection_);
+ balloon_collection_->set_space_change_listener(ui_manager_.get());
+ service_.reset(new DesktopNotificationService(profile(), ui_manager_.get()));
+ log_output_.clear();
+}
+
+void DesktopNotificationsTest::TearDown() {
+ service_.reset(NULL);
+ profile_.reset(NULL);
+ ui_manager_.reset(NULL);
+}
+
+ViewHostMsg_ShowNotification_Params
+DesktopNotificationsTest::StandardTestNotification() {
+ ViewHostMsg_ShowNotification_Params params;
+ params.notification_id = 0;
+ params.origin = GURL("http://www.google.com");
+ params.is_html = false;
+ params.icon_url = GURL("/icon.png");
+ params.title = ASCIIToUTF16("Title");
+ params.body = ASCIIToUTF16("Text");
+ params.direction = WebKit::WebTextDirectionDefault;
+ return params;
+}
+
+TEST_F(DesktopNotificationsTest, TestShow) {
+ ViewHostMsg_ShowNotification_Params params = StandardTestNotification();
+ params.notification_id = 1;
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ params, 0, 0, DesktopNotificationService::PageNotification));
+
+ MessageLoopForUI::current()->RunAllPending();
+ EXPECT_EQ(1, balloon_collection_->count());
+
+ ViewHostMsg_ShowNotification_Params params2;
+ params2.origin = GURL("http://www.google.com");
+ params2.is_html = true;
+ params2.contents_url = GURL("http://www.google.com/notification.html");
+ params2.notification_id = 2;
+
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ params2, 0, 0, DesktopNotificationService::PageNotification));
+ MessageLoopForUI::current()->RunAllPending();
+ EXPECT_EQ(2, balloon_collection_->count());
+
+ EXPECT_EQ("notification displayed\n"
+ "notification displayed\n",
+ log_output_);
+}
+
+TEST_F(DesktopNotificationsTest, TestClose) {
+ ViewHostMsg_ShowNotification_Params params = StandardTestNotification();
+ params.notification_id = 1;
+
+ // Request a notification; should open a balloon.
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ params, 0, 0, DesktopNotificationService::PageNotification));
+ MessageLoopForUI::current()->RunAllPending();
+ EXPECT_EQ(1, balloon_collection_->count());
+
+ // Close all the open balloons.
+ std::set<Balloon*> balloons = balloon_collection_->balloons();
+ std::set<Balloon*>::iterator iter;
+ for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
+ (*iter)->OnClose(true);
+ }
+
+ // Verify that the balloon collection is now empty.
+ EXPECT_EQ(0, balloon_collection_->count());
+
+ EXPECT_EQ("notification displayed\n"
+ "notification closed by user\n",
+ log_output_);
+}
+
+TEST_F(DesktopNotificationsTest, TestCancel) {
+ int process_id = 0;
+ int route_id = 0;
+ int notification_id = 1;
+
+ ViewHostMsg_ShowNotification_Params params = StandardTestNotification();
+ params.notification_id = notification_id;
+
+ // Request a notification; should open a balloon.
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ params, process_id, route_id,
+ DesktopNotificationService::PageNotification));
+ MessageLoopForUI::current()->RunAllPending();
+ EXPECT_EQ(1, balloon_collection_->count());
+
+ // Cancel the same notification
+ service_->CancelDesktopNotification(process_id,
+ route_id,
+ notification_id);
+ MessageLoopForUI::current()->RunAllPending();
+ // Verify that the balloon collection is now empty.
+ EXPECT_EQ(0, balloon_collection_->count());
+
+ EXPECT_EQ("notification displayed\n"
+ "notification closed by script\n",
+ log_output_);
+}
+
+TEST_F(DesktopNotificationsTest, TestManyNotifications) {
+ int process_id = 0;
+ int route_id = 0;
+
+ // Request lots of identical notifications.
+ const int kLotsOfToasts = 20;
+ for (int id = 1; id <= kLotsOfToasts; ++id) {
+ SCOPED_TRACE(StringPrintf("Creation loop: id=%d", id));
+ ViewHostMsg_ShowNotification_Params params = StandardTestNotification();
+ params.notification_id = id;
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ params, process_id, route_id,
+ DesktopNotificationService::PageNotification));
+ }
+ MessageLoopForUI::current()->RunAllPending();
+
+ // Build up an expected log of what should be happening.
+ std::string expected_log;
+ for (int i = 0; i < kLotsOfToasts; ++i) {
+ expected_log.append("notification displayed\n");
+ }
+
+ EXPECT_EQ(kLotsOfToasts, balloon_collection_->count());
+ EXPECT_EQ(expected_log, log_output_);
+
+ // Cancel half of the notifications from the start
+ int id;
+ int cancelled = kLotsOfToasts / 2;
+ for (id = 1;
+ id <= cancelled;
+ ++id) {
+ SCOPED_TRACE(StringPrintf("Cancel half of notifications: id=%d", id));
+ service_->CancelDesktopNotification(process_id, route_id, id);
+ MessageLoopForUI::current()->RunAllPending();
+ expected_log.append("notification closed by script\n");
+ EXPECT_EQ(kLotsOfToasts - id,
+ balloon_collection_->count());
+ EXPECT_EQ(expected_log, log_output_);
+ }
+
+ // Now cancel the rest. It should empty the balloon space.
+ for (; id <= kLotsOfToasts; ++id) {
+ SCOPED_TRACE(StringPrintf("Cancel loop: id=%d", id));
+ service_->CancelDesktopNotification(process_id, route_id, id);
+ expected_log.append("notification closed by script\n");
+ MessageLoopForUI::current()->RunAllPending();
+ EXPECT_EQ(expected_log, log_output_);
+ }
+
+ // Verify that the balloon collection is now empty.
+ EXPECT_EQ(0, balloon_collection_->count());
+}
+
+TEST_F(DesktopNotificationsTest, TestEarlyDestruction) {
+ // Create some toasts and then prematurely delete the notification service,
+ // just to make sure nothing crashes/leaks.
+ for (int id = 0; id <= 3; ++id) {
+ SCOPED_TRACE(StringPrintf("Show Text loop: id=%d", id));
+
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ StandardTestNotification(), 0, 0,
+ DesktopNotificationService::PageNotification));
+ }
+ service_.reset(NULL);
+}
+
+TEST_F(DesktopNotificationsTest, TestUserInputEscaping) {
+ // Create a test script with some HTML; assert that it doesn't get into the
+ // data:// URL that's produced for the balloon.
+ ViewHostMsg_ShowNotification_Params params = StandardTestNotification();
+ params.title = ASCIIToUTF16("<script>window.alert('uh oh');</script>");
+ params.body = ASCIIToUTF16("<i>this text is in italics</i>");
+ params.notification_id = 1;
+ EXPECT_TRUE(service_->ShowDesktopNotification(
+ params, 0, 0, DesktopNotificationService::PageNotification));
+
+ MessageLoopForUI::current()->RunAllPending();
+ EXPECT_EQ(1, balloon_collection_->count());
+ Balloon* balloon = (*balloon_collection_->balloons().begin());
+ GURL data_url = balloon->notification().content_url();
+ EXPECT_EQ(std::string::npos, data_url.spec().find("<script>"));
+ EXPECT_EQ(std::string::npos, data_url.spec().find("<i>"));
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/notifications/desktop_notifications_unittest.h b/chrome/browser/chromeos/notifications/desktop_notifications_unittest.h
new file mode 100644
index 0000000..b424c67
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/desktop_notifications_unittest.h
@@ -0,0 +1,113 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_DESKTOP_NOTIFICATIONS_UNITTEST_H_
+#define CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_DESKTOP_NOTIFICATIONS_UNITTEST_H_
+
+#include <set>
+#include <string>
+
+#include "base/message_loop.h"
+#include "base/string_util.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/chrome_thread.h"
+#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
+#include "chrome/browser/notifications/balloon.h"
+#include "chrome/browser/notifications/desktop_notification_service.h"
+#include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/notifications/notification_object_proxy.h"
+#include "chrome/browser/notifications/notification_test_util.h"
+#include "chrome/browser/notifications/notification_ui_manager.h"
+#include "chrome/browser/notifications/notifications_prefs_cache.h"
+#include "chrome/common/render_messages.h"
+#include "chrome/test/testing_profile.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace chromeos {
+
+class DesktopNotificationsTest;
+typedef LoggingNotificationProxyBase<DesktopNotificationsTest>
+ LoggingNotificationProxy;
+
+// Test version of the balloon collection which counts the number
+// of notifications that are added to it.
+class MockBalloonCollection : public BalloonCollectionImpl {
+ public:
+ MockBalloonCollection();
+
+ // BalloonCollectionImpl overrides
+ virtual void Add(const Notification& notification,
+ Profile* profile);
+ virtual bool Remove(const Notification& notification);
+ virtual Balloon* MakeBalloon(const Notification& notification,
+ Profile* profile);
+ virtual void OnBalloonClosed(Balloon* source);
+
+ // Number of balloons being shown.
+ std::set<Balloon*>& balloons() { return balloons_; }
+ int count() const { return balloons_.size(); }
+
+ // Returns the highest y-coordinate of all the balloons in the collection.
+ int UppermostVerticalPosition();
+
+ private:
+ std::set<Balloon*> balloons_;
+ scoped_refptr<LoggingNotificationProxy> log_proxy_;
+};
+
+class DesktopNotificationsTest : public testing::Test {
+ public:
+ DesktopNotificationsTest();
+ ~DesktopNotificationsTest();
+
+ static void log(const std::string& message) {
+ log_output_.append(message);
+ }
+
+ Profile* profile() { return profile_.get(); }
+
+ protected:
+ // testing::Test overrides
+ virtual void SetUp();
+ virtual void TearDown();
+
+ void AllowOrigin(const GURL& origin) {
+ service_->GrantPermission(origin);
+ }
+
+ void DenyOrigin(const GURL& origin) {
+ service_->DenyPermission(origin);
+ }
+
+ int HasPermission(const GURL& origin) {
+ return service_->prefs_cache()->HasPermission(origin);
+ }
+
+ // Constructs a notification parameter structure for use in tests.
+ ViewHostMsg_ShowNotification_Params StandardTestNotification();
+
+ // Create a message loop to allow notifications code to post tasks,
+ // and a thread so that notifications code runs on the expected thread.
+ MessageLoopForUI message_loop_;
+ ChromeThread ui_thread_;
+
+ // Test profile.
+ scoped_ptr<TestingProfile> profile_;
+
+ // Mock balloon collection -- owned by the NotificationUIManager
+ MockBalloonCollection* balloon_collection_;
+
+ // Real UI manager.
+ scoped_ptr<NotificationUIManager> ui_manager_;
+
+ // Real DesktopNotificationService
+ scoped_ptr<DesktopNotificationService> service_;
+
+ // Contains the cumulative output of the unit test.
+ static std::string log_output_;
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_DESKTOP_NOTIFICATIONS_UNITTEST_H_
diff --git a/chrome/browser/chromeos/notifications/notification_browsertest.cc b/chrome/browser/chromeos/notifications/notification_browsertest.cc
new file mode 100644
index 0000000..a1e1152
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/notification_browsertest.cc
@@ -0,0 +1,549 @@
+// Copyright (c) 2010 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.
+
+#include "app/x11_util.h"
+#include "base/message_loop.h"
+#include "base/ref_counted.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
+#include "chrome/browser/chromeos/notifications/balloon_view.h"
+#include "chrome/browser/chromeos/notifications/notification_panel.h"
+#include "chrome/browser/chromeos/notifications/system_notification_factory.h"
+#include "chrome/browser/notifications/notification_delegate.h"
+#include "chrome/browser/notifications/notification_ui_manager.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/test/in_process_browser_test.h"
+#include "chrome/test/ui_test_utils.h"
+
+namespace {
+
+class MockNotificationDelegate : public NotificationDelegate {
+ public:
+ explicit MockNotificationDelegate(const std::string& id) : id_(id) {}
+
+ virtual void Display() {}
+ virtual void Error() {}
+ virtual void Close(bool by_user) {}
+ virtual std::string id() const { return id_; }
+
+ private:
+ std::string id_;
+
+ DISALLOW_COPY_AND_ASSIGN(MockNotificationDelegate);
+};
+
+// The name of ChromeOS's window manager.
+const char* kChromeOsWindowManagerName = "chromeos-wm";
+
+} // namespace
+
+namespace chromeos {
+
+class NotificationTest : public InProcessBrowserTest,
+ public NotificationObserver {
+ public:
+ NotificationTest()
+ : under_chromeos_(false),
+ state_(PanelController::INITIAL),
+ expected_(PanelController::INITIAL) {
+ }
+
+ protected:
+ virtual void SetUp() {
+ // Detect if we're running under ChromeOS WindowManager. See
+ // the description for "under_chromeos_" below for why we need this.
+ std::string wm_name;
+ bool wm_name_valid = x11_util::GetWindowManagerName(&wm_name);
+ // NOTE: On Chrome OS the wm and Chrome are started in parallel. This
+ // means it's possible for us not to be able to get the name of the window
+ // manager. We assume that when this happens we're on Chrome OS.
+ under_chromeos_ = (!wm_name_valid ||
+ wm_name == kChromeOsWindowManagerName);
+ InProcessBrowserTest::SetUp();
+ }
+
+ BalloonCollectionImpl* GetBalloonCollectionImpl() {
+ return static_cast<BalloonCollectionImpl*>(
+ g_browser_process->notification_ui_manager()->balloon_collection());
+ }
+
+ NotificationPanel* GetNotificationPanel() {
+ return static_cast<NotificationPanel*>(
+ GetBalloonCollectionImpl()->notification_ui());
+ }
+
+ Notification NewMockNotification(const std::string& id) {
+ return NewMockNotification(new MockNotificationDelegate(id));
+ }
+
+ Notification NewMockNotification(NotificationDelegate* delegate) {
+ std::string text = delegate->id();
+ return SystemNotificationFactory::Create(
+ GURL(), ASCIIToUTF16(text.c_str()), ASCIIToUTF16(text.c_str()),
+ delegate);
+ }
+
+ void MarkStale(const char* id) {
+ GetNotificationPanel()->GetTester()->MarkStale(NewMockNotification(id));
+ }
+
+ // Waits untilt the panel's state becomes the specified state.
+ // Does nothing if it's not running with ChromeOS Window Manager.
+ void WaitForPanelState(NotificationPanelTester* tester,
+ PanelController::State state) {
+ if (under_chromeos_ && state != state_) {
+ expected_ = state;
+ ui_test_utils::RunAllPendingInMessageLoop();
+ }
+ }
+
+ // Busy loop to wait until the webkit give some size to the notification.
+ void WaitForResize(BalloonViewImpl* view) {
+ while (view->bounds().IsEmpty()) {
+ ui_test_utils::RunAllPendingInMessageLoop();
+ }
+ }
+
+ // NotificationObserver overrides.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ ASSERT_TRUE(NotificationType::PANEL_STATE_CHANGED == type);
+ PanelController::State* state =
+ reinterpret_cast<PanelController::State*>(details.map_key());
+ state_ = *state;
+ if (under_chromeos_ && expected_ == state_) {
+ expected_ = PanelController::INITIAL;
+ MessageLoop::current()->Quit();
+ }
+ }
+
+ private:
+ // ChromeOS build of chrome communicates with ChromeOS's
+ // WindowManager, and behaves differently if it runs under a
+ // chromeos window manager. ChromeOS WindowManager sends
+ // EXPANDED/MINIMIED state change message when the panels's state
+ // changed (regardless of who changed it), and to avoid
+ // mis-recognizing such events as user-initiated actions, we need to
+ // wait and eat them before moving to a next step.
+ bool under_chromeos_;
+ PanelController::State state_;
+ PanelController::State expected_;
+};
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestBasic) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+
+ // Using system notification as regular notification.
+ collection->Add(NewMockNotification("1"), browser()->profile());
+
+ EXPECT_EQ(1, tester->GetNewNotificationCount());
+ EXPECT_EQ(1, tester->GetNotificationCount());
+ EXPECT_EQ(0, tester->GetStickyNotificationCount());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ collection->Add(NewMockNotification("2"), browser()->profile());
+
+ EXPECT_EQ(2, tester->GetNewNotificationCount());
+ EXPECT_EQ(2, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ collection->Remove(NewMockNotification("1"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+
+ EXPECT_EQ(1, tester->GetNewNotificationCount());
+ EXPECT_EQ(1, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ collection->Remove(NewMockNotification("2"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(0, tester->GetNewNotificationCount());
+ EXPECT_EQ(0, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ // CLOSE is asynchronous. Run the all pending tasks to finish closing
+ // task.
+ ui_test_utils::RunAllPendingInMessageLoop();
+}
+
+// [CLOSED] -add->[STICKY_AND_NEW] -mouse-> [KEEP_SIZE] -remove/add->
+// [KEEP_SIZE] -remove-> [CLOSED] -add-> [STICKY_AND_NEW] -remove-> [CLOSED]
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestKeepSizeState) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ // Using system notification as regular notification.
+ collection->Add(NewMockNotification("1"), browser()->profile());
+ collection->Add(NewMockNotification("2"), browser()->profile());
+
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ panel->OnMouseMotion(gfx::Point(10, 10));
+ EXPECT_EQ(NotificationPanel::KEEP_SIZE, tester->state());
+
+ collection->Remove(NewMockNotification("1"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(1, tester->GetNewNotificationCount());
+ EXPECT_EQ(1, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::KEEP_SIZE, tester->state());
+
+ collection->Add(NewMockNotification("1"), browser()->profile());
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(2, tester->GetNewNotificationCount());
+ EXPECT_EQ(2, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::KEEP_SIZE, tester->state());
+
+ collection->Remove(NewMockNotification("1"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(1, tester->GetNewNotificationCount());
+ EXPECT_EQ(1, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::KEEP_SIZE, tester->state());
+
+ collection->Remove(NewMockNotification("2"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(0, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ collection->Add(NewMockNotification("3"), browser()->profile());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ collection->Remove(NewMockNotification("3"));
+
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(0, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+}
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestSystemNotification) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ scoped_refptr<MockNotificationDelegate> delegate(
+ new MockNotificationDelegate("power"));
+ NotificationPanelTester* tester = panel->GetTester();
+
+ Notification notify = NewMockNotification(delegate.get());
+ collection->AddSystemNotification(notify, browser()->profile(), true, false);
+
+ EXPECT_EQ(1, tester->GetNewNotificationCount());
+ EXPECT_EQ(1, tester->GetStickyNotificationCount());
+
+ Notification update = SystemNotificationFactory::Create(
+ GURL(), ASCIIToUTF16("Title"), ASCIIToUTF16("updated"), delegate.get());
+ collection->UpdateNotification(update);
+
+ EXPECT_EQ(1, tester->GetStickyNotificationCount());
+
+ Notification update_and_show = SystemNotificationFactory::Create(
+ GURL(), ASCIIToUTF16("Title"), ASCIIToUTF16("updated and shown"),
+ delegate.get());
+ collection->UpdateAndShowNotification(update_and_show);
+
+ EXPECT_EQ(1, tester->GetStickyNotificationCount());
+
+ // Dismiss the notification.
+ // TODO(oshima): Consider updating API to Remove(NotificationDelegate)
+ // or Remove(std::string id);
+ collection->Remove(Notification(GURL(), GURL(), std::wstring(), string16(),
+ delegate.get()));
+ ui_test_utils::RunAllPendingInMessageLoop();
+
+ EXPECT_EQ(0, tester->GetStickyNotificationCount());
+ EXPECT_EQ(0, tester->GetNewNotificationCount());
+ // TODO(oshima): check content, etc..
+}
+
+// [CLOSED] -add,add->[STICKY_AND_NEW] -stale-> [MINIMIZED] -remove->
+// [MINIMIZED] -remove-> [CLOSED]
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestStateTransition1) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+
+ tester->SetStaleTimeout(0);
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ collection->Add(NewMockNotification("1"), browser()->profile());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ collection->Add(NewMockNotification("2"), browser()->profile());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::MINIMIZED, tester->state());
+
+ collection->Remove(NewMockNotification("2"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::MINIMIZED, tester->state());
+
+ collection->Remove(NewMockNotification("1"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(0, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ ui_test_utils::RunAllPendingInMessageLoop();
+}
+
+// [CLOSED] -add->[STICKY_AND_NEW] -stale-> [MINIMIZED] -add->
+// [STICKY_AND_NEW] -stale-> [MINIMIZED] -add sys-> [STICKY_NEW]
+// -stale-> [STICKY_NEW] -remove-> [STICKY_NEW] -remove sys->
+// [MINIMIZED] -remove-> [CLOSED]
+//
+// This test depends on the fact that the panel state change occurs
+// quicker than stale timeout, thus the stale timeout cannot be set to
+// 0. This test explicitly controls the stale state instead.
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestStateTransition2) {
+ // Register observer here as the registration does not work in SetUp().
+ NotificationRegistrar registrar;
+ registrar.Add(this,
+ NotificationType::PANEL_STATE_CHANGED,
+ NotificationService::AllSources());
+
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+
+ // See description above.
+ tester->SetStaleTimeout(100000);
+
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ collection->Add(NewMockNotification("1"), browser()->profile());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ ui_test_utils::RunAllPendingInMessageLoop();
+
+ // Make the notification stale and make sure panel is minimized state.
+ MarkStale("1");
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::MINIMIZED, tester->state());
+ WaitForPanelState(tester, PanelController::MINIMIZED);
+
+ // Adding new notification expands the panel.
+ collection->Add(NewMockNotification("2"), browser()->profile());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ WaitForPanelState(tester, PanelController::EXPANDED);
+
+ // The panel must be minimzied when the new notification becomes stale.
+ MarkStale("2");
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::MINIMIZED, tester->state());
+ WaitForPanelState(tester, PanelController::MINIMIZED);
+
+ // The panel must be expanded again when a new system notification is added.
+ collection->AddSystemNotification(
+ NewMockNotification("3"), browser()->profile(), true, false);
+ EXPECT_EQ(3, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ WaitForPanelState(tester, PanelController::EXPANDED);
+
+ // Running all events nor removing non sticky should not change the state.
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ collection->Remove(NewMockNotification("1"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ // Removing the system notification should minimize the panel.
+ collection->Remove(NewMockNotification("3"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(1, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::MINIMIZED, tester->state());
+ WaitForPanelState(tester, PanelController::MINIMIZED);
+
+ // Removing the last notification. Should close the panel.
+
+ collection->Remove(NewMockNotification("2"));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(0, tester->GetNotificationCount());
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+
+ ui_test_utils::RunAllPendingInMessageLoop();
+}
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestCleanupOnExit) {
+ NotificationRegistrar registrar;
+ registrar.Add(this,
+ NotificationType::PANEL_STATE_CHANGED,
+ NotificationService::AllSources());
+
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+
+ // Don't become stale.
+ tester->SetStaleTimeout(100000);
+
+ collection->Add(NewMockNotification("1"), browser()->profile());
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ WaitForPanelState(tester, PanelController::EXPANDED);
+ // end without closing.
+}
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestCloseOpen) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+ Profile* profile = browser()->profile();
+
+ collection->Add(NewMockNotification("1"), profile);
+ collection->Add(NewMockNotification("2"), profile);
+ ui_test_utils::RunAllPendingInMessageLoop();
+ WaitForPanelState(tester, PanelController::EXPANDED);
+ PanelController* controller = tester->GetPanelController();
+ // close now
+ panel->ClosePanel();
+ controller->Close();
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+ // open again
+ collection->Add(NewMockNotification("3"), profile);
+ WaitForPanelState(tester, PanelController::EXPANDED);
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+
+ // close again
+ controller = tester->GetPanelController();
+ panel->ClosePanel();
+ controller->Close();
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::CLOSED, tester->state());
+}
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestScrollBalloonToVisible) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+ Profile* profile = browser()->profile();
+
+ // Create notifications enough to overflow the panel size.
+ const int create_count = 15;
+
+ // new notification is always visible
+ for (int i = 0; i < create_count; i++) {
+ {
+ SCOPED_TRACE(StringPrintf("new n%d", i));
+ std::string id = StringPrintf("n%d", i);
+ collection->Add(NewMockNotification(id), profile);
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ BalloonViewImpl* view =
+ tester->GetBalloonView(collection, NewMockNotification(id));
+ WaitForResize(view);
+ EXPECT_TRUE(tester->IsVisible(view));
+ }
+ {
+ SCOPED_TRACE(StringPrintf("new s%d", i));
+ std::string id = StringPrintf("s%d", i);
+ collection->AddSystemNotification(
+ NewMockNotification(id), browser()->profile(), true, false);
+ ui_test_utils::RunAllPendingInMessageLoop();
+ BalloonViewImpl* view =
+ tester->GetBalloonView(collection, NewMockNotification(id));
+ WaitForResize(view);
+ EXPECT_TRUE(tester->IsVisible(view));
+ }
+ }
+ // Update should not change the visibility
+ for (int i = 0; i < create_count; i++) {
+ {
+ SCOPED_TRACE(StringPrintf("update n%d", i));
+ Notification notify = NewMockNotification(StringPrintf("n%d", i));
+ // The last shown notification is sticky, which makes all non sticky
+ // invisible.
+ EXPECT_TRUE(collection->UpdateNotification(notify));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ BalloonViewImpl* view = tester->GetBalloonView(collection, notify);
+ EXPECT_FALSE(tester->IsVisible(view));
+ }
+ {
+ SCOPED_TRACE(StringPrintf("update s%d", i));
+ Notification notify = NewMockNotification(StringPrintf("s%d", i));
+ BalloonViewImpl* view = tester->GetBalloonView(collection, notify);
+ bool currently_visible = tester->IsVisible(view);
+ EXPECT_TRUE(collection->UpdateNotification(notify));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(view, tester->GetBalloonView(collection, notify));
+ EXPECT_EQ(currently_visible, tester->IsVisible(view));
+ }
+ }
+ // UpdateAndShowNotification makes notification visible
+ for (int i = 0; i < create_count; i++) {
+ {
+ SCOPED_TRACE(StringPrintf("update and show n%d", i));
+ Notification notify = NewMockNotification(StringPrintf("n%d", i));
+ EXPECT_TRUE(collection->UpdateAndShowNotification(notify));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ BalloonViewImpl* view = tester->GetBalloonView(collection, notify);
+ EXPECT_TRUE(tester->IsVisible(view));
+ }
+ {
+ SCOPED_TRACE(StringPrintf("update and show s%d", i));
+ Notification notify = NewMockNotification(StringPrintf("s%d", i));
+ EXPECT_TRUE(collection->UpdateAndShowNotification(notify));
+ ui_test_utils::RunAllPendingInMessageLoop();
+ BalloonViewImpl* view = tester->GetBalloonView(collection, notify);
+ EXPECT_TRUE(tester->IsVisible(view));
+ }
+ }
+}
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestActivateDeactivate) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+ Profile* profile = browser()->profile();
+
+ collection->Add(NewMockNotification("1"), profile);
+ collection->AddSystemNotification(
+ NewMockNotification("2"), profile, true, false);
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ BalloonViewImpl* view1 =
+ tester->GetBalloonView(collection, NewMockNotification("1"));
+ BalloonViewImpl* view2 =
+ tester->GetBalloonView(collection, NewMockNotification("2"));
+ // Wait until all renderers get size.
+ WaitForResize(view1);
+ WaitForResize(view2);
+
+ panel->OnMouseMotion(gfx::Point(10, 50));
+ EXPECT_TRUE(tester->IsActive(view1));
+ EXPECT_FALSE(tester->IsActive(view2));
+
+ panel->OnMouseMotion(gfx::Point(10, 10));
+ EXPECT_FALSE(tester->IsActive(view1));
+ EXPECT_TRUE(tester->IsActive(view2));
+
+ panel->OnMouseMotion(gfx::Point(500, 500));
+ EXPECT_FALSE(tester->IsActive(view1));
+ EXPECT_FALSE(tester->IsActive(view2));
+}
+
+IN_PROC_BROWSER_TEST_F(NotificationTest, TestCloseDismissAllNonSticky) {
+ BalloonCollectionImpl* collection = GetBalloonCollectionImpl();
+ NotificationPanel* panel = GetNotificationPanel();
+ NotificationPanelTester* tester = panel->GetTester();
+ Profile* profile = browser()->profile();
+
+ collection->Add(NewMockNotification("1"), profile);
+ collection->AddSystemNotification(
+ NewMockNotification("2"), profile, true, false);
+ collection->Add(NewMockNotification("3"), profile);
+
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(NotificationPanel::STICKY_AND_NEW, tester->state());
+ EXPECT_EQ(3, tester->GetNotificationCount());
+ EXPECT_EQ(1, tester->GetStickyNotificationCount());
+
+ // Hide
+ panel->Hide();
+ ui_test_utils::RunAllPendingInMessageLoop();
+ EXPECT_EQ(1, tester->GetNotificationCount());
+ EXPECT_EQ(1, tester->GetStickyNotificationCount());
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/notifications/notification_panel.cc b/chrome/browser/chromeos/notifications/notification_panel.cc
new file mode 100644
index 0000000..0f53f9f
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/notification_panel.cc
@@ -0,0 +1,871 @@
+// Copyright (c) 2010 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 "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
+#include "chrome/browser/chromeos/notifications/balloon_view.h"
+#include "gfx/canvas.h"
+#include "grit/generated_resources.h"
+#include "third_party/cros/chromeos_wm_ipc_enums.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 to preevnt recursive calls to PaintNow, which is observed
+// with gtk 2.18.6. See http://crbug.com/42235 for more details.
+class PanelWidget : public views::WidgetGtk {
+ public:
+ PanelWidget() : WidgetGtk(TYPE_WINDOW), painting_(false) {
+ }
+
+ virtual ~PanelWidget() {
+ // Enable double buffering because the panel has both pure views control and
+ // native controls (scroll bar).
+ EnableDoubleBuffer(true);
+ }
+
+ // views::WidgetGtk overrides.
+ virtual void PaintNow(const gfx::Rect& update_rect) {
+ if (!painting_) {
+ painting_ = true;
+ WidgetGtk::PaintNow(update_rect);
+ painting_ = false;
+ }
+ }
+
+ private:
+ // True if the painting is in progress.
+ bool painting_;
+
+ DISALLOW_COPY_AND_ASSIGN(PanelWidget);
+};
+
+// 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);
+
+ int x = 0, y = 0;
+ GetContainedWidgetEventCoordinates(event, &x, &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;
+ GetBounds(&bounds, true);
+ 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 = GetChildViewCount() - 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 = GetChildViewCount() - 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 = GetChildViewCount() - 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 = GetChildViewCount() - 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 = GetChildViewCount() - 1; i >= 0; --i) {
+ BalloonViewImpl* view =
+ static_cast<BalloonViewImpl*>(GetChildViewAt(i));
+ view->set_stale();
+ }
+ }
+
+ void DismissAll() {
+ for (int i = GetChildViewCount() - 1; i >= 0; --i) {
+ BalloonViewImpl* view =
+ static_cast<BalloonViewImpl*>(GetChildViewAt(i));
+ view->Close(true);
+ }
+ }
+
+ BalloonViewImpl* FindBalloonView(const Notification& notification) {
+ for (int i = GetChildViewCount() - 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 = GetChildViewCount() - 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:
+ 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_->GetChildViewCount() != 0 &&
+ non_sticky_container_->GetChildViewCount() != 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 (sticky_container_->HasChildView(view)) {
+ container = sticky_container_;
+ } else if (non_sticky_container_->HasChildView(view)) {
+ 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_->GetChildViewCount() +
+ non_sticky_container_->GetChildViewCount();
+ }
+
+ // 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_->GetChildViewCount() +
+ non_sticky_container_->GetNewCount();
+ }
+
+ // Returns the # of sticky notifications.
+ int GetStickyNotificationCount() {
+ return sticky_container_->GetChildViewCount();
+ }
+
+ // Returns true if the |view| is contained in the panel.
+ bool HasBalloonView(View* view) {
+ return sticky_container_->HasChildView(view) ||
+ non_sticky_container_->HasChildView(view);
+ }
+
+ // 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.
+ panel_widget_ = new PanelWidget();
+ 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();
+}
+
+void NotificationPanel::ClosePanel() {
+ SET_STATE(CLOSED);
+ UpdatePanel(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 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_->GetBounds(&panel_bounds, true);
+ 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->SetBounds(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)
+ DLOG(INFO) << "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) {
+ BalloonCollectionImpl::Balloons::iterator iter =
+ collection->FindBalloon(notification);
+ DCHECK(iter != collection->balloons_.end());
+ Balloon* balloon = (*iter);
+ 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->bounds().size()));
+}
+
+
+bool NotificationPanelTester::IsActive(const BalloonViewImpl* view) const {
+ return panel_->active_ == view;
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/notifications/notification_panel.h b/chrome/browser/chromeos/notifications/notification_panel.h
new file mode 100644
index 0000000..7b795e4
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/notification_panel.h
@@ -0,0 +1,245 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_NOTIFICATION_PANEL_H_
+#define CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_NOTIFICATION_PANEL_H_
+
+#include "base/task.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/chromeos/frame/panel_controller.h"
+#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
+#include "chrome/common/notification_registrar.h"
+#include "gfx/rect.h"
+
+class Balloon;
+class Notification;
+
+namespace views {
+class ScrollView;
+} // namespace views
+
+namespace chromeos {
+
+class BalloonContainer;
+class BalloonViewImpl;
+class NotificationPanelTester;
+
+// NotificationPanel is a panel that displays notifications. It has
+// several states and displays the different portion of notifications
+// depending on in which state the panel is. The following shows
+// how the panel's state changes in response to various events.
+//
+// TODO(oshima): add remove event and fix state transition graph below.
+// Event List:
+// close: a user pressed close button on the title bar,
+// or the system closed the panel.
+// new : a new notification is added.
+// stale: one of new notifications became stale.
+// expand: a user pressed minimized panel to expand.
+// minimize: a user pressed the panel's title bar to minimize.
+// user: the user's mouse moved over the panel, indicates
+// that user is trying to interact with the panel.
+// For state, see State enum's description below.
+//
+//
+// [CLOSE]<-(event=close)-+ +--(event=stale, cond=has new|sticky)
+// | | | (event=new)
+// | | V |
+// +--(event=new)-->[STICKY_AND_NEW]----- +--------(event=user)
+// | ^ | |
+// | | (event=stale, V
+// | | cond=has new, no sticy) +[ KEEP_SIZE ]<-+
+// | (event=new) (event=minimize) | | |
+// | | | | | |
+// | | | (event=minimize)(event=close)|
+// | | +---------------+ | |
+// | | V V |
+// | [ MINIMIZED ]---(event=close)--> [CLOSE] |
+// | | ^ |
+// | | | |
+// | (event=expand) (event=minmize) (event=user)
+// | V | |
+// +--(event=open)---->[ FULL ]-------------+-------------------+
+// | ^ |
+// (event=close) +-------(event=stale)(event=new)
+// |
+// [CLOSE] <------+
+//
+class NotificationPanel : public PanelController::Delegate,
+ public BalloonCollectionImpl::NotificationUI,
+ public NotificationObserver {
+ public:
+ enum State {
+ FULL, // Show all notifications
+ KEEP_SIZE, // Don't change the size.
+ STICKY_AND_NEW, // Show only new and sticky notifications.
+ MINIMIZED, // The panel is minimized.
+ CLOSED, // The panel is closed.
+ };
+
+ NotificationPanel();
+ virtual ~NotificationPanel();
+
+ // Shows/Hides the Panel.
+ void Show();
+ void Hide();
+
+ // BalloonCollectionImpl::NotificationUI overrides..
+ virtual void Add(Balloon* balloon);
+ virtual bool Update(Balloon* balloon);
+ virtual void Remove(Balloon* balloon);
+ virtual void Show(Balloon* balloon);
+ virtual void ResizeNotification(Balloon* balloon,
+ const gfx::Size& size);
+ virtual void SetActiveView(BalloonViewImpl* view);
+
+ // PanelController overrides.
+ virtual string16 GetPanelTitle();
+ virtual SkBitmap GetPanelIcon();
+ virtual void ClosePanel();
+
+ // NotificationObserver overrides:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Called when a mouse left the panel window.
+ void OnMouseLeave();
+ void OnMouseMotion(const gfx::Point& point);
+
+ NotificationPanelTester* GetTester();
+
+ private:
+ friend class NotificationPanelTester;
+
+ void Init();
+
+ // Unregister the panel's state change notification.
+ void UnregisterNotification();
+
+ // Update the Panel Size according to its state.
+ void UpdatePanel(bool update_panel_size);
+
+ // Scroll the panel so that the |balloon| is visible.
+ void ScrollBalloonToVisible(Balloon* balloon);
+
+ // Update the container's bounds so that it can show all notifications.
+ void UpdateContainerBounds();
+
+ // Update the notification's control view state.
+ void UpdateControl();
+
+ // Returns the panel's preferred bounds in the screen's coordinates.
+ // The position will be controlled by window manager so
+ // the origin is always (0, 0).
+ gfx::Rect GetPreferredBounds();
+
+ // Returns the bounds that covers sticky and new notifications.
+ gfx::Rect GetStickyNewBounds();
+
+ void StartStaleTimer(Balloon* balloon);
+
+ // A callback function that is called when the notification
+ // (that the view is associated with) becomes stale after a timeout.
+ void OnStale(BalloonViewImpl* view);
+
+ // Set the state. It can also print the
+ void SetState(State, const char* method_name);
+
+ // Mark the given notification as stale.
+ void MarkStale(const Notification& notification);
+
+ // Contains all notifications. This is owned by the panel so that we can
+ // re-attach to the widget when closing and opening the panel.
+ scoped_ptr<BalloonContainer> balloon_container_;
+
+ // The notification panel's widget.
+ views::Widget* panel_widget_;
+
+ // The notification panel's widget.
+ views::Widget* container_host_;
+
+ // Panel controller for the notification panel.
+ // This is owned by the panel to compute the panel size before
+ // actually opening the panel.
+ scoped_ptr<PanelController> panel_controller_;
+
+ // A scrollable parent of the BalloonContainer.
+ scoped_ptr<views::ScrollView> scroll_view_;
+
+ // Panel's state.
+ State state_;
+
+ ScopedRunnableMethodFactory<NotificationPanel> task_factory_;
+
+ // The minimum size of a notification.
+ gfx::Rect min_bounds_;
+
+ // Stale timeout.
+ int stale_timeout_;
+
+ // A registrar to subscribe PANEL_STATE_CHANGED event.
+ NotificationRegistrar registrar_;
+
+ // The notification a mouse pointer is currently on. NULL if the mouse
+ // is out of the panel.
+ BalloonViewImpl* active_;
+
+ // A balloon that should be visible when it gets some size.
+ Balloon* scroll_to_;
+
+ // An object that provides interfacce for tests.
+ scoped_ptr<NotificationPanelTester> tester_;
+
+ DISALLOW_COPY_AND_ASSIGN(NotificationPanel);
+};
+
+class NotificationPanelTester {
+ public:
+ explicit NotificationPanelTester(NotificationPanel* panel)
+ : panel_(panel) {
+ }
+
+ NotificationPanel::State state() {
+ return panel_->state_;
+ }
+
+ // Returns number of of sticky and new notifications.
+ int GetNotificationCount() const;
+
+ // Returns number of new notifications.
+ int GetNewNotificationCount() const;
+
+ // Returns number of of sticky notifications.
+ int GetStickyNotificationCount() const;
+
+ // Sets the timeout for a notification to become stale.
+ void SetStaleTimeout(int timeout);
+
+ // Mark the given notification as stale.
+ void MarkStale(const Notification& notification);
+
+ // Returns the notification panel's PanelController.
+ PanelController* GetPanelController() const;
+
+ // Returns the BalloonView object of the notification.
+ BalloonViewImpl* GetBalloonView(BalloonCollectionImpl* collection,
+ const Notification& notification);
+
+ // True if the view is in visible in the ScrollView.
+ bool IsVisible(const BalloonViewImpl* view) const;
+
+ // True if the view is currently active.
+ bool IsActive(const BalloonViewImpl* view) const;
+
+ private:
+ NotificationPanel* panel_;
+ DISALLOW_COPY_AND_ASSIGN(NotificationPanelTester);
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_NOTIFICATION_PANEL_H_
diff --git a/chrome/browser/chromeos/notifications/system_notification.cc b/chrome/browser/chromeos/notifications/system_notification.cc
new file mode 100644
index 0000000..f2d9d4a
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/system_notification.cc
@@ -0,0 +1,70 @@
+// Copyright (c) 2010 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.
+
+#include "chrome/browser/chromeos/notifications/system_notification.h"
+
+#include "app/resource_bundle.h"
+#include "base/base64.h"
+#include "base/move.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/chromeos/notifications/system_notification_factory.h"
+#include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/notifications/notification_ui_manager.h"
+
+namespace chromeos {
+
+SystemNotification::SystemNotification(Profile* profile, std::string id,
+ int icon_resource_id, string16 title)
+ : profile_(profile),
+ collection_(static_cast<BalloonCollectionImpl*>(
+ g_browser_process->notification_ui_manager()->balloon_collection())),
+ delegate_(new Delegate(base::move(id))),
+ title_(move(title)),
+ visible_(false),
+ urgent_(false) {
+ // Load resource icon and covert to base64 encoded data url
+ scoped_refptr<RefCountedMemory> raw_icon(ResourceBundle::GetSharedInstance().
+ LoadDataResourceBytes(icon_resource_id));
+ std::string str_gurl;
+ std::copy(raw_icon->front(), raw_icon->front() + raw_icon->size(),
+ std::back_inserter(str_gurl));
+ base::Base64Encode(str_gurl, &str_gurl);
+ str_gurl.insert(0, "data:image/png;base64,");
+ GURL tmp_gurl(str_gurl);
+ icon_.Swap(&tmp_gurl);
+}
+
+SystemNotification::~SystemNotification() {
+ Hide();
+}
+
+void SystemNotification::Show(const string16& message, bool urgent) {
+ Notification notify = SystemNotificationFactory::Create(icon_,
+ title_, message, delegate_.get());
+ if (visible_) {
+ // Force showing a user hidden notification on an urgent transition.
+ if (urgent && !urgent_) {
+ collection_->UpdateAndShowNotification(notify);
+ } else {
+ collection_->UpdateNotification(notify);
+ }
+ } else {
+ collection_->AddSystemNotification(notify, profile_, true /* sticky */,
+ false /* no controls */);
+ }
+ visible_ = true;
+ urgent_ = urgent;
+}
+
+void SystemNotification::Hide() {
+ if (visible_) {
+ collection_->Remove(Notification(GURL(), GURL(), std::wstring(), string16(),
+ delegate_.get()));
+
+ visible_ = false;
+ urgent_ = false;
+ }
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/notifications/system_notification.h b/chrome/browser/chromeos/notifications/system_notification.h
new file mode 100644
index 0000000..ff1efef
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/system_notification.h
@@ -0,0 +1,78 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_SYSTEM_NOTIFICATION_H_
+#define CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_SYSTEM_NOTIFICATION_H_
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/move.h"
+#include "base/ref_counted.h"
+#include "base/string16.h"
+#include "chrome/browser/chromeos/notifications/balloon_collection_impl.h"
+#include "chrome/browser/notifications/notification_delegate.h"
+#include "googleurl/src/gurl.h"
+
+class Profile;
+
+namespace chromeos {
+
+// The system notification object handles the display of a system notification
+
+class SystemNotification {
+ public:
+ // The profile is the current user profile. The id is any string used
+ // to uniquely identify this notification. The title is the title of
+ // the message to be displayed. On creation, the message is hidden.
+ SystemNotification(Profile* profile, std::string id, int icon_resource_id,
+ string16 title);
+
+ ~SystemNotification();
+
+ // Show will show or update the message for this notification
+ // on a transition to urgent, the notification will be shown if it was
+ // previously hidden or minimized by the user.
+ void Show(const string16& message, bool urgent);
+
+ // Hide will dismiss the notification, if the notification is already
+ // hidden it does nothing
+ void Hide();
+
+ // Current visibility state for this notification.
+ bool visible() const { return visible_; }
+
+ // Current urgent state for this notification.
+ bool urgent() const { return urgent_; }
+
+ private:
+ class Delegate : public NotificationDelegate {
+ public:
+ explicit Delegate(std::string id) : id_(base::move(id)) {}
+ void Display() {}
+ void Error() {}
+ void Close(bool by_user) {}
+ std::string id() const { return id_; }
+
+ private:
+ std::string id_;
+
+ DISALLOW_COPY_AND_ASSIGN(Delegate);
+ };
+
+ Profile* profile_;
+ BalloonCollectionImpl* collection_;
+ scoped_refptr<Delegate> delegate_;
+ GURL icon_;
+ string16 title_;
+ bool visible_;
+ bool urgent_;
+
+ DISALLOW_COPY_AND_ASSIGN(SystemNotification);
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_SYSTEM_NOTIFICATION_H_
+
diff --git a/chrome/browser/chromeos/notifications/system_notification_factory.cc b/chrome/browser/chromeos/notifications/system_notification_factory.cc
new file mode 100644
index 0000000..ac01322
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/system_notification_factory.cc
@@ -0,0 +1,21 @@
+// Copyright (c) 2010 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.
+
+#include "chrome/browser/chromeos/notifications/system_notification_factory.h"
+
+#include "chrome/browser/notifications/desktop_notification_service.h"
+
+namespace chromeos {
+
+// static
+Notification SystemNotificationFactory::Create(
+ const GURL& icon, const string16& title,
+ const string16& text,
+ NotificationDelegate* delegate) {
+ string16 content_url = DesktopNotificationService::CreateDataUrl(
+ icon, title, text, WebKit::WebTextDirectionDefault);
+ return Notification(GURL(), GURL(content_url), std::wstring(), string16(),
+ delegate);
+}
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/notifications/system_notification_factory.h b/chrome/browser/chromeos/notifications/system_notification_factory.h
new file mode 100644
index 0000000..6acd53a
--- /dev/null
+++ b/chrome/browser/chromeos/notifications/system_notification_factory.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_SYSTEM_NOTIFICATION_FACTORY_H_
+#define CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_SYSTEM_NOTIFICATION_FACTORY_H_
+
+#include "base/basictypes.h"
+#include "chrome/browser/notifications/notification.h"
+
+class GURL;
+class NotificationDelegate;
+
+namespace chromeos {
+
+// A utility class for system notifications.
+class SystemNotificationFactory {
+ public:
+
+ // Creates a system notification.
+ static Notification Create(
+ const GURL& icon, const string16& title,
+ const string16& text,
+ NotificationDelegate* delegate);
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_NOTIFICATIONS_SYSTEM_NOTIFICATION_FACTORY_H_