infobar_view.cc revision 0529e5d033099cbfc42635f6f6183833b09dff6e
1// Copyright (c) 2012 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/infobars/infobar_view.h" 6 7#if defined(OS_WIN) 8#include <shellapi.h> 9#endif 10 11#include <algorithm> 12 13#include "base/memory/scoped_ptr.h" 14#include "base/strings/utf_string_conversions.h" 15#include "chrome/browser/ui/views/infobars/infobar_background.h" 16#include "components/infobars/core/infobar_delegate.h" 17#include "grit/generated_resources.h" 18#include "grit/theme_resources.h" 19#include "grit/ui_resources.h" 20#include "third_party/skia/include/effects/SkGradientShader.h" 21#include "ui/accessibility/ax_view_state.h" 22#include "ui/base/l10n/l10n_util.h" 23#include "ui/base/resource/resource_bundle.h" 24#include "ui/gfx/canvas.h" 25#include "ui/gfx/image/image.h" 26#include "ui/views/controls/button/image_button.h" 27#include "ui/views/controls/button/label_button.h" 28#include "ui/views/controls/button/label_button_border.h" 29#include "ui/views/controls/button/menu_button.h" 30#include "ui/views/controls/image_view.h" 31#include "ui/views/controls/label.h" 32#include "ui/views/controls/link.h" 33#include "ui/views/controls/menu/menu_runner.h" 34#include "ui/views/layout/layout_constants.h" 35#include "ui/views/widget/widget.h" 36#include "ui/views/window/non_client_view.h" 37 38#if defined(OS_WIN) 39#include "base/win/win_util.h" 40#include "base/win/windows_version.h" 41#include "ui/gfx/icon_util.h" 42#include "ui/gfx/win/hwnd_util.h" 43#endif 44 45 46// Helpers -------------------------------------------------------------------- 47 48namespace { 49 50const int kEdgeItemPadding = views::kRelatedControlHorizontalSpacing; 51const int kIconToLabelSpacing = views::kRelatedControlHorizontalSpacing; 52const int kBeforeCloseButtonSpacing = views::kUnrelatedControlHorizontalSpacing; 53 54bool SortLabelsByDecreasingWidth(views::Label* label_1, views::Label* label_2) { 55 return label_1->GetPreferredSize().width() > 56 label_2->GetPreferredSize().width(); 57} 58 59} // namespace 60 61 62// InfoBar -------------------------------------------------------------------- 63 64// static 65const int infobars::InfoBar::kSeparatorLineHeight = 66 views::NonClientFrameView::kClientEdgeThickness; 67const int infobars::InfoBar::kDefaultArrowTargetHeight = 9; 68const int infobars::InfoBar::kMaximumArrowTargetHeight = 24; 69const int infobars::InfoBar::kDefaultArrowTargetHalfWidth = 70 kDefaultArrowTargetHeight; 71const int infobars::InfoBar::kMaximumArrowTargetHalfWidth = 14; 72const int infobars::InfoBar::kDefaultBarTargetHeight = 36; 73 74// InfoBarView ---------------------------------------------------------------- 75 76// static 77const int InfoBarView::kButtonButtonSpacing = views::kRelatedButtonHSpacing; 78const int InfoBarView::kEndOfLabelSpacing = views::kItemLabelSpacing; 79 80InfoBarView::InfoBarView(scoped_ptr<infobars::InfoBarDelegate> delegate) 81 : infobars::InfoBar(delegate.Pass()), 82 views::ExternalFocusTracker(this, NULL), 83 icon_(NULL), 84 close_button_(NULL) { 85 set_owned_by_client(); // InfoBar deletes itself at the appropriate time. 86 set_background( 87 new InfoBarBackground(infobars::InfoBar::delegate()->GetInfoBarType())); 88} 89 90InfoBarView::~InfoBarView() { 91 // We should have closed any open menus in PlatformSpecificHide(), then 92 // subclasses' RunMenu() functions should have prevented opening any new ones 93 // once we became unowned. 94 DCHECK(!menu_runner_.get()); 95} 96 97views::Label* InfoBarView::CreateLabel(const base::string16& text) const { 98 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 99 views::Label* label = new views::Label( 100 text, rb.GetFontList(ui::ResourceBundle::MediumFont)); 101 label->SizeToPreferredSize(); 102 label->SetBackgroundColor(background()->get_color()); 103 label->SetEnabledColor(SK_ColorBLACK); 104 label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 105 return label; 106} 107 108views::Link* InfoBarView::CreateLink(const base::string16& text, 109 views::LinkListener* listener) const { 110 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 111 views::Link* link = new views::Link(text); 112 link->SetFontList(rb.GetFontList(ui::ResourceBundle::MediumFont)); 113 link->SizeToPreferredSize(); 114 link->SetHorizontalAlignment(gfx::ALIGN_LEFT); 115 link->set_listener(listener); 116 link->SetBackgroundColor(background()->get_color()); 117 return link; 118} 119 120// static 121views::MenuButton* InfoBarView::CreateMenuButton( 122 const base::string16& text, 123 views::MenuButtonListener* menu_button_listener) { 124 scoped_ptr<views::TextButtonDefaultBorder> menu_button_border( 125 new views::TextButtonDefaultBorder()); 126 const int kNormalImageSet[] = IMAGE_GRID(IDR_INFOBARBUTTON_NORMAL); 127 menu_button_border->set_normal_painter( 128 views::Painter::CreateImageGridPainter(kNormalImageSet)); 129 const int kHotImageSet[] = IMAGE_GRID(IDR_INFOBARBUTTON_HOVER); 130 menu_button_border->set_hot_painter( 131 views::Painter::CreateImageGridPainter(kHotImageSet)); 132 const int kPushedImageSet[] = IMAGE_GRID(IDR_INFOBARBUTTON_PRESSED); 133 menu_button_border->set_pushed_painter( 134 views::Painter::CreateImageGridPainter(kPushedImageSet)); 135 136 views::MenuButton* menu_button = new views::MenuButton( 137 NULL, text, menu_button_listener, true); 138 menu_button->SetBorder(menu_button_border.PassAs<views::Border>()); 139 menu_button->set_animate_on_state_change(false); 140 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 141 menu_button->set_menu_marker( 142 rb.GetImageNamed(IDR_INFOBARBUTTON_MENU_DROPARROW).ToImageSkia()); 143 menu_button->SetEnabledColor(SK_ColorBLACK); 144 menu_button->SetHoverColor(SK_ColorBLACK); 145 menu_button->SetFontList(rb.GetFontList(ui::ResourceBundle::MediumFont)); 146 menu_button->SizeToPreferredSize(); 147 menu_button->SetFocusable(true); 148 return menu_button; 149} 150 151// static 152views::LabelButton* InfoBarView::CreateLabelButton( 153 views::ButtonListener* listener, 154 const base::string16& text, 155 bool needs_elevation) { 156 scoped_ptr<views::LabelButtonBorder> label_button_border( 157 new views::LabelButtonBorder(views::Button::STYLE_TEXTBUTTON)); 158 const int kNormalImageSet[] = IMAGE_GRID(IDR_INFOBARBUTTON_NORMAL); 159 label_button_border->SetPainter( 160 false, views::Button::STATE_NORMAL, 161 views::Painter::CreateImageGridPainter(kNormalImageSet)); 162 const int kHoveredImageSet[] = IMAGE_GRID(IDR_INFOBARBUTTON_HOVER); 163 label_button_border->SetPainter( 164 false, views::Button::STATE_HOVERED, 165 views::Painter::CreateImageGridPainter(kHoveredImageSet)); 166 const int kPressedImageSet[] = IMAGE_GRID(IDR_INFOBARBUTTON_PRESSED); 167 label_button_border->SetPainter( 168 false, views::Button::STATE_PRESSED, 169 views::Painter::CreateImageGridPainter(kPressedImageSet)); 170 171 views::LabelButton* label_button = new views::LabelButton(listener, text); 172 label_button->SetBorder(label_button_border.PassAs<views::Border>()); 173 label_button->set_animate_on_state_change(false); 174 label_button->SetTextColor(views::Button::STATE_NORMAL, SK_ColorBLACK); 175 label_button->SetTextColor(views::Button::STATE_HOVERED, SK_ColorBLACK); 176 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 177 label_button->SetFontList(rb.GetFontList(ui::ResourceBundle::MediumFont)); 178#if defined(OS_WIN) 179 if (needs_elevation && 180 (base::win::GetVersion() >= base::win::VERSION_VISTA) && 181 base::win::UserAccountControlIsEnabled()) { 182 SHSTOCKICONINFO icon_info = { sizeof(SHSTOCKICONINFO) }; 183 // Even with the runtime guard above, we have to use GetProcAddress() here, 184 // because otherwise the loader will try to resolve the function address on 185 // startup, which will break on XP. 186 typedef HRESULT (STDAPICALLTYPE *GetStockIconInfo)(SHSTOCKICONID, UINT, 187 SHSTOCKICONINFO*); 188 GetStockIconInfo func = reinterpret_cast<GetStockIconInfo>( 189 GetProcAddress(GetModuleHandle(L"shell32.dll"), "SHGetStockIconInfo")); 190 if (SUCCEEDED((*func)(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, 191 &icon_info))) { 192 scoped_ptr<SkBitmap> icon(IconUtil::CreateSkBitmapFromHICON( 193 icon_info.hIcon, gfx::Size(GetSystemMetrics(SM_CXSMICON), 194 GetSystemMetrics(SM_CYSMICON)))); 195 if (icon.get()) { 196 label_button->SetImage(views::Button::STATE_NORMAL, 197 gfx::ImageSkia::CreateFrom1xBitmap(*icon)); 198 } 199 DestroyIcon(icon_info.hIcon); 200 } 201 } 202#endif 203 label_button->SizeToPreferredSize(); 204 label_button->SetFocusable(true); 205 return label_button; 206} 207 208// static 209void InfoBarView::AssignWidths(Labels* labels, int available_width) { 210 std::sort(labels->begin(), labels->end(), SortLabelsByDecreasingWidth); 211 AssignWidthsSorted(labels, available_width); 212} 213 214void InfoBarView::Layout() { 215 // Calculate the fill and stroke paths. We do this here, rather than in 216 // PlatformSpecificRecalculateHeight(), because this is also reached when our 217 // width is changed, which affects both paths. 218 stroke_path_.rewind(); 219 fill_path_.rewind(); 220 const infobars::InfoBarContainer::Delegate* delegate = container_delegate(); 221 if (delegate) { 222 static_cast<InfoBarBackground*>(background())->set_separator_color( 223 delegate->GetInfoBarSeparatorColor()); 224 int arrow_x; 225 SkScalar arrow_fill_height = 226 SkIntToScalar(std::max(arrow_height() - kSeparatorLineHeight, 0)); 227 SkScalar arrow_fill_half_width = SkIntToScalar(arrow_half_width()); 228 SkScalar separator_height = SkIntToScalar(kSeparatorLineHeight); 229 if (delegate->DrawInfoBarArrows(&arrow_x) && arrow_fill_height) { 230 // Skia pixel centers are at the half-values, so the arrow is horizontally 231 // centered at |arrow_x| + 0.5. Vertically, the stroke path is the center 232 // of the separator, while the fill path is a closed path that extends up 233 // through the entire height of the separator and down to the bottom of 234 // the arrow where it joins the bar. 235 stroke_path_.moveTo( 236 SkIntToScalar(arrow_x) + SK_ScalarHalf - arrow_fill_half_width, 237 SkIntToScalar(arrow_height()) - (separator_height * SK_ScalarHalf)); 238 stroke_path_.rLineTo(arrow_fill_half_width, -arrow_fill_height); 239 stroke_path_.rLineTo(arrow_fill_half_width, arrow_fill_height); 240 241 fill_path_ = stroke_path_; 242 // Move the top of the fill path up to the top of the separator and then 243 // extend it down all the way through. 244 fill_path_.offset(0, -separator_height * SK_ScalarHalf); 245 // This 0.01 hack prevents the fill from filling more pixels on the right 246 // edge of the arrow than on the left. 247 const SkScalar epsilon = 0.01f; 248 fill_path_.rLineTo(-epsilon, 0); 249 fill_path_.rLineTo(0, separator_height); 250 fill_path_.rLineTo(epsilon - (arrow_fill_half_width * 2), 0); 251 fill_path_.close(); 252 } 253 } 254 if (bar_height()) { 255 fill_path_.addRect(0.0, SkIntToScalar(arrow_height()), 256 SkIntToScalar(width()), SkIntToScalar(height() - kSeparatorLineHeight)); 257 } 258 259 int start_x = kEdgeItemPadding; 260 if (icon_ != NULL) { 261 icon_->SetPosition(gfx::Point(start_x, OffsetY(icon_))); 262 start_x = icon_->bounds().right() + kIconToLabelSpacing; 263 } 264 265 int content_minimum_width = ContentMinimumWidth(); 266 close_button_->SetPosition(gfx::Point( 267 std::max( 268 start_x + content_minimum_width + 269 ((content_minimum_width > 0) ? kBeforeCloseButtonSpacing : 0), 270 width() - kEdgeItemPadding - close_button_->width()), 271 OffsetY(close_button_))); 272} 273 274void InfoBarView::ViewHierarchyChanged( 275 const ViewHierarchyChangedDetails& details) { 276 View::ViewHierarchyChanged(details); 277 278 if (details.is_add && (details.child == this) && (close_button_ == NULL)) { 279 gfx::Image image = delegate()->GetIcon(); 280 if (!image.IsEmpty()) { 281 icon_ = new views::ImageView; 282 icon_->SetImage(image.ToImageSkia()); 283 icon_->SizeToPreferredSize(); 284 AddChildView(icon_); 285 } 286 287 close_button_ = new views::ImageButton(this); 288 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 289 close_button_->SetImage(views::CustomButton::STATE_NORMAL, 290 rb.GetImageNamed(IDR_CLOSE_1).ToImageSkia()); 291 close_button_->SetImage(views::CustomButton::STATE_HOVERED, 292 rb.GetImageNamed(IDR_CLOSE_1_H).ToImageSkia()); 293 close_button_->SetImage(views::CustomButton::STATE_PRESSED, 294 rb.GetImageNamed(IDR_CLOSE_1_P).ToImageSkia()); 295 close_button_->SizeToPreferredSize(); 296 close_button_->SetAccessibleName( 297 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); 298 close_button_->SetFocusable(true); 299 AddChildView(close_button_); 300 } else if ((close_button_ != NULL) && (details.parent == this) && 301 (details.child != close_button_) && (close_button_->parent() == this) && 302 (child_at(child_count() - 1) != close_button_)) { 303 // For accessibility, ensure the close button is the last child view. 304 RemoveChildView(close_button_); 305 AddChildView(close_button_); 306 } 307 308 // Ensure the infobar is tall enough to display its contents. 309 const int kMinimumVerticalPadding = 6; 310 int height = kDefaultBarTargetHeight; 311 for (int i = 0; i < child_count(); ++i) { 312 const int child_height = child_at(i)->height(); 313 height = std::max(height, child_height + kMinimumVerticalPadding); 314 } 315 SetBarTargetHeight(height); 316} 317 318void InfoBarView::PaintChildren(gfx::Canvas* canvas) { 319 canvas->Save(); 320 321 // TODO(scr): This really should be the |fill_path_|, but the clipPath seems 322 // broken on non-Windows platforms (crbug.com/75154). For now, just clip to 323 // the bar bounds. 324 // 325 // canvas->sk_canvas()->clipPath(fill_path_); 326 DCHECK_EQ(total_height(), height()) 327 << "Infobar piecewise heights do not match overall height"; 328 canvas->ClipRect(gfx::Rect(0, arrow_height(), width(), bar_height())); 329 views::View::PaintChildren(canvas); 330 canvas->Restore(); 331} 332 333void InfoBarView::ButtonPressed(views::Button* sender, 334 const ui::Event& event) { 335 if (!owner()) 336 return; // We're closing; don't call anything, it might access the owner. 337 if (sender == close_button_) { 338 delegate()->InfoBarDismissed(); 339 RemoveSelf(); 340 } 341} 342 343int InfoBarView::ContentMinimumWidth() { 344 return 0; 345} 346 347int InfoBarView::StartX() const { 348 // Ensure we don't return a value greater than EndX(), so children can safely 349 // set something's width to "EndX() - StartX()" without risking that being 350 // negative. 351 return std::min(EndX(), (icon_ != NULL) ? 352 (icon_->bounds().right() + kIconToLabelSpacing) : kEdgeItemPadding); 353} 354 355int InfoBarView::EndX() const { 356 return close_button_->x() - kBeforeCloseButtonSpacing; 357} 358 359int InfoBarView::OffsetY(views::View* view) const { 360 return arrow_height() + 361 std::max((bar_target_height() - view->height()) / 2, 0) - 362 (bar_target_height() - bar_height()); 363} 364 365const infobars::InfoBarContainer::Delegate* InfoBarView::container_delegate() 366 const { 367 const infobars::InfoBarContainer* infobar_container = container(); 368 return infobar_container ? infobar_container->delegate() : NULL; 369} 370 371void InfoBarView::RunMenuAt(ui::MenuModel* menu_model, 372 views::MenuButton* button, 373 views::MenuItemView::AnchorPosition anchor) { 374 DCHECK(owner()); // We'd better not open any menus while we're closing. 375 gfx::Point screen_point; 376 views::View::ConvertPointToScreen(button, &screen_point); 377 menu_runner_.reset(new views::MenuRunner(menu_model)); 378 // Ignore the result since we don't need to handle a deleted menu specially. 379 ignore_result(menu_runner_->RunMenuAt( 380 GetWidget(), button, gfx::Rect(screen_point, button->size()), anchor, 381 ui::MENU_SOURCE_NONE, views::MenuRunner::HAS_MNEMONICS)); 382} 383 384// static 385void InfoBarView::AssignWidthsSorted(Labels* labels, int available_width) { 386 if (labels->empty()) 387 return; 388 gfx::Size back_label_size(labels->back()->GetPreferredSize()); 389 back_label_size.set_width( 390 std::min(back_label_size.width(), 391 available_width / static_cast<int>(labels->size()))); 392 labels->back()->SetSize(back_label_size); 393 labels->pop_back(); 394 AssignWidthsSorted(labels, available_width - back_label_size.width()); 395} 396 397void InfoBarView::PlatformSpecificShow(bool animate) { 398 // If we gain focus, we want to restore it to the previously-focused element 399 // when we're hidden. So when we're in a Widget, create a focus tracker so 400 // that if we gain focus we'll know what the previously-focused element was. 401 SetFocusManager(GetFocusManager()); 402 403 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true); 404} 405 406void InfoBarView::PlatformSpecificHide(bool animate) { 407 // Cancel any menus we may have open. It doesn't make sense to leave them 408 // open while we're hidden, and if we're going to become unowned, we can't 409 // allow the user to choose any options and potentially call functions that 410 // try to access the owner. 411 menu_runner_.reset(); 412 413 // It's possible to be called twice (once with |animate| true and once with it 414 // false); in this case the second SetFocusManager() call will silently no-op. 415 SetFocusManager(NULL); 416 417 if (!animate) 418 return; 419 420 // Do not restore focus (and active state with it) if some other top-level 421 // window became active. 422 views::Widget* widget = GetWidget(); 423 if (!widget || widget->IsActive()) 424 FocusLastFocusedExternalView(); 425} 426 427void InfoBarView::PlatformSpecificOnHeightsRecalculated() { 428 // Ensure that notifying our container of our size change will result in a 429 // re-layout. 430 InvalidateLayout(); 431} 432 433void InfoBarView::GetAccessibleState(ui::AXViewState* state) { 434 state->name = l10n_util::GetStringUTF16( 435 (delegate()->GetInfoBarType() == 436 infobars::InfoBarDelegate::WARNING_TYPE) ? 437 IDS_ACCNAME_INFOBAR_WARNING : IDS_ACCNAME_INFOBAR_PAGE_ACTION); 438 state->role = ui::AX_ROLE_ALERT; 439} 440 441gfx::Size InfoBarView::GetPreferredSize() { 442 return gfx::Size( 443 kEdgeItemPadding + (icon_ ? (icon_->width() + kIconToLabelSpacing) : 0) + 444 ContentMinimumWidth() + kBeforeCloseButtonSpacing + 445 close_button_->width() + kEdgeItemPadding, 446 total_height()); 447} 448 449void InfoBarView::OnWillChangeFocus(View* focused_before, View* focused_now) { 450 views::ExternalFocusTracker::OnWillChangeFocus(focused_before, focused_now); 451 452 // This will trigger some screen readers to read the entire contents of this 453 // infobar. 454 if (focused_before && focused_now && !Contains(focused_before) && 455 Contains(focused_now)) { 456 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true); 457 } 458} 459