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 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 115 BalloonViewImpl::~BalloonViewImpl() { 116 } 117 118 void BalloonViewImpl::Close(bool by_user) { 119 MessageLoop::current()->PostTask(FROM_HERE, 120 method_factory_.NewRunnableMethod( 121 &BalloonViewImpl::DelayedClose, by_user)); 122 } 123 124 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 133 BalloonHost* BalloonViewImpl::GetHost() const { 134 return html_contents_.get(); 135 } 136 137 void BalloonViewImpl::RunMenu(views::View* source, const gfx::Point& pt) { 138 RunOptionsMenu(pt); 139 } 140 141 void BalloonViewImpl::OnDisplayChanged() { 142 collection_->DisplayChanged(); 143 } 144 145 void BalloonViewImpl::OnWorkAreaChanged() { 146 collection_->DisplayChanged(); 147 } 148 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 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 167 gfx::Size BalloonViewImpl::GetPreferredSize() { 168 return gfx::Size(1000, 1000); 169 } 170 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 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 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 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 251 gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const { 252 return gfx::Rect( 253 width() - kDismissButtonWidth - 254 kDismissButtonRightMargin - kRightShadowWidth, 255 kDismissButtonTopMargin, 256 kDismissButtonWidth, 257 kDismissButtonHeight); 258 } 259 260 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 270 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 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 365 void BalloonViewImpl::RunOptionsMenu(const gfx::Point& pt) { 366 CreateOptionsMenu(); 367 options_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT); 368 } 369 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 378 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 404 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 428 gfx::Point BalloonViewImpl::GetContentsOffset() const { 429 return gfx::Point(kLeftShadowWidth + kLeftMargin, 430 kTopShadowWidth + kTopMargin); 431 } 432 433 int BalloonViewImpl::GetShelfHeight() const { 434 // TODO(johnnyg): add scaling here. 435 return kDefaultShelfHeight; 436 } 437 438 int BalloonViewImpl::GetBalloonFrameHeight() const { 439 return GetTotalHeight() - GetShelfHeight(); 440 } 441 442 int BalloonViewImpl::GetTotalWidth() const { 443 return balloon_->content_size().width() 444 + kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; 445 } 446 447 int BalloonViewImpl::GetTotalHeight() const { 448 return balloon_->content_size().height() 449 + kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth 450 + GetShelfHeight(); 451 } 452 453 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 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 488 void BalloonViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) { 489 SizeContentsWindow(); 490 } 491 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