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.
5 #include "chrome/browser/ui/views/notifications/balloon_view_views.h"
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 "content/public/browser/notification_details.h"
19 #include "content/public/browser/notification_source.h"
20 #include "content/public/browser/notification_types.h"
21 #include "content/public/browser/render_view_host.h"
22 #include "content/public/browser/render_widget_host_view.h"
23 #include "content/public/browser/web_contents.h"
24 #include "grit/generated_resources.h"
25 #include "grit/theme_resources.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "ui/gfx/animation/slide_animation.h"
29 #include "ui/gfx/canvas.h"
30 #include "ui/gfx/native_widget_types.h"
31 #include "ui/gfx/path.h"
32 #include "ui/views/bubble/bubble_border.h"
33 #include "ui/views/controls/button/image_button.h"
34 #include "ui/views/controls/button/menu_button.h"
35 #include "ui/views/controls/button/text_button.h"
36 #include "ui/views/controls/label.h"
37 #include "ui/views/controls/menu/menu_item_view.h"
38 #include "ui/views/controls/menu/menu_runner.h"
39 #include "ui/views/controls/native/native_view_host.h"
40 #include "ui/views/widget/widget.h"
42 #if defined(OS_CHROMEOS)
43 #include "chrome/browser/chromeos/notifications/balloon_view_host_chromeos.h"
45 #include "chrome/browser/ui/views/notifications/balloon_view_host.h"
50 const int kTopMargin
= 2;
51 const int kBottomMargin
= 0;
52 const int kLeftMargin
= 4;
53 const int kRightMargin
= 4;
55 // Margin between various shelf buttons/label and the shelf border.
56 const int kShelfMargin
= 2;
58 // Spacing between the options and close buttons.
59 const int kOptionsDismissSpacing
= 4;
61 // Spacing between the options button and label text.
62 const int kLabelOptionsSpacing
= 4;
64 // Margin between shelf border and title label.
65 const int kLabelLeftMargin
= 6;
67 // Size of the drop shadow. The shadow is provided by BubbleBorder,
69 const int kLeftShadowWidth
= 0;
70 const int kRightShadowWidth
= 0;
71 const int kTopShadowWidth
= 0;
72 const int kBottomShadowWidth
= 6;
74 // Optional animation.
75 const bool kAnimateEnabled
= true;
78 const SkColor kControlBarBackgroundColor
= SkColorSetRGB(245, 245, 245);
79 const SkColor kControlBarTextColor
= SkColorSetRGB(125, 125, 125);
80 const SkColor kControlBarSeparatorLineColor
= SkColorSetRGB(180, 180, 180);
85 int BalloonView::GetHorizontalMargin() {
86 return kLeftMargin
+ kRightMargin
+ kLeftShadowWidth
+ kRightShadowWidth
;
89 BalloonViewImpl::BalloonViewImpl(BalloonCollection
* collection
)
91 collection_(collection
),
92 frame_container_(NULL
),
93 html_container_(NULL
),
95 options_menu_button_(NULL
),
96 enable_web_ui_(false),
97 closed_by_user_(false),
99 // We're owned by Balloon and don't want to be deleted by our parent View.
100 set_owned_by_client();
102 set_border(new views::BubbleBorder(views::BubbleBorder::FLOAT
,
103 views::BubbleBorder::NO_SHADOW
, SK_ColorWHITE
));
106 BalloonViewImpl::~BalloonViewImpl() {
109 void BalloonViewImpl::Close(bool by_user
) {
115 html_contents_
->Shutdown();
116 // Detach contents from the widget before they close.
117 // This is necessary because a widget may be deleted
118 // after this when chrome is shutting down.
119 html_container_
->GetRootView()->RemoveAllChildViews(true);
120 html_container_
->Close();
121 frame_container_
->GetRootView()->RemoveAllChildViews(true);
122 frame_container_
->Close();
123 closed_by_user_
= by_user
;
124 // |frame_container_->::Close()| is async. When processed it'll call back to
125 // DeleteDelegate() and we'll cleanup.
128 gfx::Size
BalloonViewImpl::GetSize() const {
129 // BalloonView has no size if it hasn't been shown yet (which is when
132 return gfx::Size(0, 0);
134 return gfx::Size(GetTotalWidth(), GetTotalHeight());
137 BalloonHost
* BalloonViewImpl::GetHost() const {
138 return html_contents_
.get();
141 void BalloonViewImpl::OnMenuButtonClicked(views::View
* source
,
142 const gfx::Point
& point
) {
145 menu_runner_
.reset(new views::MenuRunner(options_menu_model_
.get()));
147 gfx::Point screen_location
;
148 views::View::ConvertPointToScreen(options_menu_button_
, &screen_location
);
149 if (menu_runner_
->RunMenuAt(
150 source
->GetWidget()->GetTopLevelWidget(),
151 options_menu_button_
,
152 gfx::Rect(screen_location
, options_menu_button_
->size()),
153 views::MenuItemView::TOPRIGHT
,
154 ui::MENU_SOURCE_NONE
,
155 views::MenuRunner::HAS_MNEMONICS
) == views::MenuRunner::MENU_DELETED
)
159 void BalloonViewImpl::OnDisplayChanged() {
160 collection_
->DisplayChanged();
163 void BalloonViewImpl::OnWorkAreaChanged() {
164 collection_
->DisplayChanged();
167 void BalloonViewImpl::DeleteDelegate() {
168 balloon_
->OnClose(closed_by_user_
);
171 void BalloonViewImpl::ButtonPressed(views::Button
* sender
, const ui::Event
&) {
172 // The only button currently is the close button.
173 DCHECK_EQ(close_button_
, sender
);
177 gfx::Size
BalloonViewImpl::GetPreferredSize() {
178 return gfx::Size(1000, 1000);
181 void BalloonViewImpl::SizeContentsWindow() {
182 if (!html_container_
|| !frame_container_
)
185 gfx::Rect contents_rect
= GetContentsRectangle();
186 html_container_
->SetBounds(contents_rect
);
187 html_container_
->StackAboveWidget(frame_container_
);
190 GetContentsMask(contents_rect
, &path
);
191 html_container_
->SetShape(path
.CreateNativeRegion());
193 close_button_
->SetBoundsRect(GetCloseButtonBounds());
194 options_menu_button_
->SetBoundsRect(GetOptionsButtonBounds());
195 source_label_
->SetBoundsRect(GetLabelBounds());
198 void BalloonViewImpl::RepositionToBalloon() {
202 DCHECK(frame_container_
);
203 DCHECK(html_container_
);
206 if (!kAnimateEnabled
) {
207 frame_container_
->SetBounds(GetBoundsForFrameContainer());
208 gfx::Rect contents_rect
= GetContentsRectangle();
209 html_container_
->SetBounds(contents_rect
);
210 html_contents_
->SetPreferredSize(contents_rect
.size());
211 content::RenderWidgetHostView
* view
=
212 html_contents_
->web_contents()->GetRenderWidgetHostView();
214 view
->SetSize(contents_rect
.size());
218 anim_frame_end_
= GetBoundsForFrameContainer();
219 anim_frame_start_
= frame_container_
->GetClientAreaBoundsInScreen();
220 animation_
.reset(new gfx::SlideAnimation(this));
224 void BalloonViewImpl::Update() {
228 // Tls might get called before html_contents_ is set in Show() if more than
229 // one update with the same replace_id occurs, or if an update occurs after
230 // the ballon has been closed (e.g. during shutdown) but before this has been
232 if (!html_contents_
.get() || !html_contents_
->web_contents())
234 html_contents_
->web_contents()->GetController().LoadURL(
235 balloon_
->notification().content_url(), content::Referrer(),
236 content::PAGE_TRANSITION_LINK
, std::string());
239 void BalloonViewImpl::AnimationProgressed(const gfx::Animation
* animation
) {
240 DCHECK_EQ(animation_
.get(), animation
);
242 // Linear interpolation from start to end position.
243 gfx::Rect
frame_position(animation_
->CurrentValueBetween(
244 anim_frame_start_
, anim_frame_end_
));
245 frame_container_
->SetBounds(frame_position
);
248 gfx::Rect contents_rect
= GetContentsRectangle();
249 html_container_
->SetBounds(contents_rect
);
250 GetContentsMask(contents_rect
, &path
);
251 html_container_
->SetShape(path
.CreateNativeRegion());
253 html_contents_
->SetPreferredSize(contents_rect
.size());
254 content::RenderWidgetHostView
* view
=
255 html_contents_
->web_contents()->GetRenderWidgetHostView();
257 view
->SetSize(contents_rect
.size());
260 gfx::Rect
BalloonViewImpl::GetCloseButtonBounds() const {
261 gfx::Rect
bounds(GetContentsBounds());
262 bounds
.set_height(GetShelfHeight());
263 const gfx::Size
& pref_size(close_button_
->GetPreferredSize());
264 bounds
.Inset(bounds
.width() - kShelfMargin
- pref_size
.width(), 0,
266 bounds
.ClampToCenteredSize(pref_size
);
270 gfx::Rect
BalloonViewImpl::GetOptionsButtonBounds() const {
271 gfx::Rect
bounds(GetContentsBounds());
272 bounds
.set_height(GetShelfHeight());
273 const gfx::Size
& pref_size(options_menu_button_
->GetPreferredSize());
274 bounds
.set_x(GetCloseButtonBounds().x() - kOptionsDismissSpacing
-
276 bounds
.set_width(pref_size
.width());
277 bounds
.ClampToCenteredSize(pref_size
);
281 gfx::Rect
BalloonViewImpl::GetLabelBounds() const {
282 gfx::Rect
bounds(GetContentsBounds());
283 bounds
.set_height(GetShelfHeight());
284 gfx::Size
pref_size(source_label_
->GetPreferredSize());
285 bounds
.Inset(kLabelLeftMargin
, 0, bounds
.width() -
286 GetOptionsButtonBounds().x() + kLabelOptionsSpacing
, 0);
287 pref_size
.set_width(bounds
.width());
288 bounds
.ClampToCenteredSize(pref_size
);
292 void BalloonViewImpl::Show(Balloon
* balloon
) {
296 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
300 const base::string16 source_label_text
= l10n_util::GetStringFUTF16(
301 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL
,
302 balloon
->notification().display_source());
304 source_label_
= new views::Label(source_label_text
);
305 AddChildView(source_label_
);
306 options_menu_button_
=
307 new views::MenuButton(NULL
, base::string16(), this, false);
308 AddChildView(options_menu_button_
);
309 #if defined(OS_CHROMEOS)
310 // Disable and hide the options menu on ChromeOS. This is a short term fix
311 // for a crash (long term we're redesigning notifications).
312 options_menu_button_
->SetEnabled(false);
313 options_menu_button_
->SetVisible(false);
315 close_button_
= new views::ImageButton(this);
316 close_button_
->SetTooltipText(l10n_util::GetStringUTF16(
317 IDS_NOTIFICATION_BALLOON_DISMISS_LABEL
));
318 AddChildView(close_button_
);
320 // We have to create two windows: one for the contents and one for the
322 // * The contents is an html window which cannot be a
323 // layered window (because it may have child windows for instance).
324 // * The frame is a layered window so that we can have nicely rounded
325 // corners using alpha blending (and we may do other alpha blending
327 // Unfortunately, layered windows cannot have child windows. (Well, they can
328 // but the child windows don't render).
330 // We carefully keep these two windows in sync to present the illusion of
331 // one window to the user.
333 // We don't let the OS manage the RTL layout of these widgets, because
334 // this code is already taking care of correctly reversing the layout.
335 #if defined(OS_CHROMEOS) && defined(USE_AURA)
336 html_contents_
.reset(new chromeos::BalloonViewHost(balloon
));
338 html_contents_
.reset(new BalloonViewHost(balloon
));
340 html_contents_
->SetPreferredSize(gfx::Size(10000, 10000));
342 html_contents_
->EnableWebUI();
344 html_container_
= new views::Widget
;
345 views::Widget::InitParams
params(views::Widget::InitParams::TYPE_POPUP
);
346 html_container_
->Init(params
);
347 html_container_
->SetContentsView(html_contents_
->view());
349 frame_container_
= new views::Widget
;
350 params
.delegate
= this;
351 params
.opacity
= views::Widget::InitParams::TRANSLUCENT_WINDOW
;
352 params
.bounds
= GetBoundsForFrameContainer();
353 frame_container_
->Init(params
);
354 frame_container_
->SetContentsView(this);
355 frame_container_
->StackAboveWidget(html_container_
);
357 // GetContentsRectangle() is calculated relative to |frame_container_|. Make
358 // sure |frame_container_| has bounds before we ask for
359 // GetContentsRectangle().
360 html_container_
->SetBounds(GetContentsRectangle());
362 // SetAlwaysOnTop should be called after StackAboveWidget because otherwise
363 // the top-most flag will be removed.
364 html_container_
->SetAlwaysOnTop(true);
365 frame_container_
->SetAlwaysOnTop(true);
367 close_button_
->SetImage(views::CustomButton::STATE_NORMAL
,
368 rb
.GetImageSkiaNamed(IDR_CLOSE_1
));
369 close_button_
->SetImage(views::CustomButton::STATE_HOVERED
,
370 rb
.GetImageSkiaNamed(IDR_CLOSE_1_H
));
371 close_button_
->SetImage(views::CustomButton::STATE_PRESSED
,
372 rb
.GetImageSkiaNamed(IDR_CLOSE_1_P
));
373 close_button_
->SetBoundsRect(GetCloseButtonBounds());
374 close_button_
->SetBackground(SK_ColorBLACK
,
375 rb
.GetImageSkiaNamed(IDR_CLOSE_1
),
376 rb
.GetImageSkiaNamed(IDR_CLOSE_1_MASK
));
378 options_menu_button_
->SetIcon(*rb
.GetImageSkiaNamed(IDR_BALLOON_WRENCH
));
379 options_menu_button_
->SetHoverIcon(
380 *rb
.GetImageSkiaNamed(IDR_BALLOON_WRENCH_H
));
381 options_menu_button_
->SetPushedIcon(*rb
.GetImageSkiaNamed(
382 IDR_BALLOON_WRENCH_P
));
383 options_menu_button_
->set_alignment(views::TextButton::ALIGN_CENTER
);
384 options_menu_button_
->set_border(NULL
);
385 options_menu_button_
->SetBoundsRect(GetOptionsButtonBounds());
387 source_label_
->SetFontList(rb
.GetFontList(ui::ResourceBundle::SmallFont
));
388 source_label_
->SetBackgroundColor(kControlBarBackgroundColor
);
389 source_label_
->SetEnabledColor(kControlBarTextColor
);
390 source_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
391 source_label_
->SetBoundsRect(GetLabelBounds());
393 SizeContentsWindow();
394 html_container_
->Show();
395 frame_container_
->Show();
397 notification_registrar_
.Add(
398 this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED
,
399 content::Source
<Balloon
>(balloon
));
402 void BalloonViewImpl::CreateOptionsMenu() {
403 if (options_menu_model_
.get())
405 options_menu_model_
.reset(new NotificationOptionsMenuModel(balloon_
));
408 void BalloonViewImpl::GetContentsMask(const gfx::Rect
& rect
,
409 gfx::Path
* path
) const {
410 // This rounds the corners, and we also cut out a circle for the close
411 // button, since we can't guarantee the ordering of two top-most windows.
412 SkScalar radius
= SkIntToScalar(views::BubbleBorder::GetCornerRadius());
413 SkScalar spline_radius
= radius
-
414 SkScalarMul(radius
, (SK_ScalarSqrt2
- SK_Scalar1
) * 4 / 3);
415 SkScalar left
= SkIntToScalar(0);
416 SkScalar top
= SkIntToScalar(0);
417 SkScalar right
= SkIntToScalar(rect
.width());
418 SkScalar bottom
= SkIntToScalar(rect
.height());
420 path
->moveTo(left
, top
);
421 path
->lineTo(right
, top
);
422 path
->lineTo(right
, bottom
- radius
);
423 path
->cubicTo(right
, bottom
- spline_radius
,
424 right
- spline_radius
, bottom
,
425 right
- radius
, bottom
);
426 path
->lineTo(left
+ radius
, bottom
);
427 path
->cubicTo(left
+ spline_radius
, bottom
,
428 left
, bottom
- spline_radius
,
429 left
, bottom
- radius
);
430 path
->lineTo(left
, top
);
434 void BalloonViewImpl::GetFrameMask(const gfx::Rect
& rect
,
435 gfx::Path
* path
) const {
436 SkScalar radius
= SkIntToScalar(views::BubbleBorder::GetCornerRadius());
437 SkScalar spline_radius
= radius
-
438 SkScalarMul(radius
, (SK_ScalarSqrt2
- SK_Scalar1
) * 4 / 3);
439 SkScalar left
= SkIntToScalar(rect
.x());
440 SkScalar top
= SkIntToScalar(rect
.y());
441 SkScalar right
= SkIntToScalar(rect
.right());
442 SkScalar bottom
= SkIntToScalar(rect
.bottom());
444 path
->moveTo(left
, bottom
);
445 path
->lineTo(left
, top
+ radius
);
446 path
->cubicTo(left
, top
+ spline_radius
,
447 left
+ spline_radius
, top
,
449 path
->lineTo(right
- radius
, top
);
450 path
->cubicTo(right
- spline_radius
, top
,
451 right
, top
+ spline_radius
,
452 right
, top
+ radius
);
453 path
->lineTo(right
, bottom
);
454 path
->lineTo(left
, bottom
);
458 gfx::Point
BalloonViewImpl::GetContentsOffset() const {
459 return gfx::Point(kLeftShadowWidth
+ kLeftMargin
,
460 kTopShadowWidth
+ kTopMargin
);
463 gfx::Rect
BalloonViewImpl::GetBoundsForFrameContainer() const {
464 return gfx::Rect(balloon_
->GetPosition().x(), balloon_
->GetPosition().y(),
465 GetTotalWidth(), GetTotalHeight());
468 int BalloonViewImpl::GetShelfHeight() const {
469 // TODO(johnnyg): add scaling here.
470 int max_button_height
= std::max(std::max(
471 close_button_
->GetPreferredSize().height(),
472 options_menu_button_
->GetPreferredSize().height()),
473 source_label_
->GetPreferredSize().height());
474 return max_button_height
+ kShelfMargin
* 2;
477 int BalloonViewImpl::GetBalloonFrameHeight() const {
478 return GetTotalHeight() - GetShelfHeight();
481 int BalloonViewImpl::GetTotalWidth() const {
482 return balloon_
->content_size().width() +
483 kLeftMargin
+ kRightMargin
+ kLeftShadowWidth
+ kRightShadowWidth
;
486 int BalloonViewImpl::GetTotalHeight() const {
487 return balloon_
->content_size().height() +
488 kTopMargin
+ kBottomMargin
+ kTopShadowWidth
+ kBottomShadowWidth
+
492 gfx::Rect
BalloonViewImpl::GetContentsRectangle() const {
493 if (!frame_container_
)
496 gfx::Size content_size
= balloon_
->content_size();
497 gfx::Point offset
= GetContentsOffset();
498 gfx::Rect frame_rect
= frame_container_
->GetWindowBoundsInScreen();
499 return gfx::Rect(frame_rect
.x() + offset
.x(),
500 frame_rect
.y() + GetShelfHeight() + offset
.y(),
501 content_size
.width(),
502 content_size
.height());
505 void BalloonViewImpl::OnPaint(gfx::Canvas
* canvas
) {
507 // Paint the menu bar area white, with proper rounded corners.
509 gfx::Rect rect
= GetContentsBounds();
510 rect
.set_height(GetShelfHeight());
511 GetFrameMask(rect
, &path
);
514 paint
.setAntiAlias(true);
515 paint
.setColor(kControlBarBackgroundColor
);
516 canvas
->DrawPath(path
, paint
);
518 // Draw a 1-pixel gray line between the content and the menu bar.
519 int line_width
= GetTotalWidth() - kLeftMargin
- kRightMargin
;
520 canvas
->FillRect(gfx::Rect(kLeftMargin
, rect
.bottom(), line_width
, 1),
521 kControlBarSeparatorLineColor
);
522 View::OnPaint(canvas
);
523 OnPaintBorder(canvas
);
526 void BalloonViewImpl::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
527 SizeContentsWindow();
530 void BalloonViewImpl::Observe(int type
,
531 const content::NotificationSource
& source
,
532 const content::NotificationDetails
& details
) {
533 if (type
!= chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED
) {
538 // If the renderer process attached to this balloon is disconnected
539 // (e.g., because of a crash), we want to close the balloon.
540 notification_registrar_
.Remove(
541 this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED
,
542 content::Source
<Balloon
>(balloon_
));