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