1 // Copyright (c) 2011 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/find_bar_view.h" 6 7 #include <algorithm> 8 9 #include "base/string_number_conversions.h" 10 #include "base/string_util.h" 11 #include "base/utf_string_conversions.h" 12 #include "chrome/browser/profiles/profile.h" 13 #include "chrome/browser/themes/theme_service.h" 14 #include "chrome/browser/ui/find_bar/find_bar_controller.h" 15 #include "chrome/browser/ui/find_bar/find_bar_state.h" 16 #include "chrome/browser/ui/find_bar/find_tab_helper.h" 17 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 18 #include "chrome/browser/ui/view_ids.h" 19 #include "chrome/browser/ui/views/find_bar_host.h" 20 #include "chrome/browser/ui/views/frame/browser_view.h" 21 #include "content/browser/tab_contents/tab_contents.h" 22 #include "grit/generated_resources.h" 23 #include "grit/theme_resources.h" 24 #include "third_party/skia/include/effects/SkGradientShader.h" 25 #include "ui/base/l10n/l10n_util.h" 26 #include "ui/base/resource/resource_bundle.h" 27 #include "ui/gfx/canvas.h" 28 #include "views/background.h" 29 #include "views/controls/button/image_button.h" 30 #include "views/controls/label.h" 31 #include "views/controls/textfield/textfield.h" 32 #include "views/widget/widget.h" 33 34 // The amount of whitespace to have before the find button. 35 static const int kWhiteSpaceAfterMatchCountLabel = 1; 36 37 // The margins around the search field and the close button. 38 static const int kMarginLeftOfCloseButton = 3; 39 static const int kMarginRightOfCloseButton = 7; 40 static const int kMarginLeftOfFindTextfield = 12; 41 42 // The margins around the match count label (We add extra space so that the 43 // background highlight extends beyond just the text). 44 static const int kMatchCountExtraWidth = 9; 45 46 // Minimum width for the match count label. 47 static const int kMatchCountMinWidth = 30; 48 49 // The text color for the match count label. 50 static const SkColor kTextColorMatchCount = SkColorSetRGB(178, 178, 178); 51 52 // The text color for the match count label when no matches are found. 53 static const SkColor kTextColorNoMatch = SK_ColorBLACK; 54 55 // The background color of the match count label when results are found. 56 static const SkColor kBackgroundColorMatch = SK_ColorWHITE; 57 58 // The background color of the match count label when no results are found. 59 static const SkColor kBackgroundColorNoMatch = SkColorSetRGB(255, 102, 102); 60 61 // The background images for the dialog. They are split into a left, a middle 62 // and a right part. The middle part determines the height of the dialog. The 63 // middle part is stretched to fill any remaining part between the left and the 64 // right image, after sizing the dialog to kWindowWidth. 65 static const SkBitmap* kDialog_left = NULL; 66 static const SkBitmap* kDialog_middle = NULL; 67 static const SkBitmap* kDialog_right = NULL; 68 69 // When we are animating, we draw only the top part of the left and right 70 // edges to give the illusion that the find dialog is attached to the 71 // window during this animation; this is the height of the items we draw. 72 static const int kAnimatingEdgeHeight = 5; 73 74 // The background image for the Find text box, which we draw behind the Find box 75 // to provide the Chrome look to the edge of the text box. 76 static const SkBitmap* kBackground = NULL; 77 78 // The rounded edge on the left side of the Find text box. 79 static const SkBitmap* kBackground_left = NULL; 80 81 // The default number of average characters that the text box will be. This 82 // number brings the width on a "regular fonts" system to about 300px. 83 static const int kDefaultCharWidth = 43; 84 85 //////////////////////////////////////////////////////////////////////////////// 86 // FindBarView, public: 87 88 FindBarView::FindBarView(FindBarHost* host) 89 : DropdownBarView(host), 90 find_text_(NULL), 91 match_count_text_(NULL), 92 focus_forwarder_view_(NULL), 93 find_previous_button_(NULL), 94 find_next_button_(NULL), 95 close_button_(NULL) { 96 SetID(VIEW_ID_FIND_IN_PAGE); 97 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 98 99 find_text_ = new SearchTextfieldView(); 100 find_text_->SetID(VIEW_ID_FIND_IN_PAGE_TEXT_FIELD); 101 find_text_->SetFont(rb.GetFont(ResourceBundle::BaseFont)); 102 find_text_->set_default_width_in_chars(kDefaultCharWidth); 103 find_text_->SetController(this); 104 find_text_->SetAccessibleName(l10n_util::GetStringUTF16(IDS_ACCNAME_FIND)); 105 AddChildView(find_text_); 106 107 match_count_text_ = new views::Label(); 108 match_count_text_->SetFont(rb.GetFont(ResourceBundle::BaseFont)); 109 match_count_text_->SetColor(kTextColorMatchCount); 110 match_count_text_->SetHorizontalAlignment(views::Label::ALIGN_CENTER); 111 AddChildView(match_count_text_); 112 113 // Create a focus forwarder view which sends focus to find_text_. 114 focus_forwarder_view_ = new FocusForwarderView(find_text_); 115 AddChildView(focus_forwarder_view_); 116 117 find_previous_button_ = new views::ImageButton(this); 118 find_previous_button_->set_tag(FIND_PREVIOUS_TAG); 119 find_previous_button_->SetFocusable(true); 120 find_previous_button_->SetImage(views::CustomButton::BS_NORMAL, 121 rb.GetBitmapNamed(IDR_FINDINPAGE_PREV)); 122 find_previous_button_->SetImage(views::CustomButton::BS_HOT, 123 rb.GetBitmapNamed(IDR_FINDINPAGE_PREV_H)); 124 find_previous_button_->SetImage(views::CustomButton::BS_DISABLED, 125 rb.GetBitmapNamed(IDR_FINDINPAGE_PREV_P)); 126 find_previous_button_->SetTooltipText(UTF16ToWide( 127 l10n_util::GetStringUTF16(IDS_FIND_IN_PAGE_PREVIOUS_TOOLTIP))); 128 find_previous_button_->SetAccessibleName( 129 l10n_util::GetStringUTF16(IDS_ACCNAME_PREVIOUS)); 130 AddChildView(find_previous_button_); 131 132 find_next_button_ = new views::ImageButton(this); 133 find_next_button_->set_tag(FIND_NEXT_TAG); 134 find_next_button_->SetFocusable(true); 135 find_next_button_->SetImage(views::CustomButton::BS_NORMAL, 136 rb.GetBitmapNamed(IDR_FINDINPAGE_NEXT)); 137 find_next_button_->SetImage(views::CustomButton::BS_HOT, 138 rb.GetBitmapNamed(IDR_FINDINPAGE_NEXT_H)); 139 find_next_button_->SetImage(views::CustomButton::BS_DISABLED, 140 rb.GetBitmapNamed(IDR_FINDINPAGE_NEXT_P)); 141 find_next_button_->SetTooltipText( 142 UTF16ToWide(l10n_util::GetStringUTF16(IDS_FIND_IN_PAGE_NEXT_TOOLTIP))); 143 find_next_button_->SetAccessibleName( 144 l10n_util::GetStringUTF16(IDS_ACCNAME_NEXT)); 145 AddChildView(find_next_button_); 146 147 close_button_ = new views::ImageButton(this); 148 close_button_->set_tag(CLOSE_TAG); 149 close_button_->SetFocusable(true); 150 close_button_->SetImage(views::CustomButton::BS_NORMAL, 151 rb.GetBitmapNamed(IDR_CLOSE_BAR)); 152 close_button_->SetImage(views::CustomButton::BS_HOT, 153 rb.GetBitmapNamed(IDR_CLOSE_BAR_H)); 154 close_button_->SetImage(views::CustomButton::BS_PUSHED, 155 rb.GetBitmapNamed(IDR_CLOSE_BAR_P)); 156 close_button_->SetTooltipText( 157 UTF16ToWide(l10n_util::GetStringUTF16(IDS_FIND_IN_PAGE_CLOSE_TOOLTIP))); 158 close_button_->SetAccessibleName( 159 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); 160 AddChildView(close_button_); 161 162 if (kDialog_left == NULL) { 163 // Background images for the dialog. 164 kDialog_left = rb.GetBitmapNamed(IDR_FIND_DIALOG_LEFT); 165 kDialog_middle = rb.GetBitmapNamed(IDR_FIND_DIALOG_MIDDLE); 166 kDialog_right = rb.GetBitmapNamed(IDR_FIND_DIALOG_RIGHT); 167 168 // Background images for the Find edit box. 169 kBackground = rb.GetBitmapNamed(IDR_FIND_BOX_BACKGROUND); 170 kBackground_left = rb.GetBitmapNamed(IDR_FIND_BOX_BACKGROUND_LEFT); 171 } 172 } 173 174 FindBarView::~FindBarView() { 175 } 176 177 void FindBarView::SetFindText(const string16& find_text) { 178 find_text_->SetText(find_text); 179 } 180 181 string16 FindBarView::GetFindText() const { 182 return find_text_->text(); 183 } 184 185 string16 FindBarView::GetFindSelectedText() const { 186 return find_text_->GetSelectedText(); 187 } 188 189 string16 FindBarView::GetMatchCountText() const { 190 return WideToUTF16Hack(match_count_text_->GetText()); 191 } 192 193 void FindBarView::UpdateForResult(const FindNotificationDetails& result, 194 const string16& find_text) { 195 bool have_valid_range = 196 result.number_of_matches() != -1 && result.active_match_ordinal() != -1; 197 198 // http://crbug.com/34970: some IMEs get confused if we change the text 199 // composed by them. To avoid this problem, we should check the IME status and 200 // update the text only when the IME is not composing text. 201 if (find_text_->text() != find_text && !find_text_->IsIMEComposing()) { 202 find_text_->SetText(find_text); 203 find_text_->SelectAll(); 204 } 205 206 if (!find_text.empty() && have_valid_range) { 207 match_count_text_->SetText(UTF16ToWide( 208 l10n_util::GetStringFUTF16(IDS_FIND_IN_PAGE_COUNT, 209 base::IntToString16(result.active_match_ordinal()), 210 base::IntToString16(result.number_of_matches())))); 211 212 UpdateMatchCountAppearance(result.number_of_matches() == 0 && 213 result.final_update()); 214 } else { 215 // If there was no text entered, we don't show anything in the result count 216 // area. 217 match_count_text_->SetText(std::wstring()); 218 219 UpdateMatchCountAppearance(false); 220 } 221 222 // The match_count label may have increased/decreased in size so we need to 223 // do a layout and repaint the dialog so that the find text field doesn't 224 // partially overlap the match-count label when it increases on no matches. 225 Layout(); 226 SchedulePaint(); 227 } 228 229 void FindBarView::ClearMatchCount() { 230 match_count_text_->SetText(L""); 231 UpdateMatchCountAppearance(false); 232 Layout(); 233 SchedulePaint(); 234 } 235 236 void FindBarView::SetFocusAndSelection(bool select_all) { 237 find_text_->RequestFocus(); 238 if (select_all && !find_text_->text().empty()) 239 find_text_->SelectAll(); 240 } 241 242 /////////////////////////////////////////////////////////////////////////////// 243 // FindBarView, views::View overrides: 244 245 void FindBarView::OnPaint(gfx::Canvas* canvas) { 246 SkPaint paint; 247 248 // Determine the find bar size as well as the offset from which to tile the 249 // toolbar background image. First, get the widget bounds. 250 gfx::Rect bounds = GetWidget()->GetWindowScreenBounds(); 251 // Now convert from screen to parent coordinates. 252 gfx::Point origin(bounds.origin()); 253 BrowserView* browser_view = host()->browser_view(); 254 ConvertPointToView(NULL, browser_view, &origin); 255 bounds.set_origin(origin); 256 // Finally, calculate the background image tiling offset. 257 origin = browser_view->OffsetPointForToolbarBackgroundImage(origin); 258 259 // First, we draw the background image for the whole dialog (3 images: left, 260 // middle and right). Note, that the window region has been set by the 261 // controller, so the whitespace in the left and right background images is 262 // actually outside the window region and is therefore not drawn. See 263 // FindInPageWidgetWin::CreateRoundedWindowEdges() for details. 264 ui::ThemeProvider* tp = GetThemeProvider(); 265 canvas->TileImageInt(*tp->GetBitmapNamed(IDR_THEME_TOOLBAR), origin.x(), 266 origin.y(), 0, 0, bounds.width(), bounds.height()); 267 268 // Now flip the canvas for the rest of the graphics if in RTL mode. 269 canvas->Save(); 270 if (base::i18n::IsRTL()) { 271 canvas->TranslateInt(width(), 0); 272 canvas->ScaleInt(-1, 1); 273 } 274 275 canvas->DrawBitmapInt(*kDialog_left, 0, 0); 276 277 // Stretch the middle background to cover all of the area between the two 278 // other images. 279 canvas->TileImageInt(*kDialog_middle, kDialog_left->width(), 0, 280 bounds.width() - kDialog_left->width() - kDialog_right->width(), 281 kDialog_middle->height()); 282 283 canvas->DrawBitmapInt(*kDialog_right, bounds.width() - kDialog_right->width(), 284 0); 285 286 // Then we draw the background image for the Find Textfield. We start by 287 // calculating the position of background images for the Find text box. 288 int find_text_x = find_text_->x(); 289 gfx::Point back_button_origin = find_previous_button_->bounds().origin(); 290 291 // Draw the image to the left that creates a curved left edge for the box. 292 canvas->TileImageInt(*kBackground_left, 293 find_text_x - kBackground_left->width(), back_button_origin.y(), 294 kBackground_left->width(), kBackground_left->height()); 295 296 // Draw the top and bottom border for whole text box (encompasses both the 297 // find_text_ edit box and the match_count_text_ label). 298 canvas->TileImageInt(*kBackground, find_text_x, back_button_origin.y(), 299 back_button_origin.x() - find_text_x, 300 kBackground->height()); 301 302 if (animation_offset() > 0) { 303 // While animating we draw the curved edges at the point where the 304 // controller told us the top of the window is: |animation_offset()|. 305 canvas->TileImageInt(*kDialog_left, bounds.x(), animation_offset(), 306 kDialog_left->width(), kAnimatingEdgeHeight); 307 canvas->TileImageInt(*kDialog_right, 308 bounds.width() - kDialog_right->width(), animation_offset(), 309 kDialog_right->width(), kAnimatingEdgeHeight); 310 } 311 312 canvas->Restore(); 313 } 314 315 void FindBarView::Layout() { 316 gfx::Size panel_size = GetPreferredSize(); 317 318 // First we draw the close button on the far right. 319 gfx::Size sz = close_button_->GetPreferredSize(); 320 close_button_->SetBounds(panel_size.width() - sz.width() - 321 kMarginRightOfCloseButton, 322 (height() - sz.height()) / 2, 323 sz.width(), 324 sz.height()); 325 // Set the color. 326 OnThemeChanged(); 327 328 // Next, the FindNext button to the left the close button. 329 sz = find_next_button_->GetPreferredSize(); 330 find_next_button_->SetBounds(close_button_->x() - 331 find_next_button_->width() - 332 kMarginLeftOfCloseButton, 333 (height() - sz.height()) / 2, 334 sz.width(), 335 sz.height()); 336 337 // Then, the FindPrevious button to the left the FindNext button. 338 sz = find_previous_button_->GetPreferredSize(); 339 find_previous_button_->SetBounds(find_next_button_->x() - 340 find_previous_button_->width(), 341 (height() - sz.height()) / 2, 342 sz.width(), 343 sz.height()); 344 345 // Then the label showing the match count number. 346 sz = match_count_text_->GetPreferredSize(); 347 // We want to make sure the red "no-match" background almost completely fills 348 // up the amount of vertical space within the text box. We therefore fix the 349 // size relative to the button heights. We use the FindPrev button, which has 350 // a 1px outer whitespace margin, 1px border and we want to appear 1px below 351 // the border line so we subtract 3 for top and 3 for bottom. 352 sz.set_height(find_previous_button_->height() - 6); // Subtract 3px x 2. 353 354 // We extend the label bounds a bit to give the background highlighting a bit 355 // of breathing room (margins around the text). 356 sz.Enlarge(kMatchCountExtraWidth, 0); 357 sz.set_width(std::max(kMatchCountMinWidth, static_cast<int>(sz.width()))); 358 int match_count_x = find_previous_button_->x() - 359 kWhiteSpaceAfterMatchCountLabel - 360 sz.width(); 361 match_count_text_->SetBounds(match_count_x, 362 (height() - sz.height()) / 2, 363 sz.width(), 364 sz.height()); 365 366 // And whatever space is left in between, gets filled up by the find edit box. 367 sz = find_text_->GetPreferredSize(); 368 sz.set_width(match_count_x - kMarginLeftOfFindTextfield); 369 find_text_->SetBounds(match_count_x - sz.width(), 370 (height() - sz.height()) / 2 + 1, 371 sz.width(), 372 sz.height()); 373 374 // The focus forwarder view is a hidden view that should cover the area 375 // between the find text box and the find button so that when the user clicks 376 // in that area we focus on the find text box. 377 int find_text_edge = find_text_->x() + find_text_->width(); 378 focus_forwarder_view_->SetBounds(find_text_edge, 379 find_previous_button_->y(), 380 find_previous_button_->x() - 381 find_text_edge, 382 find_previous_button_->height()); 383 } 384 385 void FindBarView::ViewHierarchyChanged(bool is_add, View* parent, View* child) { 386 if (is_add && child == this) { 387 find_text_->SetHorizontalMargins(3, 3); // Left and Right margins. 388 find_text_->SetVerticalMargins(0, 0); // Top and bottom margins. 389 find_text_->RemoveBorder(); // We draw our own border (a background image). 390 } 391 } 392 393 gfx::Size FindBarView::GetPreferredSize() { 394 gfx::Size prefsize = find_text_->GetPreferredSize(); 395 prefsize.set_height(kDialog_middle->height()); 396 397 // Add up all the preferred sizes and margins of the rest of the controls. 398 prefsize.Enlarge(kMarginLeftOfCloseButton + kMarginRightOfCloseButton + 399 kMarginLeftOfFindTextfield, 400 0); 401 prefsize.Enlarge(find_previous_button_->GetPreferredSize().width(), 0); 402 prefsize.Enlarge(find_next_button_->GetPreferredSize().width(), 0); 403 prefsize.Enlarge(close_button_->GetPreferredSize().width(), 0); 404 return prefsize; 405 } 406 407 //////////////////////////////////////////////////////////////////////////////// 408 // FindBarView, views::ButtonListener implementation: 409 410 void FindBarView::ButtonPressed( 411 views::Button* sender, const views::Event& event) { 412 switch (sender->tag()) { 413 case FIND_PREVIOUS_TAG: 414 case FIND_NEXT_TAG: 415 if (!find_text_->text().empty()) { 416 find_bar_host()->GetFindBarController()->tab_contents()-> 417 find_tab_helper()->StartFinding(find_text_->text(), 418 sender->tag() == FIND_NEXT_TAG, 419 false); // Not case sensitive. 420 } 421 if (event.IsMouseEvent()) { 422 // If mouse event, we move the focus back to the text-field, so that the 423 // user doesn't have to click on the text field to change the search. We 424 // don't want to do this for keyboard clicks on the button, since the 425 // user is more likely to press FindNext again than change the search 426 // query. 427 find_text_->RequestFocus(); 428 } 429 break; 430 case CLOSE_TAG: 431 find_bar_host()->GetFindBarController()->EndFindSession( 432 FindBarController::kKeepSelection); 433 break; 434 default: 435 NOTREACHED() << L"Unknown button"; 436 break; 437 } 438 } 439 440 //////////////////////////////////////////////////////////////////////////////// 441 // FindBarView, views::TextfieldController implementation: 442 443 void FindBarView::ContentsChanged(views::Textfield* sender, 444 const string16& new_contents) { 445 FindBarController* controller = find_bar_host()->GetFindBarController(); 446 DCHECK(controller); 447 // We must guard against a NULL tab_contents, which can happen if the text 448 // in the Find box is changed right after the tab is destroyed. Otherwise, it 449 // can lead to crashes, as exposed by automation testing in issue 8048. 450 if (!controller->tab_contents()) 451 return; 452 FindTabHelper* find_tab_helper = 453 controller->tab_contents()->find_tab_helper(); 454 455 // When the user changes something in the text box we check the contents and 456 // if the textbox contains something we set it as the new search string and 457 // initiate search (even though old searches might be in progress). 458 if (!new_contents.empty()) { 459 // The last two params here are forward (true) and case sensitive (false). 460 find_tab_helper->StartFinding(new_contents, true, false); 461 } else { 462 find_tab_helper->StopFinding(FindBarController::kClearSelection); 463 UpdateForResult(find_tab_helper->find_result(), string16()); 464 465 // Clearing the text box should clear the prepopulate state so that when 466 // we close and reopen the Find box it doesn't show the search we just 467 // deleted. We can't do this on ChromeOS yet because we get ContentsChanged 468 // sent for a lot more things than just the user nulling out the search 469 // terms. See http://crbug.com/45372. 470 FindBarState* find_bar_state = 471 controller->tab_contents()->profile()->GetFindBarState(); 472 find_bar_state->set_last_prepopulate_text(string16()); 473 } 474 } 475 476 bool FindBarView::HandleKeyEvent(views::Textfield* sender, 477 const views::KeyEvent& key_event) { 478 // If the dialog is not visible, there is no reason to process keyboard input. 479 if (!host()->IsVisible()) 480 return false; 481 482 if (find_bar_host()->MaybeForwardKeyEventToWebpage(key_event)) 483 return true; // Handled, we are done! 484 485 if (key_event.key_code() == ui::VKEY_RETURN) { 486 // Pressing Return/Enter starts the search (unless text box is empty). 487 string16 find_string = find_text_->text(); 488 if (!find_string.empty()) { 489 FindBarController* controller = find_bar_host()->GetFindBarController(); 490 FindTabHelper* find_tab_helper = 491 controller->tab_contents()->find_tab_helper(); 492 // Search forwards for enter, backwards for shift-enter. 493 find_tab_helper->StartFinding(find_string, 494 !key_event.IsShiftDown(), 495 false); // Not case sensitive. 496 } 497 } 498 499 return false; 500 } 501 502 void FindBarView::UpdateMatchCountAppearance(bool no_match) { 503 if (no_match) { 504 match_count_text_->set_background( 505 views::Background::CreateSolidBackground(kBackgroundColorNoMatch)); 506 match_count_text_->SetColor(kTextColorNoMatch); 507 } else { 508 match_count_text_->set_background( 509 views::Background::CreateSolidBackground(kBackgroundColorMatch)); 510 match_count_text_->SetColor(kTextColorMatchCount); 511 } 512 } 513 514 bool FindBarView::FocusForwarderView::OnMousePressed( 515 const views::MouseEvent& event) { 516 if (view_to_focus_on_mousedown_) { 517 view_to_focus_on_mousedown_->ClearSelection(); 518 view_to_focus_on_mousedown_->RequestFocus(); 519 } 520 return true; 521 } 522 523 FindBarView::SearchTextfieldView::SearchTextfieldView() { 524 } 525 526 FindBarView::SearchTextfieldView::~SearchTextfieldView() { 527 } 528 529 void FindBarView::SearchTextfieldView::RequestFocus() { 530 views::View::RequestFocus(); 531 SelectAll(); 532 } 533 534 FindBarHost* FindBarView::find_bar_host() const { 535 return static_cast<FindBarHost*>(host()); 536 } 537 538 void FindBarView::OnThemeChanged() { 539 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 540 if (GetThemeProvider()) { 541 close_button_->SetBackground( 542 GetThemeProvider()->GetColor(ThemeService::COLOR_TAB_TEXT), 543 rb.GetBitmapNamed(IDR_CLOSE_BAR), 544 rb.GetBitmapNamed(IDR_CLOSE_BAR_MASK)); 545 } 546 } 547