Home | History | Annotate | Download | only in notifications
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/ui/views/notifications/balloon_view.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/message_loop.h"
     10 #include "base/utf_string_conversions.h"
     11 #include "chrome/browser/notifications/balloon.h"
     12 #include "chrome/browser/notifications/balloon_collection.h"
     13 #include "chrome/browser/notifications/desktop_notification_service.h"
     14 #include "chrome/browser/notifications/notification.h"
     15 #include "chrome/browser/notifications/notification_options_menu_model.h"
     16 #include "chrome/browser/ui/views/bubble/bubble_border.h"
     17 #include "chrome/browser/ui/views/notifications/balloon_view_host.h"
     18 #include "content/browser/renderer_host/render_view_host.h"
     19 #include "content/browser/renderer_host/render_widget_host_view.h"
     20 #include "content/common/notification_details.h"
     21 #include "content/common/notification_source.h"
     22 #include "content/common/notification_type.h"
     23 #include "grit/generated_resources.h"
     24 #include "grit/theme_resources.h"
     25 #include "ui/base/animation/slide_animation.h"
     26 #include "ui/base/l10n/l10n_util.h"
     27 #include "ui/base/resource/resource_bundle.h"
     28 #include "ui/gfx/canvas_skia.h"
     29 #include "ui/gfx/insets.h"
     30 #include "ui/gfx/native_widget_types.h"
     31 #include "views/controls/button/button.h"
     32 #include "views/controls/button/image_button.h"
     33 #include "views/controls/button/text_button.h"
     34 #include "views/controls/menu/menu_2.h"
     35 #include "views/controls/native/native_view_host.h"
     36 #include "views/painter.h"
     37 #include "views/widget/root_view.h"
     38 #if defined(OS_WIN)
     39 #include "views/widget/widget_win.h"
     40 #endif
     41 #if defined(OS_LINUX)
     42 #include "views/widget/widget_gtk.h"
     43 #endif
     44 
     45 using views::Widget;
     46 
     47 namespace {
     48 
     49 const int kTopMargin = 2;
     50 const int kBottomMargin = 0;
     51 const int kLeftMargin = 4;
     52 const int kRightMargin = 4;
     53 const int kShelfBorderTopOverlap = 0;
     54 
     55 // Properties of the dismiss button.
     56 const int kDismissButtonWidth = 14;
     57 const int kDismissButtonHeight = 14;
     58 const int kDismissButtonTopMargin = 6;
     59 const int kDismissButtonRightMargin = 6;
     60 
     61 // Properties of the options menu.
     62 const int kOptionsButtonWidth = 21;
     63 const int kOptionsButtonHeight = 14;
     64 const int kOptionsButtonTopMargin = 5;
     65 const int kOptionsButtonRightMargin = 4;
     66 
     67 // Properties of the origin label.
     68 const int kLabelLeftMargin = 10;
     69 const int kLabelTopMargin = 6;
     70 
     71 // Size of the drop shadow.  The shadow is provided by BubbleBorder,
     72 // not this class.
     73 const int kLeftShadowWidth = 0;
     74 const int kRightShadowWidth = 0;
     75 const int kTopShadowWidth = 0;
     76 const int kBottomShadowWidth = 6;
     77 
     78 // Optional animation.
     79 const bool kAnimateEnabled = true;
     80 
     81 // The shelf height for the system default font size.  It is scaled
     82 // with changes in the default font size.
     83 const int kDefaultShelfHeight = 22;
     84 
     85 // Menu commands
     86 const int kRevokePermissionCommand = 0;
     87 
     88 // Colors
     89 const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245);
     90 const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125);
     91 const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180);
     92 
     93 }  // namespace
     94 
     95 BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection)
     96     : balloon_(NULL),
     97       collection_(collection),
     98       frame_container_(NULL),
     99       html_container_(NULL),
    100       html_contents_(NULL),
    101       method_factory_(this),
    102       close_button_(NULL),
    103       animation_(NULL),
    104       options_menu_model_(NULL),
    105       options_menu_menu_(NULL),
    106       options_menu_button_(NULL) {
    107   // This object is not to be deleted by the views hierarchy,
    108   // as it is owned by the balloon.
    109   set_parent_owned(false);
    110 
    111   BubbleBorder* bubble_border = new BubbleBorder(BubbleBorder::FLOAT);
    112   set_border(bubble_border);
    113 }
    114 
    115 BalloonViewImpl::~BalloonViewImpl() {
    116 }
    117 
    118 void BalloonViewImpl::Close(bool by_user) {
    119   MessageLoop::current()->PostTask(FROM_HERE,
    120       method_factory_.NewRunnableMethod(
    121           &BalloonViewImpl::DelayedClose, by_user));
    122 }
    123 
    124 gfx::Size BalloonViewImpl::GetSize() const {
    125   // BalloonView has no size if it hasn't been shown yet (which is when
    126   // balloon_ is set).
    127   if (!balloon_)
    128     return gfx::Size(0, 0);
    129 
    130   return gfx::Size(GetTotalWidth(), GetTotalHeight());
    131 }
    132 
    133 BalloonHost* BalloonViewImpl::GetHost() const {
    134   return html_contents_.get();
    135 }
    136 
    137 void BalloonViewImpl::RunMenu(views::View* source, const gfx::Point& pt) {
    138   RunOptionsMenu(pt);
    139 }
    140 
    141 void BalloonViewImpl::OnDisplayChanged() {
    142   collection_->DisplayChanged();
    143 }
    144 
    145 void BalloonViewImpl::OnWorkAreaChanged() {
    146   collection_->DisplayChanged();
    147 }
    148 
    149 void BalloonViewImpl::ButtonPressed(views::Button* sender,
    150                                     const views::Event&) {
    151   // The only button currently is the close button.
    152   DCHECK(sender == close_button_);
    153   Close(true);
    154 }
    155 
    156 void BalloonViewImpl::DelayedClose(bool by_user) {
    157   html_contents_->Shutdown();
    158   html_container_->CloseNow();
    159   // The BalloonViewImpl has to be detached from frame_container_ now
    160   // because CloseNow on linux/views destroys the view hierachy
    161   // asynchronously.
    162   frame_container_->GetRootView()->RemoveAllChildViews(true);
    163   frame_container_->CloseNow();
    164   balloon_->OnClose(by_user);
    165 }
    166 
    167 gfx::Size BalloonViewImpl::GetPreferredSize() {
    168   return gfx::Size(1000, 1000);
    169 }
    170 
    171 void BalloonViewImpl::SizeContentsWindow() {
    172   if (!html_container_ || !frame_container_)
    173     return;
    174 
    175   gfx::Rect contents_rect = GetContentsRectangle();
    176   html_container_->SetBounds(contents_rect);
    177   html_container_->MoveAboveWidget(frame_container_);
    178 
    179   gfx::Path path;
    180   GetContentsMask(contents_rect, &path);
    181   html_container_->SetShape(path.CreateNativeRegion());
    182 
    183   close_button_->SetBoundsRect(GetCloseButtonBounds());
    184   options_menu_button_->SetBoundsRect(GetOptionsButtonBounds());
    185   source_label_->SetBoundsRect(GetLabelBounds());
    186 }
    187 
    188 void BalloonViewImpl::RepositionToBalloon() {
    189   DCHECK(frame_container_);
    190   DCHECK(html_container_);
    191   DCHECK(balloon_);
    192 
    193   if (!kAnimateEnabled) {
    194     frame_container_->SetBounds(
    195         gfx::Rect(balloon_->GetPosition().x(), balloon_->GetPosition().y(),
    196                   GetTotalWidth(), GetTotalHeight()));
    197     gfx::Rect contents_rect = GetContentsRectangle();
    198     html_container_->SetBounds(contents_rect);
    199     html_contents_->SetPreferredSize(contents_rect.size());
    200     RenderWidgetHostView* view = html_contents_->render_view_host()->view();
    201     if (view)
    202       view->SetSize(contents_rect.size());
    203     return;
    204   }
    205 
    206   anim_frame_end_ = gfx::Rect(
    207       balloon_->GetPosition().x(), balloon_->GetPosition().y(),
    208       GetTotalWidth(), GetTotalHeight());
    209   anim_frame_start_ = frame_container_->GetClientAreaScreenBounds();
    210   animation_.reset(new ui::SlideAnimation(this));
    211   animation_->Show();
    212 }
    213 
    214 void BalloonViewImpl::Update() {
    215   DCHECK(html_contents_.get()) << "BalloonView::Update called before Show";
    216   if (html_contents_->render_view_host())
    217     html_contents_->render_view_host()->NavigateToURL(
    218         balloon_->notification().content_url());
    219 }
    220 
    221 void BalloonViewImpl::AnimationProgressed(const ui::Animation* animation) {
    222   DCHECK(animation == animation_.get());
    223 
    224   // Linear interpolation from start to end position.
    225   double e = animation->GetCurrentValue();
    226   double s = (1.0 - e);
    227 
    228   gfx::Rect frame_position(
    229     static_cast<int>(s * anim_frame_start_.x() +
    230                      e * anim_frame_end_.x()),
    231     static_cast<int>(s * anim_frame_start_.y() +
    232                      e * anim_frame_end_.y()),
    233     static_cast<int>(s * anim_frame_start_.width() +
    234                      e * anim_frame_end_.width()),
    235     static_cast<int>(s * anim_frame_start_.height() +
    236                      e * anim_frame_end_.height()));
    237   frame_container_->SetBounds(frame_position);
    238 
    239   gfx::Path path;
    240   gfx::Rect contents_rect = GetContentsRectangle();
    241   html_container_->SetBounds(contents_rect);
    242   GetContentsMask(contents_rect, &path);
    243   html_container_->SetShape(path.CreateNativeRegion());
    244 
    245   html_contents_->SetPreferredSize(contents_rect.size());
    246   RenderWidgetHostView* view = html_contents_->render_view_host()->view();
    247   if (view)
    248     view->SetSize(contents_rect.size());
    249 }
    250 
    251 gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const {
    252   return gfx::Rect(
    253       width() - kDismissButtonWidth -
    254           kDismissButtonRightMargin - kRightShadowWidth,
    255       kDismissButtonTopMargin,
    256       kDismissButtonWidth,
    257       kDismissButtonHeight);
    258 }
    259 
    260 gfx::Rect BalloonViewImpl::GetOptionsButtonBounds() const {
    261   gfx::Rect close_rect = GetCloseButtonBounds();
    262 
    263   return gfx::Rect(
    264       close_rect.x() - kOptionsButtonWidth - kOptionsButtonRightMargin,
    265       kOptionsButtonTopMargin,
    266       kOptionsButtonWidth,
    267       kOptionsButtonHeight);
    268 }
    269 
    270 gfx::Rect BalloonViewImpl::GetLabelBounds() const {
    271   return gfx::Rect(
    272       kLeftShadowWidth + kLabelLeftMargin,
    273       kLabelTopMargin,
    274       std::max(0, width() - kOptionsButtonWidth -
    275                kRightMargin),
    276       kOptionsButtonHeight);
    277 }
    278 
    279 void BalloonViewImpl::Show(Balloon* balloon) {
    280   ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    281 
    282   balloon_ = balloon;
    283 
    284   SetBounds(balloon_->GetPosition().x(), balloon_->GetPosition().y(),
    285             GetTotalWidth(), GetTotalHeight());
    286 
    287   const string16 source_label_text = l10n_util::GetStringFUTF16(
    288       IDS_NOTIFICATION_BALLOON_SOURCE_LABEL,
    289       balloon->notification().display_source());
    290 
    291   source_label_ = new views::Label(UTF16ToWide(source_label_text));
    292   AddChildView(source_label_);
    293   options_menu_button_ = new views::MenuButton(NULL, L"", this, false);
    294   AddChildView(options_menu_button_);
    295   close_button_ = new views::ImageButton(this);
    296   close_button_->SetTooltipText(UTF16ToWide(l10n_util::GetStringUTF16(
    297       IDS_NOTIFICATION_BALLOON_DISMISS_LABEL)));
    298   AddChildView(close_button_);
    299 
    300   // We have to create two windows: one for the contents and one for the
    301   // frame.  Why?
    302   // * The contents is an html window which cannot be a
    303   //   layered window (because it may have child windows for instance).
    304   // * The frame is a layered window so that we can have nicely rounded
    305   //   corners using alpha blending (and we may do other alpha blending
    306   //   effects).
    307   // Unfortunately, layered windows cannot have child windows. (Well, they can
    308   // but the child windows don't render).
    309   //
    310   // We carefully keep these two windows in sync to present the illusion of
    311   // one window to the user.
    312   //
    313   // We don't let the OS manage the RTL layout of these widgets, because
    314   // this code is already taking care of correctly reversing the layout.
    315   gfx::Rect contents_rect = GetContentsRectangle();
    316   html_contents_.reset(new BalloonViewHost(balloon));
    317   html_contents_->SetPreferredSize(gfx::Size(10000, 10000));
    318   Widget::CreateParams params(Widget::CreateParams::TYPE_POPUP);
    319   params.mirror_origin_in_rtl = false;
    320   html_container_ = Widget::CreateWidget(params);
    321   html_container_->SetAlwaysOnTop(true);
    322   html_container_->Init(NULL, contents_rect);
    323   html_container_->SetContentsView(html_contents_->view());
    324 
    325   gfx::Rect balloon_rect(x(), y(), GetTotalWidth(), GetTotalHeight());
    326   params.transparent = true;
    327   frame_container_ = Widget::CreateWidget(params);
    328   frame_container_->set_widget_delegate(this);
    329   frame_container_->SetAlwaysOnTop(true);
    330   frame_container_->Init(NULL, balloon_rect);
    331   frame_container_->SetContentsView(this);
    332   frame_container_->MoveAboveWidget(html_container_);
    333 
    334   close_button_->SetImage(views::CustomButton::BS_NORMAL,
    335                           rb.GetBitmapNamed(IDR_TAB_CLOSE));
    336   close_button_->SetImage(views::CustomButton::BS_HOT,
    337                           rb.GetBitmapNamed(IDR_TAB_CLOSE_H));
    338   close_button_->SetImage(views::CustomButton::BS_PUSHED,
    339                           rb.GetBitmapNamed(IDR_TAB_CLOSE_P));
    340   close_button_->SetBoundsRect(GetCloseButtonBounds());
    341   close_button_->SetBackground(SK_ColorBLACK,
    342                                rb.GetBitmapNamed(IDR_TAB_CLOSE),
    343                                rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK));
    344 
    345   options_menu_button_->SetIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH));
    346   options_menu_button_->SetHoverIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH_H));
    347   options_menu_button_->SetPushedIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH_P));
    348   options_menu_button_->set_alignment(views::TextButton::ALIGN_CENTER);
    349   options_menu_button_->set_border(NULL);
    350   options_menu_button_->SetBoundsRect(GetOptionsButtonBounds());
    351 
    352   source_label_->SetFont(rb.GetFont(ResourceBundle::SmallFont));
    353   source_label_->SetColor(kControlBarTextColor);
    354   source_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
    355   source_label_->SetBoundsRect(GetLabelBounds());
    356 
    357   SizeContentsWindow();
    358   html_container_->Show();
    359   frame_container_->Show();
    360 
    361   notification_registrar_.Add(this,
    362     NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon));
    363 }
    364 
    365 void BalloonViewImpl::RunOptionsMenu(const gfx::Point& pt) {
    366   CreateOptionsMenu();
    367   options_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT);
    368 }
    369 
    370 void BalloonViewImpl::CreateOptionsMenu() {
    371   if (options_menu_model_.get())
    372     return;
    373 
    374   options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_));
    375   options_menu_menu_.reset(new views::Menu2(options_menu_model_.get()));
    376 }
    377 
    378 void BalloonViewImpl::GetContentsMask(const gfx::Rect& rect,
    379                                       gfx::Path* path) const {
    380   // This rounds the corners, and we also cut out a circle for the close
    381   // button, since we can't guarantee the ordering of two top-most windows.
    382   SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius());
    383   SkScalar spline_radius = radius -
    384       SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3);
    385   SkScalar left = SkIntToScalar(0);
    386   SkScalar top = SkIntToScalar(0);
    387   SkScalar right = SkIntToScalar(rect.width());
    388   SkScalar bottom = SkIntToScalar(rect.height());
    389 
    390   path->moveTo(left, top);
    391   path->lineTo(right, top);
    392   path->lineTo(right, bottom - radius);
    393   path->cubicTo(right, bottom - spline_radius,
    394                 right - spline_radius, bottom,
    395                 right - radius, bottom);
    396   path->lineTo(left + radius, bottom);
    397   path->cubicTo(left + spline_radius, bottom,
    398                 left, bottom - spline_radius,
    399                 left, bottom - radius);
    400   path->lineTo(left, top);
    401   path->close();
    402 }
    403 
    404 void BalloonViewImpl::GetFrameMask(const gfx::Rect& rect,
    405                                    gfx::Path* path) const {
    406   SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius());
    407   SkScalar spline_radius = radius -
    408       SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3);
    409   SkScalar left = SkIntToScalar(rect.x());
    410   SkScalar top = SkIntToScalar(rect.y());
    411   SkScalar right = SkIntToScalar(rect.right());
    412   SkScalar bottom = SkIntToScalar(rect.bottom());
    413 
    414   path->moveTo(left, bottom);
    415   path->lineTo(left, top + radius);
    416   path->cubicTo(left, top + spline_radius,
    417                 left + spline_radius, top,
    418                 left + radius, top);
    419   path->lineTo(right - radius, top);
    420   path->cubicTo(right - spline_radius, top,
    421                 right, top + spline_radius,
    422                 right, top + radius);
    423   path->lineTo(right, bottom);
    424   path->lineTo(left, bottom);
    425   path->close();
    426 }
    427 
    428 gfx::Point BalloonViewImpl::GetContentsOffset() const {
    429   return gfx::Point(kLeftShadowWidth + kLeftMargin,
    430                     kTopShadowWidth + kTopMargin);
    431 }
    432 
    433 int BalloonViewImpl::GetShelfHeight() const {
    434   // TODO(johnnyg): add scaling here.
    435   return kDefaultShelfHeight;
    436 }
    437 
    438 int BalloonViewImpl::GetBalloonFrameHeight() const {
    439   return GetTotalHeight() - GetShelfHeight();
    440 }
    441 
    442 int BalloonViewImpl::GetTotalWidth() const {
    443   return balloon_->content_size().width()
    444       + kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
    445 }
    446 
    447 int BalloonViewImpl::GetTotalHeight() const {
    448   return balloon_->content_size().height()
    449       + kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth
    450       + GetShelfHeight();
    451 }
    452 
    453 gfx::Rect BalloonViewImpl::GetContentsRectangle() const {
    454   if (!frame_container_)
    455     return gfx::Rect();
    456 
    457   gfx::Size content_size = balloon_->content_size();
    458   gfx::Point offset = GetContentsOffset();
    459   gfx::Rect frame_rect = frame_container_->GetWindowScreenBounds();
    460   return gfx::Rect(frame_rect.x() + offset.x(),
    461                    frame_rect.y() + GetShelfHeight() + offset.y(),
    462                    content_size.width(),
    463                    content_size.height());
    464 }
    465 
    466 void BalloonViewImpl::OnPaint(gfx::Canvas* canvas) {
    467   DCHECK(canvas);
    468   // Paint the menu bar area white, with proper rounded corners.
    469   gfx::Path path;
    470   gfx::Rect rect = GetContentsBounds();
    471   rect.set_height(GetShelfHeight());
    472   GetFrameMask(rect, &path);
    473 
    474   SkPaint paint;
    475   paint.setAntiAlias(true);
    476   paint.setColor(kControlBarBackgroundColor);
    477   canvas->AsCanvasSkia()->drawPath(path, paint);
    478 
    479   // Draw a 1-pixel gray line between the content and the menu bar.
    480   int line_width = GetTotalWidth() - kLeftMargin - kRightMargin;
    481   canvas->FillRectInt(kControlBarSeparatorLineColor,
    482       kLeftMargin, 1 + GetShelfHeight(), line_width, 1);
    483 
    484   View::OnPaint(canvas);
    485   OnPaintBorder(canvas);
    486 }
    487 
    488 void BalloonViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) {
    489   SizeContentsWindow();
    490 }
    491 
    492 void BalloonViewImpl::Observe(NotificationType type,
    493                               const NotificationSource& source,
    494                               const NotificationDetails& details) {
    495   if (type != NotificationType::NOTIFY_BALLOON_DISCONNECTED) {
    496     NOTREACHED();
    497     return;
    498   }
    499 
    500   // If the renderer process attached to this balloon is disconnected
    501   // (e.g., because of a crash), we want to close the balloon.
    502   notification_registrar_.Remove(this,
    503       NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon_));
    504   Close(false);
    505 }
    506