diff options
author | Ben Murdoch <benm@google.com> | 2010-07-29 17:14:53 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2010-08-04 14:29:45 +0100 |
commit | c407dc5cd9bdc5668497f21b26b09d988ab439de (patch) | |
tree | 7eaf8707c0309516bdb042ad976feedaf72b0bb1 /chrome/browser/chromeos/notifications | |
parent | 0998b1cdac5733f299c12d88bc31ef9c8035b8fa (diff) | |
download | external_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')
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, ©); + 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_ |