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