balloon_view.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
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/renderer_host/render_view_host.h" 17#include "chrome/browser/renderer_host/render_widget_host_view.h" 18#include "chrome/browser/themes/browser_theme_provider.h" 19#include "chrome/browser/ui/views/bubble_border.h" 20#include "chrome/browser/ui/views/notifications/balloon_view_host.h" 21#include "chrome/common/notification_details.h" 22#include "chrome/common/notification_source.h" 23#include "chrome/common/notification_type.h" 24#include "grit/generated_resources.h" 25#include "grit/theme_resources.h" 26#include "ui/base/animation/slide_animation.h" 27#include "ui/base/l10n/l10n_util.h" 28#include "ui/base/resource/resource_bundle.h" 29#include "ui/gfx/canvas_skia.h" 30#include "ui/gfx/insets.h" 31#include "ui/gfx/native_widget_types.h" 32#include "views/controls/button/button.h" 33#include "views/controls/button/image_button.h" 34#include "views/controls/button/text_button.h" 35#include "views/controls/menu/menu_2.h" 36#include "views/controls/native/native_view_host.h" 37#include "views/painter.h" 38#include "views/widget/root_view.h" 39#if defined(OS_WIN) 40#include "views/widget/widget_win.h" 41#endif 42#if defined(OS_LINUX) 43#include "views/widget/widget_gtk.h" 44#endif 45 46using views::Widget; 47 48namespace { 49 50const int kTopMargin = 2; 51const int kBottomMargin = 0; 52const int kLeftMargin = 4; 53const int kRightMargin = 4; 54const int kShelfBorderTopOverlap = 0; 55 56// Properties of the dismiss button. 57const int kDismissButtonWidth = 14; 58const int kDismissButtonHeight = 14; 59const int kDismissButtonTopMargin = 6; 60const int kDismissButtonRightMargin = 6; 61 62// Properties of the options menu. 63const int kOptionsButtonWidth = 21; 64const int kOptionsButtonHeight = 14; 65const int kOptionsButtonTopMargin = 5; 66const int kOptionsButtonRightMargin = 4; 67 68// Properties of the origin label. 69const int kLabelLeftMargin = 10; 70const int kLabelTopMargin = 6; 71 72// Size of the drop shadow. The shadow is provided by BubbleBorder, 73// not this class. 74const int kLeftShadowWidth = 0; 75const int kRightShadowWidth = 0; 76const int kTopShadowWidth = 0; 77const int kBottomShadowWidth = 6; 78 79// Optional animation. 80const bool kAnimateEnabled = true; 81 82// The shelf height for the system default font size. It is scaled 83// with changes in the default font size. 84const int kDefaultShelfHeight = 22; 85 86// Menu commands 87const int kRevokePermissionCommand = 0; 88 89// Colors 90const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245); 91const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125); 92const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180); 93 94} // namespace 95 96BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection) 97 : balloon_(NULL), 98 collection_(collection), 99 frame_container_(NULL), 100 html_container_(NULL), 101 html_contents_(NULL), 102 method_factory_(this), 103 close_button_(NULL), 104 animation_(NULL), 105 options_menu_model_(NULL), 106 options_menu_menu_(NULL), 107 options_menu_button_(NULL) { 108 // This object is not to be deleted by the views hierarchy, 109 // as it is owned by the balloon. 110 set_parent_owned(false); 111 112 BubbleBorder* bubble_border = new BubbleBorder(BubbleBorder::FLOAT); 113 set_border(bubble_border); 114} 115 116BalloonViewImpl::~BalloonViewImpl() { 117} 118 119void BalloonViewImpl::Close(bool by_user) { 120 MessageLoop::current()->PostTask(FROM_HERE, 121 method_factory_.NewRunnableMethod( 122 &BalloonViewImpl::DelayedClose, by_user)); 123} 124 125gfx::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 134BalloonHost* BalloonViewImpl::GetHost() const { 135 return html_contents_.get(); 136} 137 138void BalloonViewImpl::RunMenu(views::View* source, const gfx::Point& pt) { 139 RunOptionsMenu(pt); 140} 141 142void BalloonViewImpl::DisplayChanged() { 143 collection_->DisplayChanged(); 144} 145 146void BalloonViewImpl::WorkAreaChanged() { 147 collection_->DisplayChanged(); 148} 149 150void BalloonViewImpl::ButtonPressed(views::Button* sender, 151 const views::Event&) { 152 // The only button currently is the close button. 153 DCHECK(sender == close_button_); 154 Close(true); 155} 156 157void BalloonViewImpl::DelayedClose(bool by_user) { 158 html_contents_->Shutdown(); 159 html_container_->CloseNow(); 160 // The BalloonViewImpl has to be detached from frame_container_ now 161 // because CloseNow on linux/views destroys the view hierachy 162 // asynchronously. 163 frame_container_->GetRootView()->RemoveAllChildViews(true); 164 frame_container_->CloseNow(); 165 balloon_->OnClose(by_user); 166} 167 168void BalloonViewImpl::OnBoundsChanged() { 169 SizeContentsWindow(); 170} 171 172gfx::Size BalloonViewImpl::GetPreferredSize() { 173 return gfx::Size(1000, 1000); 174} 175 176void BalloonViewImpl::SizeContentsWindow() { 177 if (!html_container_ || !frame_container_) 178 return; 179 180 gfx::Rect contents_rect = GetContentsRectangle(); 181 html_container_->SetBounds(contents_rect); 182 html_container_->MoveAbove(frame_container_); 183 184 gfx::Path path; 185 GetContentsMask(contents_rect, &path); 186 html_container_->SetShape(path.CreateNativeRegion()); 187 188 close_button_->SetBoundsRect(GetCloseButtonBounds()); 189 options_menu_button_->SetBoundsRect(GetOptionsButtonBounds()); 190 source_label_->SetBoundsRect(GetLabelBounds()); 191} 192 193void BalloonViewImpl::RepositionToBalloon() { 194 DCHECK(frame_container_); 195 DCHECK(html_container_); 196 DCHECK(balloon_); 197 198 if (!kAnimateEnabled) { 199 frame_container_->SetBounds( 200 gfx::Rect(balloon_->GetPosition().x(), balloon_->GetPosition().y(), 201 GetTotalWidth(), GetTotalHeight())); 202 gfx::Rect contents_rect = GetContentsRectangle(); 203 html_container_->SetBounds(contents_rect); 204 html_contents_->SetPreferredSize(contents_rect.size()); 205 RenderWidgetHostView* view = html_contents_->render_view_host()->view(); 206 if (view) 207 view->SetSize(contents_rect.size()); 208 return; 209 } 210 211 anim_frame_end_ = gfx::Rect( 212 balloon_->GetPosition().x(), balloon_->GetPosition().y(), 213 GetTotalWidth(), GetTotalHeight()); 214 frame_container_->GetBounds(&anim_frame_start_, false); 215 animation_.reset(new ui::SlideAnimation(this)); 216 animation_->Show(); 217} 218 219void BalloonViewImpl::Update() { 220 DCHECK(html_contents_.get()) << "BalloonView::Update called before Show"; 221 if (html_contents_->render_view_host()) 222 html_contents_->render_view_host()->NavigateToURL( 223 balloon_->notification().content_url()); 224} 225 226void BalloonViewImpl::AnimationProgressed(const ui::Animation* animation) { 227 DCHECK(animation == animation_.get()); 228 229 // Linear interpolation from start to end position. 230 double e = animation->GetCurrentValue(); 231 double s = (1.0 - e); 232 233 gfx::Rect frame_position( 234 static_cast<int>(s * anim_frame_start_.x() + 235 e * anim_frame_end_.x()), 236 static_cast<int>(s * anim_frame_start_.y() + 237 e * anim_frame_end_.y()), 238 static_cast<int>(s * anim_frame_start_.width() + 239 e * anim_frame_end_.width()), 240 static_cast<int>(s * anim_frame_start_.height() + 241 e * anim_frame_end_.height())); 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 RenderWidgetHostView* view = html_contents_->render_view_host()->view(); 252 if (view) 253 view->SetSize(contents_rect.size()); 254} 255 256gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const { 257 return gfx::Rect( 258 width() - kDismissButtonWidth - 259 kDismissButtonRightMargin - kRightShadowWidth, 260 kDismissButtonTopMargin, 261 kDismissButtonWidth, 262 kDismissButtonHeight); 263} 264 265gfx::Rect BalloonViewImpl::GetOptionsButtonBounds() const { 266 gfx::Rect close_rect = GetCloseButtonBounds(); 267 268 return gfx::Rect( 269 close_rect.x() - kOptionsButtonWidth - kOptionsButtonRightMargin, 270 kOptionsButtonTopMargin, 271 kOptionsButtonWidth, 272 kOptionsButtonHeight); 273} 274 275gfx::Rect BalloonViewImpl::GetLabelBounds() const { 276 return gfx::Rect( 277 kLeftShadowWidth + kLabelLeftMargin, 278 kLabelTopMargin, 279 std::max(0, width() - kOptionsButtonWidth - 280 kRightMargin), 281 kOptionsButtonHeight); 282} 283 284void BalloonViewImpl::Show(Balloon* balloon) { 285 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 286 287 balloon_ = balloon; 288 289 SetBounds(balloon_->GetPosition().x(), balloon_->GetPosition().y(), 290 GetTotalWidth(), GetTotalHeight()); 291 292 const string16 source_label_text = l10n_util::GetStringFUTF16( 293 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL, 294 balloon->notification().display_source()); 295 296 source_label_ = new views::Label(UTF16ToWide(source_label_text)); 297 AddChildView(source_label_); 298 options_menu_button_ = new views::MenuButton(NULL, L"", this, false); 299 AddChildView(options_menu_button_); 300 close_button_ = new views::ImageButton(this); 301 close_button_->SetTooltipText(UTF16ToWide(l10n_util::GetStringUTF16( 302 IDS_NOTIFICATION_BALLOON_DISMISS_LABEL))); 303 AddChildView(close_button_); 304 305 // We have to create two windows: one for the contents and one for the 306 // frame. Why? 307 // * The contents is an html window which cannot be a 308 // layered window (because it may have child windows for instance). 309 // * The frame is a layered window so that we can have nicely rounded 310 // corners using alpha blending (and we may do other alpha blending 311 // effects). 312 // Unfortunately, layered windows cannot have child windows. (Well, they can 313 // but the child windows don't render). 314 // 315 // We carefully keep these two windows in sync to present the illusion of 316 // one window to the user. 317 // 318 // We don't let the OS manage the RTL layout of these widgets, because 319 // this code is already taking care of correctly reversing the layout. 320 gfx::Rect contents_rect = GetContentsRectangle(); 321 html_contents_.reset(new BalloonViewHost(balloon)); 322 html_contents_->SetPreferredSize(gfx::Size(10000, 10000)); 323 html_container_ = Widget::CreatePopupWidget(Widget::NotTransparent, 324 Widget::AcceptEvents, 325 Widget::DeleteOnDestroy, 326 Widget::DontMirrorOriginInRTL); 327 html_container_->SetAlwaysOnTop(true); 328 html_container_->Init(NULL, contents_rect); 329 html_container_->SetContentsView(html_contents_->view()); 330 331 gfx::Rect balloon_rect(x(), y(), GetTotalWidth(), GetTotalHeight()); 332 frame_container_ = Widget::CreatePopupWidget(Widget::Transparent, 333 Widget::AcceptEvents, 334 Widget::DeleteOnDestroy, 335 Widget::DontMirrorOriginInRTL); 336 frame_container_->SetWidgetDelegate(this); 337 frame_container_->SetAlwaysOnTop(true); 338 frame_container_->Init(NULL, balloon_rect); 339 frame_container_->SetContentsView(this); 340 frame_container_->MoveAbove(html_container_); 341 342 close_button_->SetImage(views::CustomButton::BS_NORMAL, 343 rb.GetBitmapNamed(IDR_TAB_CLOSE)); 344 close_button_->SetImage(views::CustomButton::BS_HOT, 345 rb.GetBitmapNamed(IDR_TAB_CLOSE_H)); 346 close_button_->SetImage(views::CustomButton::BS_PUSHED, 347 rb.GetBitmapNamed(IDR_TAB_CLOSE_P)); 348 close_button_->SetBoundsRect(GetCloseButtonBounds()); 349 close_button_->SetBackground(SK_ColorBLACK, 350 rb.GetBitmapNamed(IDR_TAB_CLOSE), 351 rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK)); 352 353 options_menu_button_->SetIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH)); 354 options_menu_button_->SetHoverIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH_H)); 355 options_menu_button_->SetPushedIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH_P)); 356 options_menu_button_->set_alignment(views::TextButton::ALIGN_CENTER); 357 options_menu_button_->set_border(NULL); 358 options_menu_button_->SetBoundsRect(GetOptionsButtonBounds()); 359 360 source_label_->SetFont(rb.GetFont(ResourceBundle::SmallFont)); 361 source_label_->SetColor(kControlBarTextColor); 362 source_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); 363 source_label_->SetBoundsRect(GetLabelBounds()); 364 365 SizeContentsWindow(); 366 html_container_->Show(); 367 frame_container_->Show(); 368 369 notification_registrar_.Add(this, 370 NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon)); 371} 372 373void BalloonViewImpl::RunOptionsMenu(const gfx::Point& pt) { 374 CreateOptionsMenu(); 375 options_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT); 376} 377 378void BalloonViewImpl::CreateOptionsMenu() { 379 if (options_menu_model_.get()) 380 return; 381 382 options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_)); 383 options_menu_menu_.reset(new views::Menu2(options_menu_model_.get())); 384} 385 386void BalloonViewImpl::GetContentsMask(const gfx::Rect& rect, 387 gfx::Path* path) const { 388 // This rounds the corners, and we also cut out a circle for the close 389 // button, since we can't guarantee the ordering of two top-most windows. 390 SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius()); 391 SkScalar spline_radius = radius - 392 SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3); 393 SkScalar left = SkIntToScalar(0); 394 SkScalar top = SkIntToScalar(0); 395 SkScalar right = SkIntToScalar(rect.width()); 396 SkScalar bottom = SkIntToScalar(rect.height()); 397 398 path->moveTo(left, top); 399 path->lineTo(right, top); 400 path->lineTo(right, bottom - radius); 401 path->cubicTo(right, bottom - spline_radius, 402 right - spline_radius, bottom, 403 right - radius, bottom); 404 path->lineTo(left + radius, bottom); 405 path->cubicTo(left + spline_radius, bottom, 406 left, bottom - spline_radius, 407 left, bottom - radius); 408 path->lineTo(left, top); 409 path->close(); 410} 411 412void BalloonViewImpl::GetFrameMask(const gfx::Rect& rect, 413 gfx::Path* path) const { 414 SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius()); 415 SkScalar spline_radius = radius - 416 SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3); 417 SkScalar left = SkIntToScalar(rect.x()); 418 SkScalar top = SkIntToScalar(rect.y()); 419 SkScalar right = SkIntToScalar(rect.right()); 420 SkScalar bottom = SkIntToScalar(rect.bottom()); 421 422 path->moveTo(left, bottom); 423 path->lineTo(left, top + radius); 424 path->cubicTo(left, top + spline_radius, 425 left + spline_radius, top, 426 left + radius, top); 427 path->lineTo(right - radius, top); 428 path->cubicTo(right - spline_radius, top, 429 right, top + spline_radius, 430 right, top + radius); 431 path->lineTo(right, bottom); 432 path->lineTo(left, bottom); 433 path->close(); 434} 435 436gfx::Point BalloonViewImpl::GetContentsOffset() const { 437 return gfx::Point(kLeftShadowWidth + kLeftMargin, 438 kTopShadowWidth + kTopMargin); 439} 440 441int BalloonViewImpl::GetShelfHeight() const { 442 // TODO(johnnyg): add scaling here. 443 return kDefaultShelfHeight; 444} 445 446int BalloonViewImpl::GetBalloonFrameHeight() const { 447 return GetTotalHeight() - GetShelfHeight(); 448} 449 450int BalloonViewImpl::GetTotalWidth() const { 451 return balloon_->content_size().width() 452 + kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; 453} 454 455int BalloonViewImpl::GetTotalHeight() const { 456 return balloon_->content_size().height() 457 + kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth 458 + GetShelfHeight(); 459} 460 461gfx::Rect BalloonViewImpl::GetContentsRectangle() const { 462 if (!frame_container_) 463 return gfx::Rect(); 464 465 gfx::Size content_size = balloon_->content_size(); 466 gfx::Point offset = GetContentsOffset(); 467 gfx::Rect frame_rect; 468 frame_container_->GetBounds(&frame_rect, true); 469 return gfx::Rect(frame_rect.x() + offset.x(), 470 frame_rect.y() + GetShelfHeight() + offset.y(), 471 content_size.width(), 472 content_size.height()); 473} 474 475void BalloonViewImpl::Paint(gfx::Canvas* canvas) { 476 DCHECK(canvas); 477 // Paint the menu bar area white, with proper rounded corners. 478 gfx::Path path; 479 gfx::Rect rect = GetContentsBounds(); 480 rect.set_height(GetShelfHeight()); 481 GetFrameMask(rect, &path); 482 483 SkPaint paint; 484 paint.setAntiAlias(true); 485 paint.setColor(kControlBarBackgroundColor); 486 canvas->AsCanvasSkia()->drawPath(path, paint); 487 488 // Draw a 1-pixel gray line between the content and the menu bar. 489 int line_width = GetTotalWidth() - kLeftMargin - kRightMargin; 490 canvas->FillRectInt(kControlBarSeparatorLineColor, 491 kLeftMargin, 1 + GetShelfHeight(), line_width, 1); 492 493 View::Paint(canvas); 494 PaintBorder(canvas); 495} 496 497void BalloonViewImpl::Observe(NotificationType type, 498 const NotificationSource& source, 499 const NotificationDetails& details) { 500 if (type != NotificationType::NOTIFY_BALLOON_DISCONNECTED) { 501 NOTREACHED(); 502 return; 503 } 504 505 // If the renderer process attached to this balloon is disconnected 506 // (e.g., because of a crash), we want to close the balloon. 507 notification_registrar_.Remove(this, 508 NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon_)); 509 Close(false); 510} 511