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 "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"
45 const int kTopMargin = 2;
46 const int kBottomMargin = 0;
47 const int kLeftMargin = 4;
48 const int kRightMargin = 4;
50 // Margin between various shelf buttons/label and the shelf border.
51 const int kShelfMargin = 2;
53 // Spacing between the options and close buttons.
54 const int kOptionsDismissSpacing = 4;
56 // Spacing between the options button and label text.
57 const int kLabelOptionsSpacing = 4;
59 // Margin between shelf border and title label.
60 const int kLabelLeftMargin = 6;
62 // Size of the drop shadow. The shadow is provided by BubbleBorder,
64 const int kLeftShadowWidth = 0;
65 const int kRightShadowWidth = 0;
66 const int kTopShadowWidth = 0;
67 const int kBottomShadowWidth = 6;
69 // Optional animation.
70 const bool kAnimateEnabled = true;
73 const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245);
74 const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125);
75 const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180);
80 int BalloonView::GetHorizontalMargin() {
81 return kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
84 BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection)
86 collection_(collection),
87 frame_container_(NULL),
88 html_container_(NULL),
90 options_menu_button_(NULL),
91 enable_web_ui_(false),
92 closed_by_user_(false),
94 // We're owned by Balloon and don't want to be deleted by our parent View.
95 set_owned_by_client();
97 SetBorder(scoped_ptr<views::Border>(
98 new views::BubbleBorder(views::BubbleBorder::FLOAT,
99 views::BubbleBorder::NO_SHADOW,
103 BalloonViewImpl::~BalloonViewImpl() {
106 void BalloonViewImpl::Close(bool by_user) {
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.
125 gfx::Size BalloonViewImpl::GetSize() const {
126 // BalloonView has no size if it hasn't been shown yet (which is when
129 return gfx::Size(0, 0);
131 return gfx::Size(GetTotalWidth(), GetTotalHeight());
134 BalloonHost* BalloonViewImpl::GetHost() const {
135 return html_contents_.get();
138 void BalloonViewImpl::OnMenuButtonClicked(views::View* source,
139 const gfx::Point& point) {
142 menu_runner_.reset(new views::MenuRunner(options_menu_model_.get()));
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)
156 void BalloonViewImpl::OnDisplayChanged() {
157 collection_->DisplayChanged();
160 void BalloonViewImpl::OnWorkAreaChanged() {
161 collection_->DisplayChanged();
164 void BalloonViewImpl::DeleteDelegate() {
165 balloon_->OnClose(closed_by_user_);
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);
174 gfx::Size BalloonViewImpl::GetPreferredSize() {
175 return gfx::Size(1000, 1000);
178 void BalloonViewImpl::SizeContentsWindow() {
179 if (!html_container_ || !frame_container_)
182 gfx::Rect contents_rect = GetContentsRectangle();
183 html_container_->SetBounds(contents_rect);
184 html_container_->StackAboveWidget(frame_container_);
187 GetContentsMask(contents_rect, &path);
188 html_container_->SetShape(path.CreateNativeRegion());
190 close_button_->SetBoundsRect(GetCloseButtonBounds());
191 options_menu_button_->SetBoundsRect(GetOptionsButtonBounds());
192 source_label_->SetBoundsRect(GetLabelBounds());
195 void BalloonViewImpl::RepositionToBalloon() {
199 DCHECK(frame_container_);
200 DCHECK(html_container_);
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();
211 view->SetSize(contents_rect.size());
215 anim_frame_end_ = GetBoundsForFrameContainer();
216 anim_frame_start_ = frame_container_->GetClientAreaBoundsInScreen();
217 animation_.reset(new gfx::SlideAnimation(this));
221 void BalloonViewImpl::Update() {
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
229 if (!html_contents_.get() || !html_contents_->web_contents())
231 html_contents_->web_contents()->GetController().LoadURL(
232 balloon_->notification().content_url(), content::Referrer(),
233 content::PAGE_TRANSITION_LINK, std::string());
236 void BalloonViewImpl::AnimationProgressed(const gfx::Animation* animation) {
237 DCHECK_EQ(animation_.get(), animation);
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);
245 gfx::Rect contents_rect = GetContentsRectangle();
246 html_container_->SetBounds(contents_rect);
247 GetContentsMask(contents_rect, &path);
248 html_container_->SetShape(path.CreateNativeRegion());
250 html_contents_->SetPreferredSize(contents_rect.size());
251 content::RenderWidgetHostView* view =
252 html_contents_->web_contents()->GetRenderWidgetHostView();
254 view->SetSize(contents_rect.size());
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,
263 bounds.ClampToCenteredSize(pref_size);
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 -
273 bounds.set_width(pref_size.width());
274 bounds.ClampToCenteredSize(pref_size);
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);
289 void BalloonViewImpl::Show(Balloon* balloon) {
293 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
297 const base::string16 source_label_text = l10n_util::GetStringFUTF16(
298 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL,
299 balloon->notification().display_source());
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_);
311 // We have to create two windows: one for the contents and one for the
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
318 // Unfortunately, layered windows cannot have child windows. (Well, they can
319 // but the child windows don't render).
321 // We carefully keep these two windows in sync to present the illusion of
322 // one window to the user.
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));
329 html_contents_->EnableWebUI();
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());
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_);
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());
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);
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));
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());
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());
380 SizeContentsWindow();
381 html_container_->Show();
382 frame_container_->Show();
384 notification_registrar_.Add(
385 this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED,
386 content::Source<Balloon>(balloon));
389 void BalloonViewImpl::CreateOptionsMenu() {
390 if (options_menu_model_.get())
392 options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_));
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());
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);
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());
431 path->moveTo(left, bottom);
432 path->lineTo(left, top + radius);
433 path->cubicTo(left, top + spline_radius,
434 left + spline_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);
445 gfx::Point BalloonViewImpl::GetContentsOffset() const {
446 return gfx::Point(kLeftShadowWidth + kLeftMargin,
447 kTopShadowWidth + kTopMargin);
450 gfx::Rect BalloonViewImpl::GetBoundsForFrameContainer() const {
451 return gfx::Rect(balloon_->GetPosition().x(), balloon_->GetPosition().y(),
452 GetTotalWidth(), GetTotalHeight());
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;
464 int BalloonViewImpl::GetBalloonFrameHeight() const {
465 return GetTotalHeight() - GetShelfHeight();
468 int BalloonViewImpl::GetTotalWidth() const {
469 return balloon_->content_size().width() +
470 kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
473 int BalloonViewImpl::GetTotalHeight() const {
474 return balloon_->content_size().height() +
475 kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth +
479 gfx::Rect BalloonViewImpl::GetContentsRectangle() const {
480 if (!frame_container_)
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());
492 void BalloonViewImpl::OnPaint(gfx::Canvas* canvas) {
494 // Paint the menu bar area white, with proper rounded corners.
496 gfx::Rect rect = GetContentsBounds();
497 rect.set_height(GetShelfHeight());
498 GetFrameMask(rect, &path);
501 paint.setAntiAlias(true);
502 paint.setColor(kControlBarBackgroundColor);
503 canvas->DrawPath(path, paint);
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);
513 void BalloonViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) {
514 SizeContentsWindow();
517 void BalloonViewImpl::Observe(int type,
518 const content::NotificationSource& source,
519 const content::NotificationDetails& details) {
520 if (type != chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED) {
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_));