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
45using views::Widget;
46
47namespace {
48
49const int kTopMargin = 2;
50const int kBottomMargin = 0;
51const int kLeftMargin = 4;
52const int kRightMargin = 4;
53const int kShelfBorderTopOverlap = 0;
54
55// Properties of the dismiss button.
56const int kDismissButtonWidth = 14;
57const int kDismissButtonHeight = 14;
58const int kDismissButtonTopMargin = 6;
59const int kDismissButtonRightMargin = 6;
60
61// Properties of the options menu.
62const int kOptionsButtonWidth = 21;
63const int kOptionsButtonHeight = 14;
64const int kOptionsButtonTopMargin = 5;
65const int kOptionsButtonRightMargin = 4;
66
67// Properties of the origin label.
68const int kLabelLeftMargin = 10;
69const int kLabelTopMargin = 6;
70
71// Size of the drop shadow.  The shadow is provided by BubbleBorder,
72// not this class.
73const int kLeftShadowWidth = 0;
74const int kRightShadowWidth = 0;
75const int kTopShadowWidth = 0;
76const int kBottomShadowWidth = 6;
77
78// Optional animation.
79const 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.
83const int kDefaultShelfHeight = 22;
84
85// Menu commands
86const int kRevokePermissionCommand = 0;
87
88// Colors
89const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245);
90const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125);
91const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180);
92
93}  // namespace
94
95BalloonViewImpl::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
115BalloonViewImpl::~BalloonViewImpl() {
116}
117
118void BalloonViewImpl::Close(bool by_user) {
119  MessageLoop::current()->PostTask(FROM_HERE,
120      method_factory_.NewRunnableMethod(
121          &BalloonViewImpl::DelayedClose, by_user));
122}
123
124gfx::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
133BalloonHost* BalloonViewImpl::GetHost() const {
134  return html_contents_.get();
135}
136
137void BalloonViewImpl::RunMenu(views::View* source, const gfx::Point& pt) {
138  RunOptionsMenu(pt);
139}
140
141void BalloonViewImpl::OnDisplayChanged() {
142  collection_->DisplayChanged();
143}
144
145void BalloonViewImpl::OnWorkAreaChanged() {
146  collection_->DisplayChanged();
147}
148
149void 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
156void 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
167gfx::Size BalloonViewImpl::GetPreferredSize() {
168  return gfx::Size(1000, 1000);
169}
170
171void 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
188void 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
214void 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
221void 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
251gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const {
252  return gfx::Rect(
253      width() - kDismissButtonWidth -
254          kDismissButtonRightMargin - kRightShadowWidth,
255      kDismissButtonTopMargin,
256      kDismissButtonWidth,
257      kDismissButtonHeight);
258}
259
260gfx::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
270gfx::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
279void 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
365void BalloonViewImpl::RunOptionsMenu(const gfx::Point& pt) {
366  CreateOptionsMenu();
367  options_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT);
368}
369
370void 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
378void 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
404void 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
428gfx::Point BalloonViewImpl::GetContentsOffset() const {
429  return gfx::Point(kLeftShadowWidth + kLeftMargin,
430                    kTopShadowWidth + kTopMargin);
431}
432
433int BalloonViewImpl::GetShelfHeight() const {
434  // TODO(johnnyg): add scaling here.
435  return kDefaultShelfHeight;
436}
437
438int BalloonViewImpl::GetBalloonFrameHeight() const {
439  return GetTotalHeight() - GetShelfHeight();
440}
441
442int BalloonViewImpl::GetTotalWidth() const {
443  return balloon_->content_size().width()
444      + kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
445}
446
447int BalloonViewImpl::GetTotalHeight() const {
448  return balloon_->content_size().height()
449      + kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth
450      + GetShelfHeight();
451}
452
453gfx::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
466void 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
488void BalloonViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) {
489  SizeContentsWindow();
490}
491
492void 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