// Copyright (c) 2011 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/ui/views/infobars/infobar_view.h"
#include "base/message_loop.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/tab_contents/infobar_delegate.h"
#include "chrome/browser/ui/views/infobars/infobar_background.h"
#include "chrome/browser/ui/views/infobars/infobar_button_border.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/animation/slide_animation.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas_skia_paint.h"
#include "views/controls/button/image_button.h"
#include "views/controls/button/menu_button.h"
#include "views/controls/button/text_button.h"
#include "views/controls/image_view.h"
#include "views/controls/label.h"
#include "views/controls/link.h"
#include "views/focus/external_focus_tracker.h"
#include "views/widget/widget.h"
#include "views/window/non_client_view.h"
#if defined(OS_WIN)
#include <shellapi.h>
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "ui/base/win/hwnd_util.h"
#include "ui/gfx/icon_util.h"
#endif
// static
const int InfoBar::kSeparatorLineHeight =
views::NonClientFrameView::kClientEdgeThickness;
const int InfoBar::kDefaultArrowTargetHeight = 9;
const int InfoBar::kMaximumArrowTargetHeight = 24;
const int InfoBar::kDefaultArrowTargetHalfWidth = kDefaultArrowTargetHeight;
const int InfoBar::kMaximumArrowTargetHalfWidth = 14;
const int InfoBar::kDefaultBarTargetHeight = 36;
const int InfoBarView::kButtonButtonSpacing = 10;
const int InfoBarView::kEndOfLabelSpacing = 16;
const int InfoBarView::kHorizontalPadding = 6;
InfoBarView::InfoBarView(InfoBarDelegate* delegate)
: InfoBar(delegate),
icon_(NULL),
close_button_(NULL),
ALLOW_THIS_IN_INITIALIZER_LIST(delete_factory_(this)),
fill_path_(new SkPath),
stroke_path_(new SkPath) {
set_parent_owned(false); // InfoBar deletes itself at the appropriate time.
set_background(new InfoBarBackground(delegate->GetInfoBarType()));
}
InfoBarView::~InfoBarView() {
}
// static
views::Label* InfoBarView::CreateLabel(const string16& text) {
views::Label* label = new views::Label(UTF16ToWideHack(text),
ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont));
label->SetColor(SK_ColorBLACK);
label->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
return label;
}
// static
views::Link* InfoBarView::CreateLink(const string16& text,
views::LinkController* controller,
const SkColor& background_color) {
views::Link* link = new views::Link;
link->SetText(UTF16ToWideHack(text));
link->SetFont(
ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont));
link->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
link->SetController(controller);
link->MakeReadableOverBackgroundColor(background_color);
return link;
}
// static
views::MenuButton* InfoBarView::CreateMenuButton(
const string16& text,
bool normal_has_border,
views::ViewMenuDelegate* menu_delegate) {
views::MenuButton* menu_button =
new views::MenuButton(NULL, UTF16ToWideHack(text), menu_delegate, true);
menu_button->set_border(new InfoBarButtonBorder);
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
menu_button->set_menu_marker(
rb.GetBitmapNamed(IDR_INFOBARBUTTON_MENU_DROPARROW));
if (normal_has_border) {
menu_button->SetNormalHasBorder(true);
menu_button->SetAnimationDuration(0);
}
menu_button->SetEnabledColor(SK_ColorBLACK);
menu_button->SetHighlightColor(SK_ColorBLACK);
menu_button->SetHoverColor(SK_ColorBLACK);
menu_button->SetFont(rb.GetFont(ResourceBundle::MediumFont));
return menu_button;
}
// static
views::TextButton* InfoBarView::CreateTextButton(
views::ButtonListener* listener,
const string16& text,
bool needs_elevation) {
views::TextButton* text_button =
new views::TextButton(listener, UTF16ToWideHack(text));
text_button->set_border(new InfoBarButtonBorder);
text_button->SetNormalHasBorder(true);
text_button->SetAnimationDuration(0);
text_button->SetEnabledColor(SK_ColorBLACK);
text_button->SetHighlightColor(SK_ColorBLACK);
text_button->SetHoverColor(SK_ColorBLACK);
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
text_button->SetFont(rb.GetFont(ResourceBundle::MediumFont));
#if defined(OS_WIN)
if (needs_elevation &&
(base::win::GetVersion() >= base::win::VERSION_VISTA) &&
base::win::UserAccountControlIsEnabled()) {
SHSTOCKICONINFO icon_info = { sizeof SHSTOCKICONINFO };
// Even with the runtime guard above, we have to use GetProcAddress() here,
// because otherwise the loader will try to resolve the function address on
// startup, which will break on XP.
typedef HRESULT (STDAPICALLTYPE *GetStockIconInfo)(SHSTOCKICONID, UINT,
SHSTOCKICONINFO*);
GetStockIconInfo func = reinterpret_cast<GetStockIconInfo>(
GetProcAddress(GetModuleHandle(L"shell32.dll"), "SHGetStockIconInfo"));
(*func)(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, &icon_info);
text_button->SetIcon(*IconUtil::CreateSkBitmapFromHICON(icon_info.hIcon,
gfx::Size(GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON))));
}
#endif
return text_button;
}
void InfoBarView::Layout() {
// Calculate the fill and stroke paths. We do this here, rather than in
// PlatformSpecificRecalculateHeight(), because this is also reached when our
// width is changed, which affects both paths.
stroke_path_->rewind();
fill_path_->rewind();
const InfoBarContainer::Delegate* delegate = container_delegate();
if (delegate) {
static_cast<InfoBarBackground*>(background())->set_separator_color(
delegate->GetInfoBarSeparatorColor());
int arrow_x;
SkScalar arrow_fill_height =
SkIntToScalar(std::max(arrow_height() - kSeparatorLineHeight, 0));
SkScalar arrow_fill_half_width = SkIntToScalar(arrow_half_width());
SkScalar separator_height = SkIntToScalar(kSeparatorLineHeight);
if (delegate->DrawInfoBarArrows(&arrow_x) && arrow_fill_height) {
// Skia pixel centers are at the half-values, so the arrow is horizontally
// centered at |arrow_x| + 0.5. Vertically, the stroke path is the center
// of the separator, while the fill path is a closed path that extends up
// through the entire height of the separator and down to the bottom of
// the arrow where it joins the bar.
stroke_path_->moveTo(
SkIntToScalar(arrow_x) + SK_ScalarHalf - arrow_fill_half_width,
SkIntToScalar(arrow_height()) - (separator_height * SK_ScalarHalf));
stroke_path_->rLineTo(arrow_fill_half_width, -arrow_fill_height);
stroke_path_->rLineTo(arrow_fill_half_width, arrow_fill_height);
*fill_path_ = *stroke_path_;
// Move the top of the fill path up to the top of the separator and then
// extend it down all the way through.
fill_path_->offset(0, -separator_height * SK_ScalarHalf);
// This 0.01 hack prevents the fill from filling more pixels on the right
// edge of the arrow than on the left.
const SkScalar epsilon = 0.01f;
fill_path_->rLineTo(-epsilon, 0);
fill_path_->rLineTo(0, separator_height);
fill_path_->rLineTo(epsilon - (arrow_fill_half_width * 2), 0);
fill_path_->close();
}
}
if (bar_height()) {
fill_path_->addRect(0.0, SkIntToScalar(arrow_height()),
SkIntToScalar(width()), SkIntToScalar(height() - kSeparatorLineHeight));
}
int start_x = kHorizontalPadding;
if (icon_ != NULL) {
gfx::Size icon_size = icon_->GetPreferredSize();
icon_->SetBounds(start_x, OffsetY(icon_size), icon_size.width(),
icon_size.height());
}
gfx::Size button_size = close_button_->GetPreferredSize();
close_button_->SetBounds(std::max(start_x + ContentMinimumWidth(),
width() - kHorizontalPadding - button_size.width()), OffsetY(button_size),
button_size.width(), button_size.height());
}
void InfoBarView::ViewHierarchyChanged(bool is_add, View* parent, View* child) {
View::ViewHierarchyChanged(is_add, parent, child);
if (child == this) {
if (is_add) {
#if defined(OS_WIN)
// When we're added to a view hierarchy within a widget, we create an
// external focus tracker to track what was focused in case we obtain
// focus so that we can restore focus when we're removed.
views::Widget* widget = GetWidget();
if (widget) {
focus_tracker_.reset(
new views::ExternalFocusTracker(this, GetFocusManager()));
}
#endif
if (GetFocusManager())
GetFocusManager()->AddFocusChangeListener(this);
if (GetWidget()) {
GetWidget()->NotifyAccessibilityEvent(
this, ui::AccessibilityTypes::EVENT_ALERT, true);
}
if (close_button_ == NULL) {
SkBitmap* image = delegate()->GetIcon();
if (image) {
icon_ = new views::ImageView;
icon_->SetImage(image);
AddChildView(icon_);
}
close_button_ = new views::ImageButton(this);
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
close_button_->SetImage(views::CustomButton::BS_NORMAL,
rb.GetBitmapNamed(IDR_CLOSE_BAR));
close_button_->SetImage(views::CustomButton::BS_HOT,
rb.GetBitmapNamed(IDR_CLOSE_BAR_H));
close_button_->SetImage(views::CustomButton::BS_PUSHED,
rb.GetBitmapNamed(IDR_CLOSE_BAR_P));
close_button_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
close_button_->SetFocusable(true);
AddChildView(close_button_);
}
} else {
DestroyFocusTracker(false);
animation()->Stop();
// Finally, clean ourselves up when we're removed from the view hierarchy
// since no-one refers to us now.
MessageLoop::current()->PostTask(FROM_HERE,
delete_factory_.NewRunnableMethod(&InfoBarView::DeleteSelf));
if (GetFocusManager())
GetFocusManager()->RemoveFocusChangeListener(this);
}
}
// For accessibility, ensure the close button is the last child view.
if ((close_button_ != NULL) && (parent == this) && (child != close_button_) &&
(close_button_->parent() == this) &&
(GetChildViewAt(child_count() - 1) != close_button_)) {
RemoveChildView(close_button_);
AddChildView(close_button_);
}
}
void InfoBarView::PaintChildren(gfx::Canvas* canvas) {
canvas->Save();
// TODO(scr): This really should be the |fill_path_|, but the clipPath seems
// broken on non-Windows platforms (crbug.com/75154). For now, just clip to
// the bar bounds.
//
// gfx::CanvasSkia* canvas_skia = canvas->AsCanvasSkia();
// canvas_skia->clipPath(*fill_path_);
DCHECK_EQ(total_height(), height())
<< "Infobar piecewise heights do not match overall height";
canvas->ClipRectInt(0, arrow_height(), width(), bar_height());
views::View::PaintChildren(canvas);
canvas->Restore();
}
void InfoBarView::ButtonPressed(views::Button* sender,
const views::Event& event) {
if (sender == close_button_) {
if (delegate())
delegate()->InfoBarDismissed();
RemoveInfoBar();
}
}
int InfoBarView::ContentMinimumWidth() const {
return 0;
}
int InfoBarView::StartX() const {
// Ensure we don't return a value greater than EndX(), so children can safely
// set something's width to "EndX() - StartX()" without risking that being
// negative.
return std::min(EndX(),
((icon_ != NULL) ? icon_->bounds().right() : 0) + kHorizontalPadding);
}
int InfoBarView::EndX() const {
const int kCloseButtonSpacing = 12;
return close_button_->x() - kCloseButtonSpacing;
}
const InfoBarContainer::Delegate* InfoBarView::container_delegate() const {
const InfoBarContainer* infobar_container = container();
return infobar_container ? infobar_container->delegate() : NULL;
}
void InfoBarView::PlatformSpecificHide(bool animate) {
if (!animate)
return;
bool restore_focus = true;
#if defined(OS_WIN)
// Do not restore focus (and active state with it) on Windows if some other
// top-level window became active.
if (GetWidget() &&
!ui::DoesWindowBelongToActiveWindow(GetWidget()->GetNativeView()))
restore_focus = false;
#endif // defined(OS_WIN)
DestroyFocusTracker(restore_focus);
}
void InfoBarView::PlatformSpecificOnHeightsRecalculated() {
// Ensure that notifying our container of our size change will result in a
// re-layout.
InvalidateLayout();
}
void InfoBarView::GetAccessibleState(ui::AccessibleViewState* state) {
if (delegate()) {
state->name = l10n_util::GetStringUTF16(
(delegate()->GetInfoBarType() == InfoBarDelegate::WARNING_TYPE) ?
IDS_ACCNAME_INFOBAR_WARNING : IDS_ACCNAME_INFOBAR_PAGE_ACTION);
}
state->role = ui::AccessibilityTypes::ROLE_ALERT;
}
gfx::Size InfoBarView::GetPreferredSize() {
return gfx::Size(0, total_height());
}
void InfoBarView::FocusWillChange(View* focused_before, View* focused_now) {
// This will trigger some screen readers to read the entire contents of this
// infobar.
if (focused_before && focused_now && !this->Contains(focused_before) &&
this->Contains(focused_now) && GetWidget()) {
GetWidget()->NotifyAccessibilityEvent(
this, ui::AccessibilityTypes::EVENT_ALERT, true);
}
}
void InfoBarView::DestroyFocusTracker(bool restore_focus) {
if (focus_tracker_ != NULL) {
if (restore_focus)
focus_tracker_->FocusLastFocusedExternalView();
focus_tracker_->SetFocusManager(NULL);
focus_tracker_.reset();
}
}
void InfoBarView::DeleteSelf() {
delete this;
}