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