omnibox_result_view.cc revision c5cede9ae108bb15f6b7a8aea21c7e1fefa2834c
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// For WinDDK ATL compatibility, these ATL headers must come first. 6#include "build/build_config.h" 7#if defined(OS_WIN) 8#include <atlbase.h> // NOLINT 9#include <atlwin.h> // NOLINT 10#endif 11 12#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h" 13 14#include <algorithm> // NOLINT 15 16#include "base/i18n/bidi_line_iterator.h" 17#include "base/memory/scoped_vector.h" 18#include "base/strings/string_number_conversions.h" 19#include "base/strings/string_util.h" 20#include "chrome/browser/ui/omnibox/omnibox_popup_model.h" 21#include "chrome/browser/ui/views/location_bar/location_bar_view.h" 22#include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h" 23#include "grit/generated_resources.h" 24#include "grit/theme_resources.h" 25#include "ui/base/l10n/l10n_util.h" 26#include "ui/base/theme_provider.h" 27#include "ui/gfx/canvas.h" 28#include "ui/gfx/color_utils.h" 29#include "ui/gfx/image/image.h" 30#include "ui/gfx/range/range.h" 31#include "ui/gfx/render_text.h" 32#include "ui/gfx/text_elider.h" 33#include "ui/gfx/text_utils.h" 34#include "ui/native_theme/native_theme.h" 35 36#if defined(OS_WIN) 37#include "ui/native_theme/native_theme_win.h" 38#endif 39 40#if defined(USE_AURA) 41#include "ui/native_theme/native_theme_aura.h" 42#endif 43 44using ui::NativeTheme; 45 46namespace { 47 48// The minimum distance between the top and bottom of the {icon|text} and the 49// top or bottom of the row. 50const int kMinimumIconVerticalPadding = 2; 51const int kMinimumTextVerticalPadding = 3; 52 53// A mapping from OmniboxResultView's ResultViewState/ColorKind types to 54// NativeTheme colors. 55struct TranslationTable { 56 ui::NativeTheme::ColorId id; 57 OmniboxResultView::ResultViewState state; 58 OmniboxResultView::ColorKind kind; 59} static const kTranslationTable[] = { 60 { NativeTheme::kColorId_ResultsTableNormalBackground, 61 OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND }, 62 { NativeTheme::kColorId_ResultsTableHoveredBackground, 63 OmniboxResultView::HOVERED, OmniboxResultView::BACKGROUND }, 64 { NativeTheme::kColorId_ResultsTableSelectedBackground, 65 OmniboxResultView::SELECTED, OmniboxResultView::BACKGROUND }, 66 { NativeTheme::kColorId_ResultsTableNormalText, 67 OmniboxResultView::NORMAL, OmniboxResultView::TEXT }, 68 { NativeTheme::kColorId_ResultsTableHoveredText, 69 OmniboxResultView::HOVERED, OmniboxResultView::TEXT }, 70 { NativeTheme::kColorId_ResultsTableSelectedText, 71 OmniboxResultView::SELECTED, OmniboxResultView::TEXT }, 72 { NativeTheme::kColorId_ResultsTableNormalDimmedText, 73 OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT }, 74 { NativeTheme::kColorId_ResultsTableHoveredDimmedText, 75 OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT }, 76 { NativeTheme::kColorId_ResultsTableSelectedDimmedText, 77 OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT }, 78 { NativeTheme::kColorId_ResultsTableNormalUrl, 79 OmniboxResultView::NORMAL, OmniboxResultView::URL }, 80 { NativeTheme::kColorId_ResultsTableHoveredUrl, 81 OmniboxResultView::HOVERED, OmniboxResultView::URL }, 82 { NativeTheme::kColorId_ResultsTableSelectedUrl, 83 OmniboxResultView::SELECTED, OmniboxResultView::URL }, 84 { NativeTheme::kColorId_ResultsTableNormalDivider, 85 OmniboxResultView::NORMAL, OmniboxResultView::DIVIDER }, 86 { NativeTheme::kColorId_ResultsTableHoveredDivider, 87 OmniboxResultView::HOVERED, OmniboxResultView::DIVIDER }, 88 { NativeTheme::kColorId_ResultsTableSelectedDivider, 89 OmniboxResultView::SELECTED, OmniboxResultView::DIVIDER }, 90}; 91 92} // namespace 93 94//////////////////////////////////////////////////////////////////////////////// 95// OmniboxResultView, public: 96 97// This class is a utility class for calculations affected by whether the result 98// view is horizontally mirrored. The drawing functions can be written as if 99// all drawing occurs left-to-right, and then use this class to get the actual 100// coordinates to begin drawing onscreen. 101class OmniboxResultView::MirroringContext { 102 public: 103 MirroringContext() : center_(0), right_(0) {} 104 105 // Tells the mirroring context to use the provided range as the physical 106 // bounds of the drawing region. When coordinate mirroring is needed, the 107 // mirror point will be the center of this range. 108 void Initialize(int x, int width) { 109 center_ = x + width / 2; 110 right_ = x + width; 111 } 112 113 // Given a logical range within the drawing region, returns the coordinate of 114 // the possibly-mirrored "left" side. (This functions exactly like 115 // View::MirroredLeftPointForRect().) 116 int mirrored_left_coord(int left, int right) const { 117 return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left; 118 } 119 120 // Given a logical coordinate within the drawing region, returns the remaining 121 // width available. 122 int remaining_width(int x) const { 123 return right_ - x; 124 } 125 126 private: 127 int center_; 128 int right_; 129 130 DISALLOW_COPY_AND_ASSIGN(MirroringContext); 131}; 132 133OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model, 134 int model_index, 135 LocationBarView* location_bar_view, 136 const gfx::FontList& font_list) 137 : edge_item_padding_(LocationBarView::GetItemPadding()), 138 item_padding_(LocationBarView::GetItemPadding()), 139 minimum_text_vertical_padding_(kMinimumTextVerticalPadding), 140 model_(model), 141 model_index_(model_index), 142 location_bar_view_(location_bar_view), 143 font_list_(font_list), 144 font_height_( 145 std::max(font_list.GetHeight(), 146 font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())), 147 mirroring_context_(new MirroringContext()), 148 keyword_icon_(new views::ImageView()), 149 animation_(new gfx::SlideAnimation(this)) { 150 CHECK_GE(model_index, 0); 151 if (default_icon_size_ == 0) { 152 default_icon_size_ = 153 location_bar_view_->GetThemeProvider()->GetImageSkiaNamed( 154 AutocompleteMatch::TypeToIcon( 155 AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width(); 156 } 157 keyword_icon_->set_owned_by_client(); 158 keyword_icon_->EnableCanvasFlippingForRTLUI(true); 159 keyword_icon_->SetImage(GetKeywordIcon()); 160 keyword_icon_->SizeToPreferredSize(); 161} 162 163OmniboxResultView::~OmniboxResultView() { 164} 165 166SkColor OmniboxResultView::GetColor( 167 ResultViewState state, 168 ColorKind kind) const { 169 for (size_t i = 0; i < arraysize(kTranslationTable); ++i) { 170 if (kTranslationTable[i].state == state && 171 kTranslationTable[i].kind == kind) { 172 return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id); 173 } 174 } 175 176 NOTREACHED(); 177 return SK_ColorRED; 178} 179 180void OmniboxResultView::SetMatch(const AutocompleteMatch& match) { 181 match_ = match; 182 ResetRenderTexts(); 183 animation_->Reset(); 184 185 AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get(); 186 if (associated_keyword_match) { 187 keyword_icon_->SetImage(GetKeywordIcon()); 188 if (!keyword_icon_->parent()) 189 AddChildView(keyword_icon_.get()); 190 } else if (keyword_icon_->parent()) { 191 RemoveChildView(keyword_icon_.get()); 192 } 193 194 Layout(); 195} 196 197void OmniboxResultView::ShowKeyword(bool show_keyword) { 198 if (show_keyword) 199 animation_->Show(); 200 else 201 animation_->Hide(); 202} 203 204void OmniboxResultView::Invalidate() { 205 keyword_icon_->SetImage(GetKeywordIcon()); 206 // While the text in the RenderTexts may not have changed, the styling 207 // (color/bold) may need to change. So we reset them to cause them to be 208 // recomputed in OnPaint(). 209 ResetRenderTexts(); 210 SchedulePaint(); 211} 212 213gfx::Size OmniboxResultView::GetPreferredSize() { 214 return gfx::Size(0, std::max( 215 default_icon_size_ + (kMinimumIconVerticalPadding * 2), 216 GetTextHeight() + (minimum_text_vertical_padding_ * 2))); 217} 218 219//////////////////////////////////////////////////////////////////////////////// 220// OmniboxResultView, protected: 221 222OmniboxResultView::ResultViewState OmniboxResultView::GetState() const { 223 if (model_->IsSelectedIndex(model_index_)) 224 return SELECTED; 225 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL; 226} 227 228int OmniboxResultView::GetTextHeight() const { 229 return font_height_; 230} 231 232void OmniboxResultView::PaintMatch( 233 const AutocompleteMatch& match, 234 gfx::RenderText* contents, 235 gfx::RenderText* description, 236 gfx::Canvas* canvas, 237 int x) const { 238 int y = text_bounds_.y(); 239 240 if (!separator_rendertext_) { 241 const base::string16& separator = 242 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR); 243 separator_rendertext_.reset(CreateRenderText(separator).release()); 244 separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT)); 245 separator_width_ = separator_rendertext_->GetContentWidth(); 246 } 247 248 int contents_max_width, description_max_width; 249 OmniboxPopupModel::ComputeMatchMaxWidths( 250 contents->GetContentWidth(), 251 separator_width_, 252 description ? description->GetContentWidth() : 0, 253 mirroring_context_->remaining_width(x), 254 !AutocompleteMatch::IsSearchType(match.type), 255 &contents_max_width, 256 &description_max_width); 257 258 x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width); 259 260 if (description_max_width != 0) { 261 x = DrawRenderText(match, separator_rendertext_.get(), false, canvas, x, y, 262 separator_width_); 263 DrawRenderText(match, description, false, canvas, x, y, 264 description_max_width); 265 } 266} 267 268int OmniboxResultView::DrawRenderText( 269 const AutocompleteMatch& match, 270 gfx::RenderText* render_text, 271 bool contents, 272 gfx::Canvas* canvas, 273 int x, 274 int y, 275 int max_width) const { 276 DCHECK(!render_text->text().empty()); 277 278 const int remaining_width = mirroring_context_->remaining_width(x); 279 int right_x = x + max_width; 280 281 // Infinite suggestions should appear with the leading ellipses vertically 282 // stacked. 283 if (contents && 284 (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE)) { 285 // When the directionality of suggestion doesn't match the UI, we try to 286 // vertically stack the ellipsis by restricting the end edge (right_x). 287 const bool is_ui_rtl = base::i18n::IsRTL(); 288 const bool is_match_contents_rtl = 289 (render_text->GetTextDirection() == base::i18n::RIGHT_TO_LEFT); 290 const int offset = 291 GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl); 292 293 scoped_ptr<gfx::RenderText> prefix_render_text( 294 CreateRenderText(base::UTF8ToUTF16( 295 match.GetAdditionalInfo(kACMatchPropertyContentsPrefix)))); 296 const int prefix_width = prefix_render_text->GetContentWidth(); 297 int prefix_x = x; 298 299 const int max_match_contents_width = model_->max_match_contents_width(); 300 301 if (is_ui_rtl != is_match_contents_rtl) { 302 // RTL infinite suggestions appear near the left edge in LTR UI, while LTR 303 // infinite suggestions appear near the right edge in RTL UI. This is 304 // against the natural horizontal alignment of the text. We reduce the 305 // width of the box for suggestion display, so that the suggestions appear 306 // in correct confines. This reduced width allows us to modify the text 307 // alignment (see below). 308 right_x = x + std::min(remaining_width - prefix_width, 309 std::max(offset, max_match_contents_width)); 310 prefix_x = right_x; 311 // We explicitly set the horizontal alignment so that when LTR suggestions 312 // show in RTL UI (or vice versa), their ellipses appear stacked in a 313 // single column. 314 render_text->SetHorizontalAlignment( 315 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); 316 } else { 317 // If the dropdown is wide enough, place the ellipsis at the position 318 // where the omitted text would have ended. Otherwise reduce the offset of 319 // the ellipsis such that the widest suggestion reaches the end of the 320 // dropdown. 321 const int start_offset = std::max(prefix_width, 322 std::min(remaining_width - max_match_contents_width, offset)); 323 right_x = x + std::min(remaining_width, start_offset + max_width); 324 x += start_offset; 325 prefix_x = x - prefix_width; 326 } 327 prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ? 328 gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR); 329 prefix_render_text->SetHorizontalAlignment( 330 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); 331 prefix_render_text->SetDisplayRect(gfx::Rect( 332 mirroring_context_->mirrored_left_coord( 333 prefix_x, prefix_x + prefix_width), y, 334 prefix_width, height())); 335 prefix_render_text->Draw(canvas); 336 } 337 338 // Set the display rect to trigger eliding. 339 render_text->SetDisplayRect(gfx::Rect( 340 mirroring_context_->mirrored_left_coord(x, right_x), y, 341 right_x - x, height())); 342 render_text->Draw(canvas); 343 return right_x; 344} 345 346scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText( 347 const base::string16& text) const { 348 scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance()); 349 render_text->SetCursorEnabled(false); 350 render_text->SetElideBehavior(gfx::ELIDE_AT_END); 351 render_text->SetFontList(font_list_); 352 render_text->SetText(text); 353 return render_text.Pass(); 354} 355 356scoped_ptr<gfx::RenderText> OmniboxResultView::CreateClassifiedRenderText( 357 const base::string16& text, 358 const ACMatchClassifications& classifications, 359 bool force_dim) const { 360 scoped_ptr<gfx::RenderText> render_text(CreateRenderText(text)); 361 const size_t text_length = render_text->text().length(); 362 for (size_t i = 0; i < classifications.size(); ++i) { 363 const size_t text_start = classifications[i].offset; 364 if (text_start >= text_length) 365 break; 366 367 const size_t text_end = (i < (classifications.size() - 1)) ? 368 std::min(classifications[i + 1].offset, text_length) : 369 text_length; 370 const gfx::Range current_range(text_start, text_end); 371 372 // Calculate style-related data. 373 if (classifications[i].style & ACMatchClassification::MATCH) 374 render_text->ApplyStyle(gfx::BOLD, true, current_range); 375 376 ColorKind color_kind = TEXT; 377 if (classifications[i].style & ACMatchClassification::URL) { 378 color_kind = URL; 379 // Consider logical string for domain "ABC.comי/hello" where ABC are 380 // Hebrew (RTL) characters. This string should ideally show as 381 // "CBA.com/hello". If we do not force LTR on URL, it will appear as 382 // "com/hello.CBA". 383 // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs, 384 // but it still has some pitfalls like : 385 // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which 386 // really confuses the path hierarchy of the URL. 387 // Also, if the URL supports https, the appearance will change into LTR 388 // directionality. 389 // In conclusion, LTR rendering of URL is probably the safest bet. 390 render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR); 391 } else if (force_dim || 392 (classifications[i].style & ACMatchClassification::DIM)) { 393 color_kind = DIMMED_TEXT; 394 } 395 render_text->ApplyColor(GetColor(GetState(), color_kind), current_range); 396 } 397 return render_text.Pass(); 398} 399 400int OmniboxResultView::GetMatchContentsWidth() const { 401 InitContentsRenderTextIfNecessary(); 402 return contents_rendertext_ ? contents_rendertext_->GetContentWidth() : 0; 403} 404 405// TODO(skanuj): This is probably identical across all OmniboxResultView rows in 406// the omnibox dropdown. Consider sharing the result. 407int OmniboxResultView::GetDisplayOffset( 408 const AutocompleteMatch& match, 409 bool is_ui_rtl, 410 bool is_match_contents_rtl) const { 411 if (match.type != AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) 412 return 0; 413 414 const base::string16& input_text = 415 base::UTF8ToUTF16(match.GetAdditionalInfo(kACMatchPropertyInputText)); 416 int contents_start_index = 0; 417 base::StringToInt(match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex), 418 &contents_start_index); 419 420 scoped_ptr<gfx::RenderText> input_render_text(CreateRenderText(input_text)); 421 const gfx::Range& glyph_bounds = 422 input_render_text->GetGlyphBounds(contents_start_index); 423 const int start_padding = is_match_contents_rtl ? 424 std::max(glyph_bounds.start(), glyph_bounds.end()) : 425 std::min(glyph_bounds.start(), glyph_bounds.end()); 426 427 return is_ui_rtl ? 428 (input_render_text->GetContentWidth() - start_padding) : start_padding; 429} 430 431// static 432int OmniboxResultView::default_icon_size_ = 0; 433 434gfx::ImageSkia OmniboxResultView::GetIcon() const { 435 const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_); 436 if (!image.IsEmpty()) 437 return image.AsImageSkia(); 438 439 int icon = match_.starred ? 440 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type); 441 if (GetState() == SELECTED) { 442 switch (icon) { 443 case IDR_OMNIBOX_EXTENSION_APP: 444 icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED; 445 break; 446 case IDR_OMNIBOX_HTTP: 447 icon = IDR_OMNIBOX_HTTP_SELECTED; 448 break; 449 case IDR_OMNIBOX_SEARCH: 450 icon = IDR_OMNIBOX_SEARCH_SELECTED; 451 break; 452 case IDR_OMNIBOX_STAR: 453 icon = IDR_OMNIBOX_STAR_SELECTED; 454 break; 455 default: 456 NOTREACHED(); 457 break; 458 } 459 } 460 return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon)); 461} 462 463const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const { 464 // NOTE: If we ever begin returning icons of varying size, then callers need 465 // to ensure that |keyword_icon_| is resized each time its image is reset. 466 return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed( 467 (GetState() == SELECTED) ? IDR_OMNIBOX_TTS_SELECTED : IDR_OMNIBOX_TTS); 468} 469 470bool OmniboxResultView::ShowOnlyKeywordMatch() const { 471 return match_.associated_keyword && 472 (keyword_icon_->x() <= icon_bounds_.right()); 473} 474 475void OmniboxResultView::ResetRenderTexts() const { 476 contents_rendertext_.reset(); 477 description_rendertext_.reset(); 478 separator_rendertext_.reset(); 479 keyword_contents_rendertext_.reset(); 480 keyword_description_rendertext_.reset(); 481} 482 483void OmniboxResultView::InitContentsRenderTextIfNecessary() const { 484 if (!contents_rendertext_) { 485 contents_rendertext_.reset( 486 CreateClassifiedRenderText( 487 match_.contents, match_.contents_class, false).release()); 488 } 489} 490 491void OmniboxResultView::Layout() { 492 const gfx::ImageSkia icon = GetIcon(); 493 494 icon_bounds_.SetRect(edge_item_padding_ + 495 ((icon.width() == default_icon_size_) ? 496 0 : LocationBarView::kIconInternalPadding), 497 (height() - icon.height()) / 2, icon.width(), icon.height()); 498 499 int text_x = edge_item_padding_ + default_icon_size_ + item_padding_; 500 int text_width = width() - text_x - edge_item_padding_; 501 502 if (match_.associated_keyword.get()) { 503 const int kw_collapsed_size = 504 keyword_icon_->width() + edge_item_padding_; 505 const int max_kw_x = width() - kw_collapsed_size; 506 const int kw_x = 507 animation_->CurrentValueBetween(max_kw_x, edge_item_padding_); 508 const int kw_text_x = kw_x + keyword_icon_->width() + item_padding_; 509 510 text_width = kw_x - text_x - item_padding_; 511 keyword_text_bounds_.SetRect( 512 kw_text_x, 0, 513 std::max(width() - kw_text_x - edge_item_padding_, 0), height()); 514 keyword_icon_->SetPosition( 515 gfx::Point(kw_x, (height() - keyword_icon_->height()) / 2)); 516 } 517 518 text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height()); 519} 520 521void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 522 animation_->SetSlideDuration(width() / 4); 523} 524 525void OmniboxResultView::OnPaint(gfx::Canvas* canvas) { 526 const ResultViewState state = GetState(); 527 if (state != NORMAL) 528 canvas->DrawColor(GetColor(state, BACKGROUND)); 529 530 // NOTE: While animating the keyword match, both matches may be visible. 531 532 if (!ShowOnlyKeywordMatch()) { 533 canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_), 534 icon_bounds_.y()); 535 int x = GetMirroredXForRect(text_bounds_); 536 mirroring_context_->Initialize(x, text_bounds_.width()); 537 InitContentsRenderTextIfNecessary(); 538 if (!description_rendertext_ && !match_.description.empty()) { 539 description_rendertext_.reset( 540 CreateClassifiedRenderText( 541 match_.description, match_.description_class, true).release()); 542 } 543 PaintMatch(match_, contents_rendertext_.get(), 544 description_rendertext_.get(), canvas, x); 545 } 546 547 AutocompleteMatch* keyword_match = match_.associated_keyword.get(); 548 if (keyword_match) { 549 int x = GetMirroredXForRect(keyword_text_bounds_); 550 mirroring_context_->Initialize(x, keyword_text_bounds_.width()); 551 if (!keyword_contents_rendertext_) { 552 keyword_contents_rendertext_.reset( 553 CreateClassifiedRenderText(keyword_match->contents, 554 keyword_match->contents_class, 555 false).release()); 556 } 557 if (!keyword_description_rendertext_ && 558 !keyword_match->description.empty()) { 559 keyword_description_rendertext_.reset( 560 CreateClassifiedRenderText(keyword_match->description, 561 keyword_match->description_class, 562 true).release()); 563 } 564 PaintMatch(*keyword_match, keyword_contents_rendertext_.get(), 565 keyword_description_rendertext_.get(), canvas, x); 566 } 567} 568 569void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) { 570 Layout(); 571 SchedulePaint(); 572} 573