blob: 43ed162cd8d66c34b1cdd0c94207d34135af624f [file] [log] [blame]
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/views/notifications/balloon_view.h"
#include <vector>
#include "app/l10n_util.h"
#include "app/resource_bundle.h"
#include "base/message_loop.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/notifications/balloon.h"
#include "chrome/browser/notifications/balloon_collection.h"
#include "chrome/browser/notifications/desktop_notification_service.h"
#include "chrome/browser/notifications/notification.h"
#include "chrome/browser/notifications/notification_options_menu_model.h"
#include "chrome/browser/renderer_host/render_view_host.h"
#include "chrome/browser/renderer_host/render_widget_host_view.h"
#include "chrome/browser/themes/browser_theme_provider.h"
#include "chrome/browser/views/bubble_border.h"
#include "chrome/browser/views/notifications/balloon_view_host.h"
#include "chrome/common/notification_details.h"
#include "chrome/common/notification_source.h"
#include "chrome/common/notification_type.h"
#include "gfx/canvas_skia.h"
#include "gfx/insets.h"
#include "gfx/native_widget_types.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/animation/slide_animation.h"
#include "views/controls/button/button.h"
#include "views/controls/button/image_button.h"
#include "views/controls/button/text_button.h"
#include "views/controls/menu/menu_2.h"
#include "views/controls/native/native_view_host.h"
#include "views/painter.h"
#include "views/widget/root_view.h"
#if defined(OS_WIN)
#include "views/widget/widget_win.h"
#endif
#if defined(OS_LINUX)
#include "views/widget/widget_gtk.h"
#endif
using views::Widget;
namespace {
const int kTopMargin = 2;
const int kBottomMargin = 0;
const int kLeftMargin = 4;
const int kRightMargin = 4;
const int kShelfBorderTopOverlap = 0;
// Properties of the dismiss button.
const int kDismissButtonWidth = 14;
const int kDismissButtonHeight = 14;
const int kDismissButtonTopMargin = 6;
const int kDismissButtonRightMargin = 6;
// Properties of the options menu.
const int kOptionsButtonWidth = 21;
const int kOptionsButtonHeight = 14;
const int kOptionsButtonTopMargin = 5;
const int kOptionsButtonRightMargin = 4;
// Properties of the origin label.
const int kLabelLeftMargin = 10;
const int kLabelTopMargin = 6;
// Size of the drop shadow. The shadow is provided by BubbleBorder,
// not this class.
const int kLeftShadowWidth = 0;
const int kRightShadowWidth = 0;
const int kTopShadowWidth = 0;
const int kBottomShadowWidth = 6;
// Optional animation.
const bool kAnimateEnabled = true;
// The shelf height for the system default font size. It is scaled
// with changes in the default font size.
const int kDefaultShelfHeight = 22;
// Menu commands
const int kRevokePermissionCommand = 0;
// Colors
const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245);
const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125);
const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180);
} // namespace
BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection)
: balloon_(NULL),
collection_(collection),
frame_container_(NULL),
html_container_(NULL),
html_contents_(NULL),
method_factory_(this),
close_button_(NULL),
animation_(NULL),
options_menu_model_(NULL),
options_menu_menu_(NULL),
options_menu_button_(NULL) {
// This object is not to be deleted by the views hierarchy,
// as it is owned by the balloon.
set_parent_owned(false);
BubbleBorder* bubble_border = new BubbleBorder(BubbleBorder::FLOAT);
set_border(bubble_border);
}
BalloonViewImpl::~BalloonViewImpl() {
}
void BalloonViewImpl::Close(bool by_user) {
MessageLoop::current()->PostTask(FROM_HERE,
method_factory_.NewRunnableMethod(
&BalloonViewImpl::DelayedClose, by_user));
}
gfx::Size BalloonViewImpl::GetSize() const {
// BalloonView has no size if it hasn't been shown yet (which is when
// balloon_ is set).
if (!balloon_)
return gfx::Size(0, 0);
return gfx::Size(GetTotalWidth(), GetTotalHeight());
}
BalloonHost* BalloonViewImpl::GetHost() const {
return html_contents_.get();
}
void BalloonViewImpl::RunMenu(views::View* source, const gfx::Point& pt) {
RunOptionsMenu(pt);
}
void BalloonViewImpl::DisplayChanged() {
collection_->DisplayChanged();
}
void BalloonViewImpl::WorkAreaChanged() {
collection_->DisplayChanged();
}
void BalloonViewImpl::ButtonPressed(views::Button* sender,
const views::Event&) {
// The only button currently is the close button.
DCHECK(sender == close_button_);
Close(true);
}
void BalloonViewImpl::DelayedClose(bool by_user) {
html_contents_->Shutdown();
html_container_->CloseNow();
// The BalloonViewImpl has to be detached from frame_container_ now
// because CloseNow on linux/views destroys the view hierachy
// asynchronously.
frame_container_->GetRootView()->RemoveAllChildViews(true);
frame_container_->CloseNow();
balloon_->OnClose(by_user);
}
void BalloonViewImpl::DidChangeBounds(const gfx::Rect& previous,
const gfx::Rect& current) {
SizeContentsWindow();
}
gfx::Size BalloonViewImpl::GetPreferredSize() {
return gfx::Size(1000, 1000);
}
void BalloonViewImpl::SizeContentsWindow() {
if (!html_container_ || !frame_container_)
return;
gfx::Rect contents_rect = GetContentsRectangle();
html_container_->SetBounds(contents_rect);
html_container_->MoveAbove(frame_container_);
gfx::Path path;
GetContentsMask(contents_rect, &path);
html_container_->SetShape(path.CreateNativeRegion());
close_button_->SetBounds(GetCloseButtonBounds());
options_menu_button_->SetBounds(GetOptionsButtonBounds());
source_label_->SetBounds(GetLabelBounds());
}
void BalloonViewImpl::RepositionToBalloon() {
DCHECK(frame_container_);
DCHECK(html_container_);
DCHECK(balloon_);
if (!kAnimateEnabled) {
frame_container_->SetBounds(
gfx::Rect(balloon_->GetPosition().x(), balloon_->GetPosition().y(),
GetTotalWidth(), GetTotalHeight()));
gfx::Rect contents_rect = GetContentsRectangle();
html_container_->SetBounds(contents_rect);
html_contents_->SetPreferredSize(contents_rect.size());
RenderWidgetHostView* view = html_contents_->render_view_host()->view();
if (view)
view->SetSize(contents_rect.size());
return;
}
anim_frame_end_ = gfx::Rect(
balloon_->GetPosition().x(), balloon_->GetPosition().y(),
GetTotalWidth(), GetTotalHeight());
frame_container_->GetBounds(&anim_frame_start_, false);
animation_.reset(new ui::SlideAnimation(this));
animation_->Show();
}
void BalloonViewImpl::Update() {
DCHECK(html_contents_.get()) << "BalloonView::Update called before Show";
if (html_contents_->render_view_host())
html_contents_->render_view_host()->NavigateToURL(
balloon_->notification().content_url());
}
void BalloonViewImpl::AnimationProgressed(const ui::Animation* animation) {
DCHECK(animation == animation_.get());
// Linear interpolation from start to end position.
double e = animation->GetCurrentValue();
double s = (1.0 - e);
gfx::Rect frame_position(
static_cast<int>(s * anim_frame_start_.x() +
e * anim_frame_end_.x()),
static_cast<int>(s * anim_frame_start_.y() +
e * anim_frame_end_.y()),
static_cast<int>(s * anim_frame_start_.width() +
e * anim_frame_end_.width()),
static_cast<int>(s * anim_frame_start_.height() +
e * anim_frame_end_.height()));
frame_container_->SetBounds(frame_position);
gfx::Path path;
gfx::Rect contents_rect = GetContentsRectangle();
html_container_->SetBounds(contents_rect);
GetContentsMask(contents_rect, &path);
html_container_->SetShape(path.CreateNativeRegion());
html_contents_->SetPreferredSize(contents_rect.size());
RenderWidgetHostView* view = html_contents_->render_view_host()->view();
if (view)
view->SetSize(contents_rect.size());
}
gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const {
return gfx::Rect(
width() - kDismissButtonWidth -
kDismissButtonRightMargin - kRightShadowWidth,
kDismissButtonTopMargin,
kDismissButtonWidth,
kDismissButtonHeight);
}
gfx::Rect BalloonViewImpl::GetOptionsButtonBounds() const {
gfx::Rect close_rect = GetCloseButtonBounds();
return gfx::Rect(
close_rect.x() - kOptionsButtonWidth - kOptionsButtonRightMargin,
kOptionsButtonTopMargin,
kOptionsButtonWidth,
kOptionsButtonHeight);
}
gfx::Rect BalloonViewImpl::GetLabelBounds() const {
return gfx::Rect(
kLeftShadowWidth + kLabelLeftMargin,
kLabelTopMargin,
std::max(0, width() - kOptionsButtonWidth -
kRightMargin),
kOptionsButtonHeight);
}
void BalloonViewImpl::Show(Balloon* balloon) {
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
balloon_ = balloon;
SetBounds(balloon_->GetPosition().x(), balloon_->GetPosition().y(),
GetTotalWidth(), GetTotalHeight());
const string16 source_label_text = l10n_util::GetStringFUTF16(
IDS_NOTIFICATION_BALLOON_SOURCE_LABEL,
balloon->notification().display_source());
source_label_ = new views::Label(UTF16ToWide(source_label_text));
AddChildView(source_label_);
options_menu_button_ = new views::MenuButton(NULL, L"", this, false);
AddChildView(options_menu_button_);
close_button_ = new views::ImageButton(this);
close_button_->SetTooltipText(UTF16ToWide(l10n_util::GetStringUTF16(
IDS_NOTIFICATION_BALLOON_DISMISS_LABEL)));
AddChildView(close_button_);
// We have to create two windows: one for the contents and one for the
// frame. Why?
// * The contents is an html window which cannot be a
// layered window (because it may have child windows for instance).
// * The frame is a layered window so that we can have nicely rounded
// corners using alpha blending (and we may do other alpha blending
// effects).
// Unfortunately, layered windows cannot have child windows. (Well, they can
// but the child windows don't render).
//
// We carefully keep these two windows in sync to present the illusion of
// one window to the user.
//
// We don't let the OS manage the RTL layout of these widgets, because
// this code is already taking care of correctly reversing the layout.
gfx::Rect contents_rect = GetContentsRectangle();
html_contents_.reset(new BalloonViewHost(balloon));
html_contents_->SetPreferredSize(gfx::Size(10000, 10000));
html_container_ = Widget::CreatePopupWidget(Widget::NotTransparent,
Widget::AcceptEvents,
Widget::DeleteOnDestroy,
Widget::DontMirrorOriginInRTL);
html_container_->SetAlwaysOnTop(true);
html_container_->Init(NULL, contents_rect);
html_container_->SetContentsView(html_contents_->view());
gfx::Rect balloon_rect(x(), y(), GetTotalWidth(), GetTotalHeight());
frame_container_ = Widget::CreatePopupWidget(Widget::Transparent,
Widget::AcceptEvents,
Widget::DeleteOnDestroy,
Widget::DontMirrorOriginInRTL);
frame_container_->SetWidgetDelegate(this);
frame_container_->SetAlwaysOnTop(true);
frame_container_->Init(NULL, balloon_rect);
frame_container_->SetContentsView(this);
frame_container_->MoveAbove(html_container_);
close_button_->SetImage(views::CustomButton::BS_NORMAL,
rb.GetBitmapNamed(IDR_TAB_CLOSE));
close_button_->SetImage(views::CustomButton::BS_HOT,
rb.GetBitmapNamed(IDR_TAB_CLOSE_H));
close_button_->SetImage(views::CustomButton::BS_PUSHED,
rb.GetBitmapNamed(IDR_TAB_CLOSE_P));
close_button_->SetBounds(GetCloseButtonBounds());
close_button_->SetBackground(SK_ColorBLACK,
rb.GetBitmapNamed(IDR_TAB_CLOSE),
rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK));
options_menu_button_->SetIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH));
options_menu_button_->SetHoverIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH_H));
options_menu_button_->SetPushedIcon(*rb.GetBitmapNamed(IDR_BALLOON_WRENCH_P));
options_menu_button_->set_alignment(views::TextButton::ALIGN_CENTER);
options_menu_button_->set_border(NULL);
options_menu_button_->SetBounds(GetOptionsButtonBounds());
source_label_->SetFont(rb.GetFont(ResourceBundle::SmallFont));
source_label_->SetColor(kControlBarTextColor);
source_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
source_label_->SetBounds(GetLabelBounds());
SizeContentsWindow();
html_container_->Show();
frame_container_->Show();
notification_registrar_.Add(this,
NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon));
}
void BalloonViewImpl::RunOptionsMenu(const gfx::Point& pt) {
CreateOptionsMenu();
options_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT);
}
void BalloonViewImpl::CreateOptionsMenu() {
if (options_menu_model_.get())
return;
options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_));
options_menu_menu_.reset(new views::Menu2(options_menu_model_.get()));
}
void BalloonViewImpl::GetContentsMask(const gfx::Rect& rect,
gfx::Path* path) const {
// This rounds the corners, and we also cut out a circle for the close
// button, since we can't guarantee the ordering of two top-most windows.
SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius());
SkScalar spline_radius = radius -
SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3);
SkScalar left = SkIntToScalar(0);
SkScalar top = SkIntToScalar(0);
SkScalar right = SkIntToScalar(rect.width());
SkScalar bottom = SkIntToScalar(rect.height());
path->moveTo(left, top);
path->lineTo(right, top);
path->lineTo(right, bottom - radius);
path->cubicTo(right, bottom - spline_radius,
right - spline_radius, bottom,
right - radius, bottom);
path->lineTo(left + radius, bottom);
path->cubicTo(left + spline_radius, bottom,
left, bottom - spline_radius,
left, bottom - radius);
path->lineTo(left, top);
path->close();
}
void BalloonViewImpl::GetFrameMask(const gfx::Rect& rect,
gfx::Path* path) const {
SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius());
SkScalar spline_radius = radius -
SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3);
SkScalar left = SkIntToScalar(rect.x());
SkScalar top = SkIntToScalar(rect.y());
SkScalar right = SkIntToScalar(rect.right());
SkScalar bottom = SkIntToScalar(rect.bottom());
path->moveTo(left, bottom);
path->lineTo(left, top + radius);
path->cubicTo(left, top + spline_radius,
left + spline_radius, top,
left + radius, top);
path->lineTo(right - radius, top);
path->cubicTo(right - spline_radius, top,
right, top + spline_radius,
right, top + radius);
path->lineTo(right, bottom);
path->lineTo(left, bottom);
path->close();
}
gfx::Point BalloonViewImpl::GetContentsOffset() const {
return gfx::Point(kLeftShadowWidth + kLeftMargin,
kTopShadowWidth + kTopMargin);
}
int BalloonViewImpl::GetShelfHeight() const {
// TODO(johnnyg): add scaling here.
return kDefaultShelfHeight;
}
int BalloonViewImpl::GetBalloonFrameHeight() const {
return GetTotalHeight() - GetShelfHeight();
}
int BalloonViewImpl::GetTotalWidth() const {
return balloon_->content_size().width()
+ kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
}
int BalloonViewImpl::GetTotalHeight() const {
return balloon_->content_size().height()
+ kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth
+ GetShelfHeight();
}
gfx::Rect BalloonViewImpl::GetContentsRectangle() const {
if (!frame_container_)
return gfx::Rect();
gfx::Size content_size = balloon_->content_size();
gfx::Point offset = GetContentsOffset();
gfx::Rect frame_rect;
frame_container_->GetBounds(&frame_rect, true);
return gfx::Rect(frame_rect.x() + offset.x(),
frame_rect.y() + GetShelfHeight() + offset.y(),
content_size.width(),
content_size.height());
}
void BalloonViewImpl::Paint(gfx::Canvas* canvas) {
DCHECK(canvas);
// Paint the menu bar area white, with proper rounded corners.
gfx::Path path;
gfx::Rect rect = GetLocalBounds(false);
rect.set_height(GetShelfHeight());
GetFrameMask(rect, &path);
SkPaint paint;
paint.setAntiAlias(true);
paint.setColor(kControlBarBackgroundColor);
canvas->AsCanvasSkia()->drawPath(path, paint);
// Draw a 1-pixel gray line between the content and the menu bar.
int line_width = GetTotalWidth() - kLeftMargin - kRightMargin;
canvas->FillRectInt(kControlBarSeparatorLineColor,
kLeftMargin, 1 + GetShelfHeight(), line_width, 1);
View::Paint(canvas);
PaintBorder(canvas);
}
void BalloonViewImpl::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
if (type != NotificationType::NOTIFY_BALLOON_DISCONNECTED) {
NOTREACHED();
return;
}
// If the renderer process attached to this balloon is disconnected
// (e.g., because of a crash), we want to close the balloon.
notification_registrar_.Remove(this,
NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon_));
Close(false);
}