Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / views / notifications / balloon_view_views.cc
1 // Copyright (c) 2012 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_views.h"
6
7 #include <algorithm>
8 #include <vector>
9
10 #include "base/bind.h"
11 #include "base/message_loop/message_loop.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/chrome_notification_types.h"
14 #include "chrome/browser/notifications/balloon_collection.h"
15 #include "chrome/browser/notifications/desktop_notification_service.h"
16 #include "chrome/browser/notifications/notification.h"
17 #include "chrome/browser/notifications/notification_options_menu_model.h"
18 #include "chrome/browser/ui/views/notifications/balloon_view_host.h"
19 #include "content/public/browser/notification_details.h"
20 #include "content/public/browser/notification_source.h"
21 #include "content/public/browser/notification_types.h"
22 #include "content/public/browser/render_view_host.h"
23 #include "content/public/browser/render_widget_host_view.h"
24 #include "content/public/browser/web_contents.h"
25 #include "grit/generated_resources.h"
26 #include "grit/theme_resources.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/base/resource/resource_bundle.h"
29 #include "ui/gfx/animation/slide_animation.h"
30 #include "ui/gfx/canvas.h"
31 #include "ui/gfx/native_widget_types.h"
32 #include "ui/gfx/path.h"
33 #include "ui/views/bubble/bubble_border.h"
34 #include "ui/views/controls/button/image_button.h"
35 #include "ui/views/controls/button/menu_button.h"
36 #include "ui/views/controls/button/text_button.h"
37 #include "ui/views/controls/label.h"
38 #include "ui/views/controls/menu/menu_item_view.h"
39 #include "ui/views/controls/menu/menu_runner.h"
40 #include "ui/views/controls/native/native_view_host.h"
41 #include "ui/views/widget/widget.h"
42
43 namespace {
44
45 const int kTopMargin = 2;
46 const int kBottomMargin = 0;
47 const int kLeftMargin = 4;
48 const int kRightMargin = 4;
49
50 // Margin between various shelf buttons/label and the shelf border.
51 const int kShelfMargin = 2;
52
53 // Spacing between the options and close buttons.
54 const int kOptionsDismissSpacing = 4;
55
56 // Spacing between the options button and label text.
57 const int kLabelOptionsSpacing = 4;
58
59 // Margin between shelf border and title label.
60 const int kLabelLeftMargin = 6;
61
62 // Size of the drop shadow.  The shadow is provided by BubbleBorder,
63 // not this class.
64 const int kLeftShadowWidth = 0;
65 const int kRightShadowWidth = 0;
66 const int kTopShadowWidth = 0;
67 const int kBottomShadowWidth = 6;
68
69 // Optional animation.
70 const bool kAnimateEnabled = true;
71
72 // Colors
73 const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245);
74 const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125);
75 const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180);
76
77 }  // namespace
78
79 // static
80 int BalloonView::GetHorizontalMargin() {
81   return kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
82 }
83
84 BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection)
85     : balloon_(NULL),
86       collection_(collection),
87       frame_container_(NULL),
88       html_container_(NULL),
89       close_button_(NULL),
90       options_menu_button_(NULL),
91       enable_web_ui_(false),
92       closed_by_user_(false),
93       closed_(false) {
94   // We're owned by Balloon and don't want to be deleted by our parent View.
95   set_owned_by_client();
96
97   SetBorder(scoped_ptr<views::Border>(
98       new views::BubbleBorder(views::BubbleBorder::FLOAT,
99                               views::BubbleBorder::NO_SHADOW,
100                               SK_ColorWHITE)));
101 }
102
103 BalloonViewImpl::~BalloonViewImpl() {
104 }
105
106 void BalloonViewImpl::Close(bool by_user) {
107   if (closed_)
108     return;
109
110   closed_ = true;
111   animation_->Stop();
112   html_contents_->Shutdown();
113   // Detach contents from the widget before they close.
114   // This is necessary because a widget may be deleted
115   // after this when chrome is shutting down.
116   html_container_->GetRootView()->RemoveAllChildViews(true);
117   html_container_->Close();
118   frame_container_->GetRootView()->RemoveAllChildViews(true);
119   frame_container_->Close();
120   closed_by_user_ = by_user;
121   // |frame_container_->::Close()| is async. When processed it'll call back to
122   // DeleteDelegate() and we'll cleanup.
123 }
124
125 gfx::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
134 BalloonHost* BalloonViewImpl::GetHost() const {
135   return html_contents_.get();
136 }
137
138 void BalloonViewImpl::OnMenuButtonClicked(views::View* source,
139                                           const gfx::Point& point) {
140   CreateOptionsMenu();
141
142   menu_runner_.reset(new views::MenuRunner(options_menu_model_.get()));
143
144   gfx::Point screen_location;
145   views::View::ConvertPointToScreen(options_menu_button_, &screen_location);
146   if (menu_runner_->RunMenuAt(
147           source->GetWidget()->GetTopLevelWidget(),
148           options_menu_button_,
149           gfx::Rect(screen_location, options_menu_button_->size()),
150           views::MenuItemView::TOPRIGHT,
151           ui::MENU_SOURCE_NONE,
152           views::MenuRunner::HAS_MNEMONICS) == views::MenuRunner::MENU_DELETED)
153     return;
154 }
155
156 void BalloonViewImpl::OnDisplayChanged() {
157   collection_->DisplayChanged();
158 }
159
160 void BalloonViewImpl::OnWorkAreaChanged() {
161   collection_->DisplayChanged();
162 }
163
164 void BalloonViewImpl::DeleteDelegate() {
165   balloon_->OnClose(closed_by_user_);
166 }
167
168 void BalloonViewImpl::ButtonPressed(views::Button* sender, const ui::Event&) {
169   // The only button currently is the close button.
170   DCHECK_EQ(close_button_, sender);
171   Close(true);
172 }
173
174 gfx::Size BalloonViewImpl::GetPreferredSize() {
175   return gfx::Size(1000, 1000);
176 }
177
178 void BalloonViewImpl::SizeContentsWindow() {
179   if (!html_container_ || !frame_container_)
180     return;
181
182   gfx::Rect contents_rect = GetContentsRectangle();
183   html_container_->SetBounds(contents_rect);
184   html_container_->StackAboveWidget(frame_container_);
185
186   gfx::Path path;
187   GetContentsMask(contents_rect, &path);
188   html_container_->SetShape(path.CreateNativeRegion());
189
190   close_button_->SetBoundsRect(GetCloseButtonBounds());
191   options_menu_button_->SetBoundsRect(GetOptionsButtonBounds());
192   source_label_->SetBoundsRect(GetLabelBounds());
193 }
194
195 void BalloonViewImpl::RepositionToBalloon() {
196   if (closed_)
197     return;
198
199   DCHECK(frame_container_);
200   DCHECK(html_container_);
201   DCHECK(balloon_);
202
203   if (!kAnimateEnabled) {
204     frame_container_->SetBounds(GetBoundsForFrameContainer());
205     gfx::Rect contents_rect = GetContentsRectangle();
206     html_container_->SetBounds(contents_rect);
207     html_contents_->SetPreferredSize(contents_rect.size());
208     content::RenderWidgetHostView* view =
209         html_contents_->web_contents()->GetRenderWidgetHostView();
210     if (view)
211       view->SetSize(contents_rect.size());
212     return;
213   }
214
215   anim_frame_end_ = GetBoundsForFrameContainer();
216   anim_frame_start_ = frame_container_->GetClientAreaBoundsInScreen();
217   animation_.reset(new gfx::SlideAnimation(this));
218   animation_->Show();
219 }
220
221 void BalloonViewImpl::Update() {
222   if (closed_)
223     return;
224
225   // Tls might get called before html_contents_ is set in Show() if more than
226   // one update with the same replace_id occurs, or if an update occurs after
227   // the ballon has been closed (e.g. during shutdown) but before this has been
228   // destroyed.
229   if (!html_contents_.get() || !html_contents_->web_contents())
230     return;
231   html_contents_->web_contents()->GetController().LoadURL(
232       balloon_->notification().content_url(), content::Referrer(),
233       content::PAGE_TRANSITION_LINK, std::string());
234 }
235
236 void BalloonViewImpl::AnimationProgressed(const gfx::Animation* animation) {
237   DCHECK_EQ(animation_.get(), animation);
238
239   // Linear interpolation from start to end position.
240   gfx::Rect frame_position(animation_->CurrentValueBetween(
241                                anim_frame_start_, anim_frame_end_));
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   content::RenderWidgetHostView* view =
252       html_contents_->web_contents()->GetRenderWidgetHostView();
253   if (view)
254     view->SetSize(contents_rect.size());
255 }
256
257 gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const {
258   gfx::Rect bounds(GetContentsBounds());
259   bounds.set_height(GetShelfHeight());
260   const gfx::Size& pref_size(close_button_->GetPreferredSize());
261   bounds.Inset(bounds.width() - kShelfMargin - pref_size.width(), 0,
262       kShelfMargin, 0);
263   bounds.ClampToCenteredSize(pref_size);
264   return bounds;
265 }
266
267 gfx::Rect BalloonViewImpl::GetOptionsButtonBounds() const {
268   gfx::Rect bounds(GetContentsBounds());
269   bounds.set_height(GetShelfHeight());
270   const gfx::Size& pref_size(options_menu_button_->GetPreferredSize());
271   bounds.set_x(GetCloseButtonBounds().x() - kOptionsDismissSpacing -
272       pref_size.width());
273   bounds.set_width(pref_size.width());
274   bounds.ClampToCenteredSize(pref_size);
275   return bounds;
276 }
277
278 gfx::Rect BalloonViewImpl::GetLabelBounds() const {
279   gfx::Rect bounds(GetContentsBounds());
280   bounds.set_height(GetShelfHeight());
281   gfx::Size pref_size(source_label_->GetPreferredSize());
282   bounds.Inset(kLabelLeftMargin, 0, bounds.width() -
283       GetOptionsButtonBounds().x() + kLabelOptionsSpacing, 0);
284   pref_size.set_width(bounds.width());
285   bounds.ClampToCenteredSize(pref_size);
286   return bounds;
287 }
288
289 void BalloonViewImpl::Show(Balloon* balloon) {
290   if (closed_)
291     return;
292
293   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
294
295   balloon_ = balloon;
296
297   const base::string16 source_label_text = l10n_util::GetStringFUTF16(
298       IDS_NOTIFICATION_BALLOON_SOURCE_LABEL,
299       balloon->notification().display_source());
300
301   source_label_ = new views::Label(source_label_text);
302   AddChildView(source_label_);
303   options_menu_button_ =
304       new views::MenuButton(NULL, base::string16(), this, false);
305   AddChildView(options_menu_button_);
306   close_button_ = new views::ImageButton(this);
307   close_button_->SetTooltipText(l10n_util::GetStringUTF16(
308       IDS_NOTIFICATION_BALLOON_DISMISS_LABEL));
309   AddChildView(close_button_);
310
311   // We have to create two windows: one for the contents and one for the
312   // frame.  Why?
313   // * The contents is an html window which cannot be a
314   //   layered window (because it may have child windows for instance).
315   // * The frame is a layered window so that we can have nicely rounded
316   //   corners using alpha blending (and we may do other alpha blending
317   //   effects).
318   // Unfortunately, layered windows cannot have child windows. (Well, they can
319   // but the child windows don't render).
320   //
321   // We carefully keep these two windows in sync to present the illusion of
322   // one window to the user.
323   //
324   // We don't let the OS manage the RTL layout of these widgets, because
325   // this code is already taking care of correctly reversing the layout.
326   html_contents_.reset(new BalloonViewHost(balloon));
327   html_contents_->SetPreferredSize(gfx::Size(10000, 10000));
328   if (enable_web_ui_)
329     html_contents_->EnableWebUI();
330
331   html_container_ = new views::Widget;
332   views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
333   html_container_->Init(params);
334   html_container_->SetContentsView(html_contents_->view());
335
336   frame_container_ = new views::Widget;
337   params.delegate = this;
338   params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
339   params.bounds = GetBoundsForFrameContainer();
340   frame_container_->Init(params);
341   frame_container_->SetContentsView(this);
342   frame_container_->StackAboveWidget(html_container_);
343
344   // GetContentsRectangle() is calculated relative to |frame_container_|. Make
345   // sure |frame_container_| has bounds before we ask for
346   // GetContentsRectangle().
347   html_container_->SetBounds(GetContentsRectangle());
348
349   // SetAlwaysOnTop should be called after StackAboveWidget because otherwise
350   // the top-most flag will be removed.
351   html_container_->SetAlwaysOnTop(true);
352   frame_container_->SetAlwaysOnTop(true);
353
354   close_button_->SetImage(views::CustomButton::STATE_NORMAL,
355                           rb.GetImageSkiaNamed(IDR_CLOSE_1));
356   close_button_->SetImage(views::CustomButton::STATE_HOVERED,
357                           rb.GetImageSkiaNamed(IDR_CLOSE_1_H));
358   close_button_->SetImage(views::CustomButton::STATE_PRESSED,
359                           rb.GetImageSkiaNamed(IDR_CLOSE_1_P));
360   close_button_->SetBoundsRect(GetCloseButtonBounds());
361   close_button_->SetBackground(SK_ColorBLACK,
362                                rb.GetImageSkiaNamed(IDR_CLOSE_1),
363                                rb.GetImageSkiaNamed(IDR_CLOSE_1_MASK));
364
365   options_menu_button_->SetIcon(*rb.GetImageSkiaNamed(IDR_BALLOON_WRENCH));
366   options_menu_button_->SetHoverIcon(
367       *rb.GetImageSkiaNamed(IDR_BALLOON_WRENCH_H));
368   options_menu_button_->SetPushedIcon(*rb.GetImageSkiaNamed(
369       IDR_BALLOON_WRENCH_P));
370   options_menu_button_->set_alignment(views::TextButton::ALIGN_CENTER);
371   options_menu_button_->SetBorder(views::Border::NullBorder());
372   options_menu_button_->SetBoundsRect(GetOptionsButtonBounds());
373
374   source_label_->SetFontList(rb.GetFontList(ui::ResourceBundle::SmallFont));
375   source_label_->SetBackgroundColor(kControlBarBackgroundColor);
376   source_label_->SetEnabledColor(kControlBarTextColor);
377   source_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
378   source_label_->SetBoundsRect(GetLabelBounds());
379
380   SizeContentsWindow();
381   html_container_->Show();
382   frame_container_->Show();
383
384   notification_registrar_.Add(
385     this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED,
386     content::Source<Balloon>(balloon));
387 }
388
389 void BalloonViewImpl::CreateOptionsMenu() {
390   if (options_menu_model_.get())
391     return;
392   options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_));
393 }
394
395 void BalloonViewImpl::GetContentsMask(const gfx::Rect& rect,
396                                       gfx::Path* path) const {
397   // This rounds the corners, and we also cut out a circle for the close
398   // button, since we can't guarantee the ordering of two top-most windows.
399   SkScalar radius = SkIntToScalar(views::BubbleBorder::GetCornerRadius());
400   SkScalar spline_radius = radius -
401       SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3);
402   SkScalar left = SkIntToScalar(0);
403   SkScalar top = SkIntToScalar(0);
404   SkScalar right = SkIntToScalar(rect.width());
405   SkScalar bottom = SkIntToScalar(rect.height());
406
407   path->moveTo(left, top);
408   path->lineTo(right, top);
409   path->lineTo(right, bottom - radius);
410   path->cubicTo(right, bottom - spline_radius,
411                 right - spline_radius, bottom,
412                 right - radius, bottom);
413   path->lineTo(left + radius, bottom);
414   path->cubicTo(left + spline_radius, bottom,
415                 left, bottom - spline_radius,
416                 left, bottom - radius);
417   path->lineTo(left, top);
418   path->close();
419 }
420
421 void BalloonViewImpl::GetFrameMask(const gfx::Rect& rect,
422                                    gfx::Path* path) const {
423   SkScalar radius = SkIntToScalar(views::BubbleBorder::GetCornerRadius());
424   SkScalar spline_radius = radius -
425       SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3);
426   SkScalar left = SkIntToScalar(rect.x());
427   SkScalar top = SkIntToScalar(rect.y());
428   SkScalar right = SkIntToScalar(rect.right());
429   SkScalar bottom = SkIntToScalar(rect.bottom());
430
431   path->moveTo(left, bottom);
432   path->lineTo(left, top + radius);
433   path->cubicTo(left, top + spline_radius,
434                 left + spline_radius, top,
435                 left + radius, top);
436   path->lineTo(right - radius, top);
437   path->cubicTo(right - spline_radius, top,
438                 right, top + spline_radius,
439                 right, top + radius);
440   path->lineTo(right, bottom);
441   path->lineTo(left, bottom);
442   path->close();
443 }
444
445 gfx::Point BalloonViewImpl::GetContentsOffset() const {
446   return gfx::Point(kLeftShadowWidth + kLeftMargin,
447                     kTopShadowWidth + kTopMargin);
448 }
449
450 gfx::Rect BalloonViewImpl::GetBoundsForFrameContainer() const {
451   return gfx::Rect(balloon_->GetPosition().x(), balloon_->GetPosition().y(),
452                    GetTotalWidth(), GetTotalHeight());
453 }
454
455 int BalloonViewImpl::GetShelfHeight() const {
456   // TODO(johnnyg): add scaling here.
457   int max_button_height = std::max(std::max(
458       close_button_->GetPreferredSize().height(),
459       options_menu_button_->GetPreferredSize().height()),
460       source_label_->GetPreferredSize().height());
461   return max_button_height + kShelfMargin * 2;
462 }
463
464 int BalloonViewImpl::GetBalloonFrameHeight() const {
465   return GetTotalHeight() - GetShelfHeight();
466 }
467
468 int BalloonViewImpl::GetTotalWidth() const {
469   return balloon_->content_size().width() +
470       kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
471 }
472
473 int BalloonViewImpl::GetTotalHeight() const {
474   return balloon_->content_size().height() +
475       kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth +
476       GetShelfHeight();
477 }
478
479 gfx::Rect BalloonViewImpl::GetContentsRectangle() const {
480   if (!frame_container_)
481     return gfx::Rect();
482
483   gfx::Size content_size = balloon_->content_size();
484   gfx::Point offset = GetContentsOffset();
485   gfx::Rect frame_rect = frame_container_->GetWindowBoundsInScreen();
486   return gfx::Rect(frame_rect.x() + offset.x(),
487                    frame_rect.y() + GetShelfHeight() + offset.y(),
488                    content_size.width(),
489                    content_size.height());
490 }
491
492 void BalloonViewImpl::OnPaint(gfx::Canvas* canvas) {
493   DCHECK(canvas);
494   // Paint the menu bar area white, with proper rounded corners.
495   gfx::Path path;
496   gfx::Rect rect = GetContentsBounds();
497   rect.set_height(GetShelfHeight());
498   GetFrameMask(rect, &path);
499
500   SkPaint paint;
501   paint.setAntiAlias(true);
502   paint.setColor(kControlBarBackgroundColor);
503   canvas->DrawPath(path, paint);
504
505   // Draw a 1-pixel gray line between the content and the menu bar.
506   int line_width = GetTotalWidth() - kLeftMargin - kRightMargin;
507   canvas->FillRect(gfx::Rect(kLeftMargin, rect.bottom(), line_width, 1),
508                    kControlBarSeparatorLineColor);
509   View::OnPaint(canvas);
510   OnPaintBorder(canvas);
511 }
512
513 void BalloonViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) {
514   SizeContentsWindow();
515 }
516
517 void BalloonViewImpl::Observe(int type,
518                               const content::NotificationSource& source,
519                               const content::NotificationDetails& details) {
520   if (type != chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED) {
521     NOTREACHED();
522     return;
523   }
524
525   // If the renderer process attached to this balloon is disconnected
526   // (e.g., because of a crash), we want to close the balloon.
527   notification_registrar_.Remove(
528       this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED,
529       content::Source<Balloon>(balloon_));
530   Close(false);
531 }