Home | History | Annotate | Download | only in omnibox
      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/gtk/omnibox/omnibox_popup_view_gtk.h"
      6 
      7 #include <gtk/gtk.h>
      8 
      9 #include <algorithm>
     10 #include <string>
     11 
     12 #include "base/basictypes.h"
     13 #include "base/i18n/rtl.h"
     14 #include "base/logging.h"
     15 #include "base/stl_util.h"
     16 #include "base/strings/utf_string_conversions.h"
     17 #include "chrome/browser/autocomplete/autocomplete_match.h"
     18 #include "chrome/browser/autocomplete/autocomplete_result.h"
     19 #include "chrome/browser/chrome_notification_types.h"
     20 #include "chrome/browser/defaults.h"
     21 #include "chrome/browser/profiles/profile.h"
     22 #include "chrome/browser/search_engines/template_url.h"
     23 #include "chrome/browser/search_engines/template_url_service.h"
     24 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
     25 #include "chrome/browser/ui/gtk/gtk_util.h"
     26 #include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
     27 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
     28 #include "chrome/browser/ui/omnibox/omnibox_view.h"
     29 #include "content/public/browser/notification_source.h"
     30 #include "grit/theme_resources.h"
     31 #include "ui/base/gtk/gtk_compat.h"
     32 #include "ui/base/gtk/gtk_hig_constants.h"
     33 #include "ui/base/gtk/gtk_screen_util.h"
     34 #include "ui/base/gtk/gtk_signal_registrar.h"
     35 #include "ui/base/gtk/gtk_windowing.h"
     36 #include "ui/gfx/color_utils.h"
     37 #include "ui/gfx/font.h"
     38 #include "ui/gfx/gtk_util.h"
     39 #include "ui/gfx/image/cairo_cached_surface.h"
     40 #include "ui/gfx/image/image.h"
     41 #include "ui/gfx/rect.h"
     42 #include "ui/gfx/skia_utils_gtk.h"
     43 
     44 namespace {
     45 
     46 const GdkColor kBorderColor = GDK_COLOR_RGB(0xc7, 0xca, 0xce);
     47 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
     48 const GdkColor kSelectedBackgroundColor = GDK_COLOR_RGB(0xdf, 0xe6, 0xf6);
     49 const GdkColor kHoveredBackgroundColor = GDK_COLOR_RGB(0xef, 0xf2, 0xfa);
     50 
     51 const GdkColor kContentTextColor = GDK_COLOR_RGB(0x00, 0x00, 0x00);
     52 const GdkColor kURLTextColor = GDK_COLOR_RGB(0x00, 0x88, 0x00);
     53 
     54 // We have a 1 pixel border around the entire results popup.
     55 const int kBorderThickness = 1;
     56 
     57 // The vertical height of each result.
     58 const int kHeightPerResult = 24;
     59 
     60 // Width of the icons.
     61 const int kIconWidth = 17;
     62 
     63 // We want to vertically center the image in the result space.
     64 const int kIconTopPadding = 2;
     65 
     66 // Space between the left edge (including the border) and the text.
     67 const int kIconLeftPadding = 3 + kBorderThickness;
     68 
     69 // Space between the image and the text.
     70 const int kIconRightPadding = 5;
     71 
     72 // Space between the left edge (including the border) and the text.
     73 const int kIconAreaWidth =
     74     kIconLeftPadding + kIconWidth + kIconRightPadding;
     75 
     76 // Space between the right edge (including the border) and the text.
     77 const int kRightPadding = 3;
     78 
     79 // When we have both a content and description string, we don't want the
     80 // content to push the description off.  Limit the content to a percentage of
     81 // the total width.
     82 const float kContentWidthPercentage = 0.7;
     83 
     84 // How much to offset the popup from the bottom of the location bar.
     85 const int kVerticalOffset = 3;
     86 
     87 // The size delta between the font used for the edit and the result rows. Passed
     88 // to gfx::Font::DeriveFont.
     89 const int kEditFontAdjust = -1;
     90 
     91 // UTF-8 Left-to-right embedding.
     92 const char* kLRE = "\xe2\x80\xaa";
     93 
     94 // Return a Rect covering the whole area of |window|.
     95 gfx::Rect GetWindowRect(GdkWindow* window) {
     96   gint width = gdk_window_get_width(window);
     97   gint height = gdk_window_get_height(window);
     98   return gfx::Rect(width, height);
     99 }
    100 
    101 // Return a Rect for the space for a result line.  This excludes the border,
    102 // but includes the padding.  This is the area that is colored for a selection.
    103 gfx::Rect GetRectForLine(size_t line, int width) {
    104   return gfx::Rect(kBorderThickness,
    105                    (line * kHeightPerResult) + kBorderThickness,
    106                    width - (kBorderThickness * 2),
    107                    kHeightPerResult);
    108 }
    109 
    110 // TODO(deanm): Find some better home for this, and make it more efficient.
    111 size_t GetUTF8Offset(const string16& text, size_t text_offset) {
    112   return UTF16ToUTF8(text.substr(0, text_offset)).length();
    113 }
    114 
    115 // Generates the normal URL color, a green color used in unhighlighted URL
    116 // text. It is a mix of |kURLTextColor| and the current text color.  Unlike the
    117 // selected text color, it is more important to match the qualities of the
    118 // foreground typeface color instead of taking the background into account.
    119 GdkColor NormalURLColor(GdkColor foreground) {
    120   color_utils::HSL fg_hsl;
    121   color_utils::SkColorToHSL(gfx::GdkColorToSkColor(foreground), &fg_hsl);
    122 
    123   color_utils::HSL hue_hsl;
    124   color_utils::SkColorToHSL(gfx::GdkColorToSkColor(kURLTextColor), &hue_hsl);
    125 
    126   // Only allow colors that have a fair amount of saturation in them (color vs
    127   // white). This means that our output color will always be fairly green.
    128   double s = std::max(0.5, fg_hsl.s);
    129 
    130   // Make sure the luminance is at least as bright as the |kURLTextColor| green
    131   // would be if we were to use that.
    132   double l;
    133   if (fg_hsl.l < hue_hsl.l)
    134     l = hue_hsl.l;
    135   else
    136     l = (fg_hsl.l + hue_hsl.l) / 2;
    137 
    138   color_utils::HSL output = { hue_hsl.h, s, l };
    139   return gfx::SkColorToGdkColor(color_utils::HSLToSkColor(output, 255));
    140 }
    141 
    142 // Generates the selected URL color, a green color used on URL text in the
    143 // currently highlighted entry in the autocomplete popup. It's a mix of
    144 // |kURLTextColor|, the current text color, and the background color (the
    145 // select highlight). It is more important to contrast with the background
    146 // saturation than to look exactly like the foreground color.
    147 GdkColor SelectedURLColor(GdkColor foreground, GdkColor background) {
    148   color_utils::HSL fg_hsl;
    149   color_utils::SkColorToHSL(gfx::GdkColorToSkColor(foreground), &fg_hsl);
    150 
    151   color_utils::HSL bg_hsl;
    152   color_utils::SkColorToHSL(gfx::GdkColorToSkColor(background), &bg_hsl);
    153 
    154   color_utils::HSL hue_hsl;
    155   color_utils::SkColorToHSL(gfx::GdkColorToSkColor(kURLTextColor), &hue_hsl);
    156 
    157   // The saturation of the text should be opposite of the background, clamped
    158   // to 0.2-0.8. We make sure it's greater than 0.2 so there's some color, but
    159   // less than 0.8 so it's not the oversaturated neon-color.
    160   double opposite_s = 1 - bg_hsl.s;
    161   double s = std::max(0.2, std::min(0.8, opposite_s));
    162 
    163   // The luminance should match the luminance of the foreground text.  Again,
    164   // we clamp so as to have at some amount of color (green) in the text.
    165   double opposite_l = fg_hsl.l;
    166   double l = std::max(0.1, std::min(0.9, opposite_l));
    167 
    168   color_utils::HSL output = { hue_hsl.h, s, l };
    169   return gfx::SkColorToGdkColor(color_utils::HSLToSkColor(output, 255));
    170 }
    171 }  // namespace
    172 
    173 void OmniboxPopupViewGtk::SetupLayoutForMatch(
    174     PangoLayout* layout,
    175     const string16& text,
    176     const AutocompleteMatch::ACMatchClassifications& classifications,
    177     const GdkColor* base_color,
    178     const GdkColor* dim_color,
    179     const GdkColor* url_color,
    180     const std::string& prefix_text) {
    181   // In RTL, mark text with left-to-right embedding mark if there is no strong
    182   // RTL characters inside it, so the ending punctuation displays correctly
    183   // and the eliding ellipsis displays correctly. We only mark the text with
    184   // LRE. Wrapping it with LRE and PDF by calling AdjustStringForLocaleDirection
    185   // or WrapStringWithLTRFormatting will render the elllipsis at the left of the
    186   // elided pure LTR text.
    187   bool marked_with_lre = false;
    188   string16 localized_text = text;
    189   // Pango is really easy to overflow and send into a computational death
    190   // spiral that can corrupt the screen. Assume that we'll never have more than
    191   // 2000 characters, which should be a safe assumption until we all get robot
    192   // eyes. http://crbug.com/66576
    193   if (localized_text.length() > 2000)
    194     localized_text = localized_text.substr(0, 2000);
    195   bool is_rtl = base::i18n::IsRTL();
    196   if (is_rtl && !base::i18n::StringContainsStrongRTLChars(localized_text)) {
    197     localized_text.insert(0, 1, base::i18n::kLeftToRightEmbeddingMark);
    198     marked_with_lre = true;
    199   }
    200 
    201   // We can have a prefix, or insert additional characters while processing the
    202   // classifications.  We need to take this in to account when we translate the
    203   // UTF-16 offsets in the classification into text_utf8 byte offsets.
    204   size_t additional_offset = prefix_text.length();  // Length in utf-8 bytes.
    205   std::string text_utf8 = prefix_text + UTF16ToUTF8(localized_text);
    206 
    207   PangoAttrList* attrs = pango_attr_list_new();
    208 
    209   // TODO(deanm): This is a hack, just to handle coloring prefix_text.
    210   // Hopefully I can clean up the match situation a bit and this will
    211   // come out cleaner.  For now, apply the base color to the whole text
    212   // so that our prefix will have the base color applied.
    213   PangoAttribute* base_fg_attr = pango_attr_foreground_new(
    214       base_color->red, base_color->green, base_color->blue);
    215   pango_attr_list_insert(attrs, base_fg_attr);  // Ownership taken.
    216 
    217   // Walk through the classifications, they are linear, in order, and should
    218   // cover the entire text.  We create a bunch of overlapping attributes,
    219   // extending from the offset to the end of the string.  The ones created
    220   // later will override the previous ones, meaning we will still setup each
    221   // portion correctly, we just don't need to compute the end offset.
    222   for (ACMatchClassifications::const_iterator i = classifications.begin();
    223        i != classifications.end(); ++i) {
    224     size_t offset = GetUTF8Offset(localized_text, i->offset) +
    225                     additional_offset;
    226 
    227     // TODO(deanm): All the colors should probably blend based on whether this
    228     // result is selected or not.  This would include the green URLs.  Right
    229     // now the caller is left to blend only the base color.  Do we need to
    230     // handle things like DIM urls?  Turns out DIM means something different
    231     // than you'd think, all of the description text is not DIM, it is a
    232     // special case that is not very common, but we should figure out and
    233     // support it.
    234     const GdkColor* color = base_color;
    235     if (i->style & ACMatchClassification::URL) {
    236       color = url_color;
    237       // Insert a left to right embedding to make sure that URLs are shown LTR.
    238       if (is_rtl && !marked_with_lre) {
    239         std::string lre(kLRE);
    240         text_utf8.insert(offset, lre);
    241         additional_offset += lre.length();
    242       }
    243     }
    244 
    245     if (i->style & ACMatchClassification::DIM)
    246       color = dim_color;
    247 
    248     PangoAttribute* fg_attr = pango_attr_foreground_new(
    249         color->red, color->green, color->blue);
    250     fg_attr->start_index = offset;
    251     pango_attr_list_insert(attrs, fg_attr);  // Ownership taken.
    252 
    253     // Matched portions are bold, otherwise use the normal weight.
    254     PangoWeight weight = (i->style & ACMatchClassification::MATCH) ?
    255         PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL;
    256     PangoAttribute* weight_attr = pango_attr_weight_new(weight);
    257     weight_attr->start_index = offset;
    258     pango_attr_list_insert(attrs, weight_attr);  // Ownership taken.
    259   }
    260 
    261   pango_layout_set_text(layout, text_utf8.data(), text_utf8.length());
    262   pango_layout_set_attributes(layout, attrs);  // Ref taken.
    263   pango_attr_list_unref(attrs);
    264 }
    265 
    266 OmniboxPopupViewGtk::OmniboxPopupViewGtk(const gfx::Font& font,
    267                                          OmniboxView* omnibox_view,
    268                                          OmniboxEditModel* edit_model,
    269                                          GtkWidget* location_bar)
    270     : signal_registrar_(new ui::GtkSignalRegistrar),
    271       model_(new OmniboxPopupModel(this, edit_model)),
    272       omnibox_view_(omnibox_view),
    273       location_bar_(location_bar),
    274       window_(gtk_window_new(GTK_WINDOW_POPUP)),
    275       layout_(NULL),
    276       theme_service_(GtkThemeService::GetFrom(edit_model->profile())),
    277       font_(font.DeriveFont(kEditFontAdjust)),
    278       ignore_mouse_drag_(false),
    279       opened_(false) {
    280   gtk_widget_set_can_focus(window_, FALSE);
    281   // Don't allow the window to be resized.  This also forces the window to
    282   // shrink down to the size of its child contents.
    283   gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
    284   gtk_widget_set_app_paintable(window_, TRUE);
    285   // Have GTK double buffer around the expose signal.
    286   gtk_widget_set_double_buffered(window_, TRUE);
    287 
    288   // Cache the layout so we don't have to create it for every expose.  If we
    289   // were a real widget we should handle changing directions, but we're not
    290   // doing RTL or anything yet, so it shouldn't be important now.
    291   layout_ = gtk_widget_create_pango_layout(window_, NULL);
    292   // We don't want the layout of search results depending on their language.
    293   pango_layout_set_auto_dir(layout_, FALSE);
    294   // We always ellipsize when drawing our text runs.
    295   pango_layout_set_ellipsize(layout_, PANGO_ELLIPSIZE_END);
    296 
    297   gtk_widget_add_events(window_, GDK_BUTTON_MOTION_MASK |
    298                                  GDK_POINTER_MOTION_MASK |
    299                                  GDK_BUTTON_PRESS_MASK |
    300                                  GDK_BUTTON_RELEASE_MASK);
    301   signal_registrar_->Connect(window_, "motion-notify-event",
    302                              G_CALLBACK(HandleMotionThunk), this);
    303   signal_registrar_->Connect(window_, "button-press-event",
    304                              G_CALLBACK(HandleButtonPressThunk), this);
    305   signal_registrar_->Connect(window_, "button-release-event",
    306                              G_CALLBACK(HandleButtonReleaseThunk), this);
    307   signal_registrar_->Connect(window_, "expose-event",
    308                              G_CALLBACK(HandleExposeThunk), this);
    309 
    310   registrar_.Add(this,
    311                  chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
    312                  content::Source<ThemeService>(theme_service_));
    313   theme_service_->InitThemesFor(this);
    314 
    315   // TODO(erg): There appears to be a bug somewhere in something which shows
    316   // itself when we're in NX. Previously, we called
    317   // gtk_util::ActAsRoundedWindow() to make this popup have rounded
    318   // corners. This worked on the standard xorg server (both locally and
    319   // remotely), but broke over NX. My current hypothesis is that it can't
    320   // handle shaping top-level windows during an expose event, but I'm not sure
    321   // how else to get accurate shaping information.
    322   //
    323   // r25080 (the original patch that added rounded corners here) should
    324   // eventually be cherry picked once I know what's going
    325   // on. http://crbug.com/22015.
    326 }
    327 
    328 OmniboxPopupViewGtk::~OmniboxPopupViewGtk() {
    329   // Stop listening to our signals before we destroy the model. I suspect that
    330   // we can race window destruction, otherwise.
    331   signal_registrar_.reset();
    332 
    333   // Explicitly destroy our model here, before we destroy our GTK widgets.
    334   // This is because the model destructor can call back into us, and we need
    335   // to make sure everything is still valid when it does.
    336   model_.reset();
    337   g_object_unref(layout_);
    338   gtk_widget_destroy(window_);
    339 }
    340 
    341 bool OmniboxPopupViewGtk::IsOpen() const {
    342   return opened_;
    343 }
    344 
    345 void OmniboxPopupViewGtk::InvalidateLine(size_t line) {
    346   // TODO(deanm): Is it possible to use some constant for the width, instead
    347   // of having to query the width of the window?
    348   GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window_));
    349   GdkRectangle line_rect = GetRectForLine(
    350       line, GetWindowRect(gdk_window).width()).ToGdkRectangle();
    351   gdk_window_invalidate_rect(gdk_window, &line_rect, FALSE);
    352 }
    353 
    354 void OmniboxPopupViewGtk::UpdatePopupAppearance() {
    355   const AutocompleteResult& result = model_->result();
    356   if (result.empty()) {
    357     Hide();
    358     return;
    359   }
    360 
    361   Show(result.size());
    362   gtk_widget_queue_draw(window_);
    363 }
    364 
    365 gfx::Rect OmniboxPopupViewGtk::GetTargetBounds() {
    366   if (!gtk_widget_get_realized(window_))
    367     return gfx::Rect();
    368 
    369   gfx::Rect retval = ui::GetWidgetScreenBounds(window_);
    370 
    371   // The widget bounds don't update synchronously so may be out of sync with
    372   // our last size request.
    373   GtkRequisition req;
    374   gtk_widget_size_request(window_, &req);
    375   retval.set_width(req.width);
    376   retval.set_height(req.height);
    377 
    378   return retval;
    379 }
    380 
    381 void OmniboxPopupViewGtk::PaintUpdatesNow() {
    382   // Paint our queued invalidations now, synchronously.
    383   GdkWindow* gdk_window = gtk_widget_get_window(window_);
    384   gdk_window_process_updates(gdk_window, FALSE);
    385 }
    386 
    387 void OmniboxPopupViewGtk::OnDragCanceled() {
    388   ignore_mouse_drag_ = true;
    389 }
    390 
    391 void OmniboxPopupViewGtk::Observe(int type,
    392                                   const content::NotificationSource& source,
    393                                   const content::NotificationDetails& details) {
    394   DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED);
    395 
    396   if (theme_service_->UsingNativeTheme()) {
    397     gtk_util::UndoForceFontSize(window_);
    398 
    399     border_color_ = theme_service_->GetBorderColor();
    400 
    401     gtk_util::GetTextColors(
    402         &background_color_, &selected_background_color_,
    403         &content_text_color_, &selected_content_text_color_);
    404 
    405     hovered_background_color_ = gtk_util::AverageColors(
    406         background_color_, selected_background_color_);
    407     url_text_color_ = NormalURLColor(content_text_color_);
    408     url_selected_text_color_ = SelectedURLColor(selected_content_text_color_,
    409                                                 selected_background_color_);
    410   } else {
    411     gtk_util::ForceFontSizePixels(window_, font_.GetFontSize());
    412 
    413     border_color_ = kBorderColor;
    414     background_color_ = kBackgroundColor;
    415     selected_background_color_ = kSelectedBackgroundColor;
    416     hovered_background_color_ = kHoveredBackgroundColor;
    417 
    418     content_text_color_ = kContentTextColor;
    419     selected_content_text_color_ = kContentTextColor;
    420     url_text_color_ = kURLTextColor;
    421     url_selected_text_color_ = kURLTextColor;
    422   }
    423 
    424   // Calculate dimmed colors.
    425   content_dim_text_color_ =
    426       gtk_util::AverageColors(content_text_color_,
    427                               background_color_);
    428   selected_content_dim_text_color_ =
    429       gtk_util::AverageColors(selected_content_text_color_,
    430                               selected_background_color_);
    431 
    432   // Set the background color, so we don't need to paint it manually.
    433   gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &background_color_);
    434 }
    435 
    436 void OmniboxPopupViewGtk::Show(size_t num_results) {
    437   gint origin_x, origin_y;
    438   GdkWindow* gdk_window = gtk_widget_get_window(location_bar_);
    439   gdk_window_get_origin(gdk_window, &origin_x, &origin_y);
    440   GtkAllocation allocation;
    441   gtk_widget_get_allocation(location_bar_, &allocation);
    442 
    443   int horizontal_offset = 1;
    444   gtk_window_move(GTK_WINDOW(window_),
    445       origin_x + allocation.x - kBorderThickness + horizontal_offset,
    446       origin_y + allocation.y + allocation.height - kBorderThickness - 1 +
    447           kVerticalOffset);
    448   gtk_widget_set_size_request(window_,
    449       allocation.width + (kBorderThickness * 2) - (horizontal_offset * 2),
    450       (num_results * kHeightPerResult) + (kBorderThickness * 2));
    451   gtk_widget_show(window_);
    452   StackWindow();
    453   opened_ = true;
    454 }
    455 
    456 void OmniboxPopupViewGtk::Hide() {
    457   gtk_widget_hide(window_);
    458   opened_ = false;
    459 }
    460 
    461 void OmniboxPopupViewGtk::StackWindow() {
    462   gfx::NativeView omnibox_view = omnibox_view_->GetNativeView();
    463   DCHECK(GTK_IS_WIDGET(omnibox_view));
    464   GtkWidget* toplevel = gtk_widget_get_toplevel(omnibox_view);
    465   DCHECK(gtk_widget_is_toplevel(toplevel));
    466   ui::StackPopupWindow(window_, toplevel);
    467 }
    468 
    469 size_t OmniboxPopupViewGtk::LineFromY(int y) {
    470   size_t line = std::max(y - kBorderThickness, 0) / kHeightPerResult;
    471   return std::min(line, model_->result().size() - 1);
    472 }
    473 
    474 void OmniboxPopupViewGtk::AcceptLine(size_t line,
    475                                      WindowOpenDisposition disposition) {
    476   // OpenMatch() may close the popup, which will clear the result set and, by
    477   // extension, |match| and its contents.  So copy the relevant match out to
    478   // make sure it stays alive until the call completes.
    479   AutocompleteMatch match = model_->result().match_at(line);
    480   omnibox_view_->OpenMatch(match, disposition, GURL(), line);
    481 }
    482 
    483 gfx::Image OmniboxPopupViewGtk::IconForMatch(
    484     const AutocompleteMatch& match,
    485     bool selected,
    486     bool is_selected_keyword) {
    487   const gfx::Image image = model_->GetIconIfExtensionMatch(match);
    488   if (!image.IsEmpty())
    489     return image;
    490 
    491   int icon;
    492   if (is_selected_keyword)
    493     icon = IDR_OMNIBOX_TTS;
    494   else if (match.starred)
    495     icon = IDR_OMNIBOX_STAR;
    496   else
    497     icon = AutocompleteMatch::TypeToIcon(match.type);
    498 
    499   if (selected) {
    500     switch (icon) {
    501       case IDR_OMNIBOX_EXTENSION_APP:
    502         icon = IDR_OMNIBOX_EXTENSION_APP_DARK;
    503         break;
    504       case IDR_OMNIBOX_HTTP:
    505         icon = IDR_OMNIBOX_HTTP_DARK;
    506         break;
    507       case IDR_OMNIBOX_SEARCH:
    508         icon = IDR_OMNIBOX_SEARCH_DARK;
    509         break;
    510       case IDR_OMNIBOX_STAR:
    511         icon = IDR_OMNIBOX_STAR_DARK;
    512         break;
    513       case IDR_OMNIBOX_TTS:
    514         icon = IDR_OMNIBOX_TTS_DARK;
    515         break;
    516       default:
    517         NOTREACHED();
    518         break;
    519     }
    520   }
    521 
    522   return theme_service_->GetImageNamed(icon);
    523 }
    524 
    525 void OmniboxPopupViewGtk::GetVisibleMatchForInput(
    526     size_t index,
    527     const AutocompleteMatch** match,
    528     bool* is_selected_keyword) {
    529   const AutocompleteResult& result = model_->result();
    530 
    531   if (result.match_at(index).associated_keyword.get() &&
    532       model_->selected_line() == index &&
    533       model_->selected_line_state() == OmniboxPopupModel::KEYWORD) {
    534     *match = result.match_at(index).associated_keyword.get();
    535     *is_selected_keyword = true;
    536     return;
    537   }
    538 
    539   *match = &result.match_at(index);
    540   *is_selected_keyword = false;
    541 }
    542 
    543 gboolean OmniboxPopupViewGtk::HandleMotion(GtkWidget* widget,
    544                                            GdkEventMotion* event) {
    545   // TODO(deanm): Windows has a bunch of complicated logic here.
    546   size_t line = LineFromY(static_cast<int>(event->y));
    547   // There is both a hovered and selected line, hovered just means your mouse
    548   // is over it, but selected is what's showing in the location edit.
    549   model_->SetHoveredLine(line);
    550   // Select the line if the user has the left mouse button down.
    551   if (!ignore_mouse_drag_ && (event->state & GDK_BUTTON1_MASK))
    552     model_->SetSelectedLine(line, false, false);
    553   return TRUE;
    554 }
    555 
    556 gboolean OmniboxPopupViewGtk::HandleButtonPress(GtkWidget* widget,
    557                                                 GdkEventButton* event) {
    558   ignore_mouse_drag_ = false;
    559   // Very similar to HandleMotion.
    560   size_t line = LineFromY(static_cast<int>(event->y));
    561   model_->SetHoveredLine(line);
    562   if (event->button == 1)
    563     model_->SetSelectedLine(line, false, false);
    564   return TRUE;
    565 }
    566 
    567 gboolean OmniboxPopupViewGtk::HandleButtonRelease(GtkWidget* widget,
    568                                                   GdkEventButton* event) {
    569   if (ignore_mouse_drag_) {
    570     // See header comment about this flag.
    571     ignore_mouse_drag_ = false;
    572     return TRUE;
    573   }
    574 
    575   size_t line = LineFromY(static_cast<int>(event->y));
    576   switch (event->button) {
    577     case 1:  // Left click.
    578       AcceptLine(line, CURRENT_TAB);
    579       break;
    580     case 2:  // Middle click.
    581       AcceptLine(line, NEW_BACKGROUND_TAB);
    582       break;
    583     default:
    584       // Don't open the result.
    585       break;
    586   }
    587   return TRUE;
    588 }
    589 
    590 gboolean OmniboxPopupViewGtk::HandleExpose(GtkWidget* widget,
    591                                            GdkEventExpose* event) {
    592   bool ltr = !base::i18n::IsRTL();
    593   const AutocompleteResult& result = model_->result();
    594 
    595   gfx::Rect window_rect = GetWindowRect(event->window);
    596   gfx::Rect damage_rect = gfx::Rect(event->area);
    597   // Handle when our window is super narrow.  A bunch of the calculations
    598   // below would go negative, and really we're not going to fit anything
    599   // useful in such a small window anyway.  Just don't paint anything.
    600   // This means we won't draw the border, but, yeah, whatever.
    601   // TODO(deanm): Make the code more robust and remove this check.
    602   if (window_rect.width() < (kIconAreaWidth * 3))
    603     return TRUE;
    604 
    605   cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(widget));
    606   gdk_cairo_rectangle(cr, &event->area);
    607   cairo_clip(cr);
    608 
    609   // This assert is kinda ugly, but it would be more currently unneeded work
    610   // to support painting a border that isn't 1 pixel thick.  There is no point
    611   // in writing that code now, and explode if that day ever comes.
    612   COMPILE_ASSERT(kBorderThickness == 1, border_1px_implied);
    613   // Draw the 1px border around the entire window.
    614   gdk_cairo_set_source_color(cr, &border_color_);
    615   cairo_rectangle(cr, 0, 0, window_rect.width(), window_rect.height());
    616   cairo_stroke(cr);
    617 
    618   pango_layout_set_height(layout_, kHeightPerResult * PANGO_SCALE);
    619 
    620   for (size_t i = 0; i < result.size(); ++i) {
    621     gfx::Rect line_rect = GetRectForLine(i, window_rect.width());
    622     // Only repaint and layout damaged lines.
    623     if (!line_rect.Intersects(damage_rect))
    624       continue;
    625 
    626     const AutocompleteMatch* match = NULL;
    627     bool is_selected_keyword = false;
    628     GetVisibleMatchForInput(i, &match, &is_selected_keyword);
    629     bool is_selected = (model_->selected_line() == i);
    630     bool is_hovered = (model_->hovered_line() == i);
    631     if (is_selected || is_hovered) {
    632       gdk_cairo_set_source_color(cr, is_selected ? &selected_background_color_ :
    633                                  &hovered_background_color_);
    634       // This entry is selected or hovered, fill a rect with the color.
    635       cairo_rectangle(cr, line_rect.x(), line_rect.y(),
    636                       line_rect.width(), line_rect.height());
    637       cairo_fill(cr);
    638     }
    639 
    640     int icon_start_x = ltr ? kIconLeftPadding :
    641         (line_rect.width() - kIconLeftPadding - kIconWidth);
    642     // Draw the icon for this result.
    643     gtk_util::DrawFullImage(cr, widget,
    644                             IconForMatch(*match, is_selected,
    645                                          is_selected_keyword),
    646                             icon_start_x, line_rect.y() + kIconTopPadding);
    647 
    648     // Draw the results text vertically centered in the results space.
    649     // First draw the contents / url, but don't let it take up the whole width
    650     // if there is also a description to be shown.
    651     bool has_description = !match->description.empty();
    652     int text_width = window_rect.width() - (kIconAreaWidth + kRightPadding);
    653     int allocated_content_width = has_description ?
    654         static_cast<int>(text_width * kContentWidthPercentage) : text_width;
    655     pango_layout_set_width(layout_, allocated_content_width * PANGO_SCALE);
    656 
    657     // Note: We force to URL to LTR for all text directions.
    658     SetupLayoutForMatch(layout_, match->contents, match->contents_class,
    659                         is_selected ? &selected_content_text_color_ :
    660                             &content_text_color_,
    661                         is_selected ? &selected_content_dim_text_color_ :
    662                             &content_dim_text_color_,
    663                         is_selected ? &url_selected_text_color_ :
    664                             &url_text_color_,
    665                         std::string());
    666 
    667     int actual_content_width, actual_content_height;
    668     pango_layout_get_size(layout_,
    669         &actual_content_width, &actual_content_height);
    670     actual_content_width /= PANGO_SCALE;
    671     actual_content_height /= PANGO_SCALE;
    672 
    673     // DCHECK_LT(actual_content_height, kHeightPerResult);  // Font is too tall.
    674     // Center the text within the line.
    675     int content_y = std::max(line_rect.y(),
    676         line_rect.y() + ((kHeightPerResult - actual_content_height) / 2));
    677 
    678     cairo_save(cr);
    679     cairo_move_to(cr,
    680                   ltr ? kIconAreaWidth :
    681                         (text_width - actual_content_width),
    682                   content_y);
    683     pango_cairo_show_layout(cr, layout_);
    684     cairo_restore(cr);
    685 
    686     if (has_description) {
    687       pango_layout_set_width(layout_,
    688           (text_width - actual_content_width) * PANGO_SCALE);
    689 
    690       // In Windows, a boolean "force_dim" is passed as true for the
    691       // description.  Here, we pass the dim text color for both normal and dim,
    692       // to accomplish the same thing.
    693       SetupLayoutForMatch(layout_, match->description, match->description_class,
    694                           is_selected ? &selected_content_dim_text_color_ :
    695                               &content_dim_text_color_,
    696                           is_selected ? &selected_content_dim_text_color_ :
    697                               &content_dim_text_color_,
    698                           is_selected ? &url_selected_text_color_ :
    699                               &url_text_color_,
    700                           std::string(" - "));
    701       gint actual_description_width;
    702       pango_layout_get_size(layout_, &actual_description_width, NULL);
    703 
    704       cairo_save(cr);
    705       cairo_move_to(cr, ltr ?
    706                     (kIconAreaWidth + actual_content_width) :
    707                     (text_width - actual_content_width -
    708                      (actual_description_width / PANGO_SCALE)),
    709                     content_y);
    710       pango_cairo_show_layout(cr, layout_);
    711       cairo_restore(cr);
    712     }
    713 
    714     if (match->associated_keyword.get()) {
    715       // If this entry has an associated keyword, draw the arrow at the extreme
    716       // other side of the omnibox.
    717       icon_start_x = ltr ? (line_rect.width() - kIconLeftPadding - kIconWidth) :
    718           kIconLeftPadding;
    719       // Draw the icon for this result.
    720       gtk_util::DrawFullImage(cr, widget,
    721                               theme_service_->GetImageNamed(
    722                                   is_selected ? IDR_OMNIBOX_TTS_DARK :
    723                                   IDR_OMNIBOX_TTS),
    724                               icon_start_x, line_rect.y() + kIconTopPadding);
    725     }
    726   }
    727 
    728   cairo_destroy(cr);
    729   return TRUE;
    730 }
    731