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