| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h" | 5 #include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h" |
| 6 | 6 |
| 7 #include <gtk/gtk.h> | 7 #include <gtk/gtk.h> |
| 8 | 8 |
| 9 #include <algorithm> | 9 #include <algorithm> |
| 10 #include <string> | 10 #include <string> |
| (...skipping 16 matching lines...) Expand all Loading... |
| 27 #include "chrome/browser/ui/omnibox/omnibox_view.h" | 27 #include "chrome/browser/ui/omnibox/omnibox_view.h" |
| 28 #include "chrome/common/chrome_notification_types.h" | 28 #include "chrome/common/chrome_notification_types.h" |
| 29 #include "content/public/browser/notification_source.h" | 29 #include "content/public/browser/notification_source.h" |
| 30 #include "grit/theme_resources.h" | 30 #include "grit/theme_resources.h" |
| 31 #include "ui/base/gtk/gtk_compat.h" | 31 #include "ui/base/gtk/gtk_compat.h" |
| 32 #include "ui/base/gtk/gtk_hig_constants.h" | 32 #include "ui/base/gtk/gtk_hig_constants.h" |
| 33 #include "ui/base/gtk/gtk_windowing.h" | 33 #include "ui/base/gtk/gtk_windowing.h" |
| 34 #include "ui/gfx/color_utils.h" | 34 #include "ui/gfx/color_utils.h" |
| 35 #include "ui/gfx/font.h" | 35 #include "ui/gfx/font.h" |
| 36 #include "ui/gfx/gtk_util.h" | 36 #include "ui/gfx/gtk_util.h" |
| 37 #include "ui/gfx/image/image.h" | |
| 38 #include "ui/gfx/image/cairo_cached_surface.h" | |
| 39 #include "ui/gfx/rect.h" | 37 #include "ui/gfx/rect.h" |
| 40 #include "ui/gfx/skia_utils_gtk.h" | 38 #include "ui/gfx/skia_utils_gtk.h" |
| 41 | 39 |
| 42 namespace { | 40 namespace { |
| 43 | 41 |
| 44 const GdkColor kBorderColor = GDK_COLOR_RGB(0xc7, 0xca, 0xce); | 42 const GdkColor kBorderColor = GDK_COLOR_RGB(0xc7, 0xca, 0xce); |
| 45 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff); | 43 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff); |
| 46 const GdkColor kSelectedBackgroundColor = GDK_COLOR_RGB(0xdf, 0xe6, 0xf6); | 44 const GdkColor kSelectedBackgroundColor = GDK_COLOR_RGB(0xdf, 0xe6, 0xf6); |
| 47 const GdkColor kHoveredBackgroundColor = GDK_COLOR_RGB(0xef, 0xf2, 0xfa); | 45 const GdkColor kHoveredBackgroundColor = GDK_COLOR_RGB(0xef, 0xf2, 0xfa); |
| 48 | 46 |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 99 // Return a Rect for the space for a result line. This excludes the border, | 97 // Return a Rect for the space for a result line. This excludes the border, |
| 100 // but includes the padding. This is the area that is colored for a selection. | 98 // but includes the padding. This is the area that is colored for a selection. |
| 101 gfx::Rect GetRectForLine(size_t line, int width) { | 99 gfx::Rect GetRectForLine(size_t line, int width) { |
| 102 return gfx::Rect(kBorderThickness, | 100 return gfx::Rect(kBorderThickness, |
| 103 (line * kHeightPerResult) + kBorderThickness, | 101 (line * kHeightPerResult) + kBorderThickness, |
| 104 width - (kBorderThickness * 2), | 102 width - (kBorderThickness * 2), |
| 105 kHeightPerResult); | 103 kHeightPerResult); |
| 106 } | 104 } |
| 107 | 105 |
| 108 // Helper for drawing an entire pixbuf without dithering. | 106 // Helper for drawing an entire pixbuf without dithering. |
| 109 void DrawFullImage(cairo_t* cr, GtkWidget* widget, const gfx::Image* image, | 107 void DrawFullPixbuf(GdkDrawable* drawable, GdkGC* gc, GdkPixbuf* pixbuf, |
| 110 gint dest_x, gint dest_y) { | 108 gint dest_x, gint dest_y) { |
| 111 gfx::CairoCachedSurface* surface = image->ToCairo(); | 109 gdk_draw_pixbuf(drawable, gc, pixbuf, |
| 112 surface->SetSource(cr, widget, dest_x, dest_y); | 110 0, 0, // Source. |
| 113 cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT); | 111 dest_x, dest_y, // Dest. |
| 114 cairo_rectangle(cr, dest_x, dest_y, surface->Width(), surface->Height()); | 112 -1, -1, // Width/height (auto). |
| 115 cairo_fill(cr); | 113 GDK_RGB_DITHER_NONE, 0, 0); // Don't dither. |
| 116 } | 114 } |
| 117 | 115 |
| 118 // TODO(deanm): Find some better home for this, and make it more efficient. | 116 // TODO(deanm): Find some better home for this, and make it more efficient. |
| 119 size_t GetUTF8Offset(const string16& text, size_t text_offset) { | 117 size_t GetUTF8Offset(const string16& text, size_t text_offset) { |
| 120 return UTF16ToUTF8(text.substr(0, text_offset)).length(); | 118 return UTF16ToUTF8(text.substr(0, text_offset)).length(); |
| 121 } | 119 } |
| 122 | 120 |
| 123 // Generates the normal URL color, a green color used in unhighlighted URL | 121 // Generates the normal URL color, a green color used in unhighlighted URL |
| 124 // text. It is a mix of |kURLTextColor| and the current text color. Unlike the | 122 // text. It is a mix of |kURLTextColor| and the current text color. Unlike the |
| 125 // selected text color, it is more important to match the qualities of the | 123 // selected text color, it is more important to match the qualities of the |
| (...skipping 207 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 333 } | 331 } |
| 334 | 332 |
| 335 OmniboxPopupViewGtk::~OmniboxPopupViewGtk() { | 333 OmniboxPopupViewGtk::~OmniboxPopupViewGtk() { |
| 336 // Explicitly destroy our model here, before we destroy our GTK widgets. | 334 // Explicitly destroy our model here, before we destroy our GTK widgets. |
| 337 // This is because the model destructor can call back into us, and we need | 335 // This is because the model destructor can call back into us, and we need |
| 338 // to make sure everything is still valid when it does. | 336 // to make sure everything is still valid when it does. |
| 339 model_.reset(); | 337 model_.reset(); |
| 340 g_object_unref(layout_); | 338 g_object_unref(layout_); |
| 341 gtk_widget_destroy(window_); | 339 gtk_widget_destroy(window_); |
| 342 | 340 |
| 343 for (ImageMap::iterator it = images_.begin(); it != images_.end(); ++it) | 341 for (PixbufMap::iterator it = pixbufs_.begin(); it != pixbufs_.end(); ++it) |
| 344 delete it->second; | 342 g_object_unref(it->second); |
| 345 } | 343 } |
| 346 | 344 |
| 347 bool OmniboxPopupViewGtk::IsOpen() const { | 345 bool OmniboxPopupViewGtk::IsOpen() const { |
| 348 return opened_; | 346 return opened_; |
| 349 } | 347 } |
| 350 | 348 |
| 351 void OmniboxPopupViewGtk::InvalidateLine(size_t line) { | 349 void OmniboxPopupViewGtk::InvalidateLine(size_t line) { |
| 352 // TODO(deanm): Is it possible to use some constant for the width, instead | 350 // TODO(deanm): Is it possible to use some constant for the width, instead |
| 353 // of having to query the width of the window? | 351 // of having to query the width of the window? |
| 354 GdkRectangle line_rect = GetRectForLine( | 352 GdkRectangle line_rect = GetRectForLine( |
| (...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 478 // OpenMatch() may close the popup, which will clear the result set and, by | 476 // OpenMatch() may close the popup, which will clear the result set and, by |
| 479 // extension, |match| and its contents. So copy the relevant match out to | 477 // extension, |match| and its contents. So copy the relevant match out to |
| 480 // make sure it stays alive until the call completes. | 478 // make sure it stays alive until the call completes. |
| 481 AutocompleteMatch match = model_->result().match_at(line); | 479 AutocompleteMatch match = model_->result().match_at(line); |
| 482 string16 keyword; | 480 string16 keyword; |
| 483 const bool is_keyword_hint = model_->GetKeywordForMatch(match, &keyword); | 481 const bool is_keyword_hint = model_->GetKeywordForMatch(match, &keyword); |
| 484 omnibox_view_->OpenMatch(match, disposition, GURL(), line, | 482 omnibox_view_->OpenMatch(match, disposition, GURL(), line, |
| 485 is_keyword_hint ? string16() : keyword); | 483 is_keyword_hint ? string16() : keyword); |
| 486 } | 484 } |
| 487 | 485 |
| 488 const gfx::Image* OmniboxPopupViewGtk::IconForMatch( | 486 GdkPixbuf* OmniboxPopupViewGtk::IconForMatch(const AutocompleteMatch& match, |
| 489 const AutocompleteMatch& match, bool selected) { | 487 bool selected) { |
| 490 const SkBitmap* bitmap = model_->GetIconIfExtensionMatch(match); | 488 const SkBitmap* bitmap = model_->GetIconIfExtensionMatch(match); |
| 491 if (bitmap) { | 489 if (bitmap) { |
| 492 if (!ContainsKey(images_, bitmap)) | 490 if (!ContainsKey(pixbufs_, bitmap)) |
| 493 images_[bitmap] = new gfx::Image(bitmap); | 491 pixbufs_[bitmap] = gfx::GdkPixbufFromSkBitmap(bitmap); |
| 494 return images_[bitmap]; | 492 return pixbufs_[bitmap]; |
| 495 } | 493 } |
| 496 | 494 |
| 497 int icon = match.starred ? | 495 int icon = match.starred ? |
| 498 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type); | 496 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type); |
| 499 if (selected) { | 497 if (selected) { |
| 500 switch (icon) { | 498 switch (icon) { |
| 501 case IDR_OMNIBOX_EXTENSION_APP: | 499 case IDR_OMNIBOX_EXTENSION_APP: |
| 502 icon = IDR_OMNIBOX_EXTENSION_APP_DARK; | 500 icon = IDR_OMNIBOX_EXTENSION_APP_DARK; |
| 503 break; | 501 break; |
| 504 case IDR_OMNIBOX_HTTP: | 502 case IDR_OMNIBOX_HTTP: |
| 505 icon = IDR_OMNIBOX_HTTP_DARK; | 503 icon = IDR_OMNIBOX_HTTP_DARK; |
| 506 break; | 504 break; |
| 507 case IDR_OMNIBOX_HISTORY: | 505 case IDR_OMNIBOX_HISTORY: |
| 508 icon = IDR_OMNIBOX_HISTORY_DARK; | 506 icon = IDR_OMNIBOX_HISTORY_DARK; |
| 509 break; | 507 break; |
| 510 case IDR_OMNIBOX_SEARCH: | 508 case IDR_OMNIBOX_SEARCH: |
| 511 icon = IDR_OMNIBOX_SEARCH_DARK; | 509 icon = IDR_OMNIBOX_SEARCH_DARK; |
| 512 break; | 510 break; |
| 513 case IDR_OMNIBOX_STAR: | 511 case IDR_OMNIBOX_STAR: |
| 514 icon = IDR_OMNIBOX_STAR_DARK; | 512 icon = IDR_OMNIBOX_STAR_DARK; |
| 515 break; | 513 break; |
| 516 default: | 514 default: |
| 517 NOTREACHED(); | 515 NOTREACHED(); |
| 518 break; | 516 break; |
| 519 } | 517 } |
| 520 } | 518 } |
| 521 | 519 |
| 522 return theme_service_->GetImageNamed(icon); | 520 // TODO(estade): Do we want to flip these for RTL? (Windows doesn't). |
| 521 return theme_service_->GetPixbufNamed(icon); |
| 523 } | 522 } |
| 524 | 523 |
| 525 gboolean OmniboxPopupViewGtk::HandleMotion(GtkWidget* widget, | 524 gboolean OmniboxPopupViewGtk::HandleMotion(GtkWidget* widget, |
| 526 GdkEventMotion* event) { | 525 GdkEventMotion* event) { |
| 527 // TODO(deanm): Windows has a bunch of complicated logic here. | 526 // TODO(deanm): Windows has a bunch of complicated logic here. |
| 528 size_t line = LineFromY(static_cast<int>(event->y)); | 527 size_t line = LineFromY(static_cast<int>(event->y)); |
| 529 // There is both a hovered and selected line, hovered just means your mouse | 528 // There is both a hovered and selected line, hovered just means your mouse |
| 530 // is over it, but selected is what's showing in the location edit. | 529 // is over it, but selected is what's showing in the location edit. |
| 531 model_->SetHoveredLine(line); | 530 model_->SetHoveredLine(line); |
| 532 // Select the line if the user has the left mouse button down. | 531 // Select the line if the user has the left mouse button down. |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 577 gfx::Rect window_rect = GetWindowRect(event->window); | 576 gfx::Rect window_rect = GetWindowRect(event->window); |
| 578 gfx::Rect damage_rect = gfx::Rect(event->area); | 577 gfx::Rect damage_rect = gfx::Rect(event->area); |
| 579 // Handle when our window is super narrow. A bunch of the calculations | 578 // Handle when our window is super narrow. A bunch of the calculations |
| 580 // below would go negative, and really we're not going to fit anything | 579 // below would go negative, and really we're not going to fit anything |
| 581 // useful in such a small window anyway. Just don't paint anything. | 580 // useful in such a small window anyway. Just don't paint anything. |
| 582 // This means we won't draw the border, but, yeah, whatever. | 581 // This means we won't draw the border, but, yeah, whatever. |
| 583 // TODO(deanm): Make the code more robust and remove this check. | 582 // TODO(deanm): Make the code more robust and remove this check. |
| 584 if (window_rect.width() < (kIconAreaWidth * 3)) | 583 if (window_rect.width() < (kIconAreaWidth * 3)) |
| 585 return TRUE; | 584 return TRUE; |
| 586 | 585 |
| 587 cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(widget->window)); | 586 GdkDrawable* drawable = GDK_DRAWABLE(event->window); |
| 588 gdk_cairo_rectangle(cr, &event->area); | 587 GdkGC* gc = gdk_gc_new(drawable); |
| 589 cairo_clip(cr); | 588 |
| 589 // kBorderColor is unallocated, so use the GdkRGB routine. |
| 590 gdk_gc_set_rgb_fg_color(gc, &border_color_); |
| 590 | 591 |
| 591 // This assert is kinda ugly, but it would be more currently unneeded work | 592 // This assert is kinda ugly, but it would be more currently unneeded work |
| 592 // to support painting a border that isn't 1 pixel thick. There is no point | 593 // to support painting a border that isn't 1 pixel thick. There is no point |
| 593 // in writing that code now, and explode if that day ever comes. | 594 // in writing that code now, and explode if that day ever comes. |
| 594 COMPILE_ASSERT(kBorderThickness == 1, border_1px_implied); | 595 COMPILE_ASSERT(kBorderThickness == 1, border_1px_implied); |
| 595 // Draw the 1px border around the entire window. | 596 // Draw the 1px border around the entire window. |
| 596 gdk_cairo_set_source_color(cr, &border_color_); | 597 gdk_draw_rectangle(drawable, gc, FALSE, |
| 597 cairo_rectangle(cr, 0, 0, window_rect.width(), window_rect.height()); | 598 0, 0, |
| 598 cairo_stroke(cr); | 599 window_rect.width() - 1, window_rect.height() - 1); |
| 599 | 600 |
| 600 pango_layout_set_height(layout_, kHeightPerResult * PANGO_SCALE); | 601 pango_layout_set_height(layout_, kHeightPerResult * PANGO_SCALE); |
| 601 | 602 |
| 602 for (size_t i = 0; i < result.size(); ++i) { | 603 for (size_t i = 0; i < result.size(); ++i) { |
| 603 gfx::Rect line_rect = GetRectForLine(i, window_rect.width()); | 604 gfx::Rect line_rect = GetRectForLine(i, window_rect.width()); |
| 604 // Only repaint and layout damaged lines. | 605 // Only repaint and layout damaged lines. |
| 605 if (!line_rect.Intersects(damage_rect)) | 606 if (!line_rect.Intersects(damage_rect)) |
| 606 continue; | 607 continue; |
| 607 | 608 |
| 608 const AutocompleteMatch& match = result.match_at(i); | 609 const AutocompleteMatch& match = result.match_at(i); |
| 609 bool is_selected = (model_->selected_line() == i); | 610 bool is_selected = (model_->selected_line() == i); |
| 610 bool is_hovered = (model_->hovered_line() == i); | 611 bool is_hovered = (model_->hovered_line() == i); |
| 611 if (is_selected || is_hovered) { | 612 if (is_selected || is_hovered) { |
| 612 gdk_cairo_set_source_color(cr, is_selected ? &selected_background_color_ : | 613 gdk_gc_set_rgb_fg_color(gc, is_selected ? &selected_background_color_ : |
| 613 &hovered_background_color_); | 614 &hovered_background_color_); |
| 614 // This entry is selected or hovered, fill a rect with the color. | 615 // This entry is selected or hovered, fill a rect with the color. |
| 615 cairo_rectangle(cr, line_rect.x(), line_rect.y(), | 616 gdk_draw_rectangle(drawable, gc, TRUE, |
| 616 line_rect.width(), line_rect.height()); | 617 line_rect.x(), line_rect.y(), |
| 617 cairo_fill(cr); | 618 line_rect.width(), line_rect.height()); |
| 618 } | 619 } |
| 619 | 620 |
| 620 int icon_start_x = ltr ? kIconLeftPadding : | 621 int icon_start_x = ltr ? kIconLeftPadding : |
| 621 (line_rect.width() - kIconLeftPadding - kIconWidth); | 622 (line_rect.width() - kIconLeftPadding - kIconWidth); |
| 622 // Draw the icon for this result. | 623 // Draw the icon for this result. |
| 623 DrawFullImage(cr, widget, | 624 DrawFullPixbuf(drawable, gc, |
| 624 IconForMatch(match, is_selected), | 625 IconForMatch(match, is_selected), |
| 625 icon_start_x, line_rect.y() + kIconTopPadding); | 626 icon_start_x, line_rect.y() + kIconTopPadding); |
| 626 | 627 |
| 627 // Draw the results text vertically centered in the results space. | 628 // Draw the results text vertically centered in the results space. |
| 628 // First draw the contents / url, but don't let it take up the whole width | 629 // First draw the contents / url, but don't let it take up the whole width |
| 629 // if there is also a description to be shown. | 630 // if there is also a description to be shown. |
| 630 bool has_description = !match.description.empty(); | 631 bool has_description = !match.description.empty(); |
| 631 int text_width = window_rect.width() - (kIconAreaWidth + kRightPadding); | 632 int text_width = window_rect.width() - (kIconAreaWidth + kRightPadding); |
| 632 int allocated_content_width = has_description ? | 633 int allocated_content_width = has_description ? |
| 633 static_cast<int>(text_width * kContentWidthPercentage) : text_width; | 634 static_cast<int>(text_width * kContentWidthPercentage) : text_width; |
| 634 pango_layout_set_width(layout_, allocated_content_width * PANGO_SCALE); | 635 pango_layout_set_width(layout_, allocated_content_width * PANGO_SCALE); |
| 635 | 636 |
| (...skipping 11 matching lines...) Expand all Loading... |
| 647 pango_layout_get_size(layout_, | 648 pango_layout_get_size(layout_, |
| 648 &actual_content_width, &actual_content_height); | 649 &actual_content_width, &actual_content_height); |
| 649 actual_content_width /= PANGO_SCALE; | 650 actual_content_width /= PANGO_SCALE; |
| 650 actual_content_height /= PANGO_SCALE; | 651 actual_content_height /= PANGO_SCALE; |
| 651 | 652 |
| 652 // DCHECK_LT(actual_content_height, kHeightPerResult); // Font is too tall. | 653 // DCHECK_LT(actual_content_height, kHeightPerResult); // Font is too tall. |
| 653 // Center the text within the line. | 654 // Center the text within the line. |
| 654 int content_y = std::max(line_rect.y(), | 655 int content_y = std::max(line_rect.y(), |
| 655 line_rect.y() + ((kHeightPerResult - actual_content_height) / 2)); | 656 line_rect.y() + ((kHeightPerResult - actual_content_height) / 2)); |
| 656 | 657 |
| 657 cairo_save(cr); | 658 gdk_draw_layout(drawable, gc, |
| 658 cairo_move_to(cr, | 659 ltr ? kIconAreaWidth : |
| 659 ltr ? kIconAreaWidth : | |
| 660 (text_width - actual_content_width), | 660 (text_width - actual_content_width), |
| 661 content_y); | 661 content_y, layout_); |
| 662 pango_cairo_show_layout(cr, layout_); | |
| 663 cairo_restore(cr); | |
| 664 | 662 |
| 665 if (has_description) { | 663 if (has_description) { |
| 666 pango_layout_set_width(layout_, | 664 pango_layout_set_width(layout_, |
| 667 (text_width - actual_content_width) * PANGO_SCALE); | 665 (text_width - actual_content_width) * PANGO_SCALE); |
| 668 | 666 |
| 669 // In Windows, a boolean "force_dim" is passed as true for the | 667 // In Windows, a boolean "force_dim" is passed as true for the |
| 670 // description. Here, we pass the dim text color for both normal and dim, | 668 // description. Here, we pass the dim text color for both normal and dim, |
| 671 // to accomplish the same thing. | 669 // to accomplish the same thing. |
| 672 SetupLayoutForMatch(layout_, match.description, match.description_class, | 670 SetupLayoutForMatch(layout_, match.description, match.description_class, |
| 673 is_selected ? &selected_content_dim_text_color_ : | 671 is_selected ? &selected_content_dim_text_color_ : |
| 674 &content_dim_text_color_, | 672 &content_dim_text_color_, |
| 675 is_selected ? &selected_content_dim_text_color_ : | 673 is_selected ? &selected_content_dim_text_color_ : |
| 676 &content_dim_text_color_, | 674 &content_dim_text_color_, |
| 677 is_selected ? &url_selected_text_color_ : | 675 is_selected ? &url_selected_text_color_ : |
| 678 &url_text_color_, | 676 &url_text_color_, |
| 679 std::string(" - ")); | 677 std::string(" - ")); |
| 680 gint actual_description_width; | 678 gint actual_description_width; |
| 681 pango_layout_get_size(layout_, &actual_description_width, NULL); | 679 pango_layout_get_size(layout_, &actual_description_width, NULL); |
| 682 | 680 gdk_draw_layout(drawable, gc, ltr ? |
| 683 cairo_save(cr); | 681 (kIconAreaWidth + actual_content_width) : |
| 684 cairo_move_to(cr, ltr ? | 682 (text_width - actual_content_width - |
| 685 (kIconAreaWidth + actual_content_width) : | 683 (actual_description_width / PANGO_SCALE)), |
| 686 (text_width - actual_content_width - | 684 content_y, layout_); |
| 687 (actual_description_width / PANGO_SCALE)), | |
| 688 content_y); | |
| 689 pango_cairo_show_layout(cr, layout_); | |
| 690 cairo_restore(cr); | |
| 691 } | 685 } |
| 692 } | 686 } |
| 693 | 687 |
| 694 cairo_destroy(cr); | 688 g_object_unref(gc); |
| 689 |
| 695 return TRUE; | 690 return TRUE; |
| 696 } | 691 } |
| OLD | NEW |