root/chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. GetWindowRect
  2. GetUTF8Offset
  3. NormalURLColor
  4. SelectedURLColor
  5. opened_
  6. Init
  7. IsOpen
  8. InvalidateLine
  9. UpdatePopupAppearance
  10. GetTargetBounds
  11. PaintUpdatesNow
  12. OnDragCanceled
  13. Observe
  14. LineFromY
  15. GetRectForLine
  16. GetHiddenMatchCount
  17. GetResult
  18. SetupLayoutForMatch
  19. Show
  20. Hide
  21. StackWindow
  22. AcceptLine
  23. IconForMatch
  24. GetVisibleMatchForInput
  25. HandleMotion
  26. HandleButtonPress
  27. HandleButtonRelease
  28. HandleExpose

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h"

#include <gtk/gtk.h>

#include <algorithm>
#include <string>

#include "base/basictypes.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/autocomplete/autocomplete_result.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/defaults.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_service.h"
#include "chrome/browser/ui/gtk/gtk_theme_service.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
#include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
#include "chrome/browser/ui/omnibox/omnibox_view.h"
#include "content/public/browser/notification_source.h"
#include "grit/theme_resources.h"
#include "ui/base/gtk/gtk_hig_constants.h"
#include "ui/base/gtk/gtk_screen_util.h"
#include "ui/base/gtk/gtk_windowing.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/gtk_compat.h"
#include "ui/gfx/gtk_util.h"
#include "ui/gfx/image/cairo_cached_surface.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/skia_utils_gtk.h"

namespace {

const GdkColor kBorderColor = GDK_COLOR_RGB(0xc7, 0xca, 0xce);
const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
const GdkColor kSelectedBackgroundColor = GDK_COLOR_RGB(0xdf, 0xe6, 0xf6);
const GdkColor kHoveredBackgroundColor = GDK_COLOR_RGB(0xef, 0xf2, 0xfa);

const GdkColor kContentTextColor = GDK_COLOR_RGB(0x00, 0x00, 0x00);
const GdkColor kURLTextColor = GDK_COLOR_RGB(0x00, 0x88, 0x00);

// We have a 1 pixel border around the entire results popup.
const int kBorderThickness = 1;

// The vertical height of each result.
const int kHeightPerResult = 24;

// Width of the icons.
const int kIconWidth = 17;

// We want to vertically center the image in the result space.
const int kIconTopPadding = 2;

// Space between the left edge (including the border) and the text.
const int kIconLeftPadding = 3 + kBorderThickness;

// Space between the image and the text.
const int kIconRightPadding = 5;

// Space between the left edge (including the border) and the text.
const int kIconAreaWidth =
    kIconLeftPadding + kIconWidth + kIconRightPadding;

// Space between the right edge (including the border) and the text.
const int kRightPadding = 3;

// When we have both a content and description string, we don't want the
// content to push the description off.  Limit the content to a percentage of
// the total width.
const float kContentWidthPercentage = 0.7;

// How much to offset the popup from the bottom of the location bar.
const int kVerticalOffset = 3;

// The size delta between the font used for the edit and the result rows. Passed
// to gfx::Font::Derive.
const int kEditFontAdjust = -1;

// UTF-8 Left-to-right embedding.
const char* kLRE = "\xe2\x80\xaa";

// Return a Rect covering the whole area of |window|.
gfx::Rect GetWindowRect(GdkWindow* window) {
  gint width = gdk_window_get_width(window);
  gint height = gdk_window_get_height(window);
  return gfx::Rect(width, height);
}

// TODO(deanm): Find some better home for this, and make it more efficient.
size_t GetUTF8Offset(const base::string16& text, size_t text_offset) {
  return base::UTF16ToUTF8(text.substr(0, text_offset)).length();
}

// Generates the normal URL color, a green color used in unhighlighted URL
// text. It is a mix of |kURLTextColor| and the current text color.  Unlike the
// selected text color, it is more important to match the qualities of the
// foreground typeface color instead of taking the background into account.
GdkColor NormalURLColor(GdkColor foreground) {
  color_utils::HSL fg_hsl;
  color_utils::SkColorToHSL(gfx::GdkColorToSkColor(foreground), &fg_hsl);

  color_utils::HSL hue_hsl;
  color_utils::SkColorToHSL(gfx::GdkColorToSkColor(kURLTextColor), &hue_hsl);

  // Only allow colors that have a fair amount of saturation in them (color vs
  // white). This means that our output color will always be fairly green.
  double s = std::max(0.5, fg_hsl.s);

  // Make sure the luminance is at least as bright as the |kURLTextColor| green
  // would be if we were to use that.
  double l;
  if (fg_hsl.l < hue_hsl.l)
    l = hue_hsl.l;
  else
    l = (fg_hsl.l + hue_hsl.l) / 2;

  color_utils::HSL output = { hue_hsl.h, s, l };
  return gfx::SkColorToGdkColor(color_utils::HSLToSkColor(output, 255));
}

// Generates the selected URL color, a green color used on URL text in the
// currently highlighted entry in the autocomplete popup. It's a mix of
// |kURLTextColor|, the current text color, and the background color (the
// select highlight). It is more important to contrast with the background
// saturation than to look exactly like the foreground color.
GdkColor SelectedURLColor(GdkColor foreground, GdkColor background) {
  color_utils::HSL fg_hsl;
  color_utils::SkColorToHSL(gfx::GdkColorToSkColor(foreground), &fg_hsl);

  color_utils::HSL bg_hsl;
  color_utils::SkColorToHSL(gfx::GdkColorToSkColor(background), &bg_hsl);

  color_utils::HSL hue_hsl;
  color_utils::SkColorToHSL(gfx::GdkColorToSkColor(kURLTextColor), &hue_hsl);

  // The saturation of the text should be opposite of the background, clamped
  // to 0.2-0.8. We make sure it's greater than 0.2 so there's some color, but
  // less than 0.8 so it's not the oversaturated neon-color.
  double opposite_s = 1 - bg_hsl.s;
  double s = std::max(0.2, std::min(0.8, opposite_s));

  // The luminance should match the luminance of the foreground text.  Again,
  // we clamp so as to have at some amount of color (green) in the text.
  double opposite_l = fg_hsl.l;
  double l = std::max(0.1, std::min(0.9, opposite_l));

  color_utils::HSL output = { hue_hsl.h, s, l };
  return gfx::SkColorToGdkColor(color_utils::HSLToSkColor(output, 255));
}
}  // namespace

OmniboxPopupViewGtk::OmniboxPopupViewGtk(const gfx::Font& font,
                                         OmniboxView* omnibox_view,
                                         OmniboxEditModel* edit_model,
                                         GtkWidget* location_bar)
    : omnibox_view_(omnibox_view),
      location_bar_(location_bar),
      window_(gtk_window_new(GTK_WINDOW_POPUP)),
      layout_(NULL),
      theme_service_(NULL),
      font_(font.Derive(kEditFontAdjust, font.GetStyle())),
      ignore_mouse_drag_(false),
      opened_(false) {
  // edit_model may be NULL in unit tests.
  if (edit_model) {
    model_.reset(new OmniboxPopupModel(this, edit_model));
    theme_service_ = GtkThemeService::GetFrom(edit_model->profile());
  }
}

void OmniboxPopupViewGtk::Init() {
  gtk_widget_set_can_focus(window_, FALSE);
  // Don't allow the window to be resized.  This also forces the window to
  // shrink down to the size of its child contents.
  gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
  gtk_widget_set_app_paintable(window_, TRUE);
  // Have GTK double buffer around the expose signal.
  gtk_widget_set_double_buffered(window_, TRUE);

  // Cache the layout so we don't have to create it for every expose.  If we
  // were a real widget we should handle changing directions, but we're not
  // doing RTL or anything yet, so it shouldn't be important now.
  layout_ = gtk_widget_create_pango_layout(window_, NULL);
  // We don't want the layout of search results depending on their language.
  pango_layout_set_auto_dir(layout_, FALSE);
  // We always ellipsize when drawing our text runs.
  pango_layout_set_ellipsize(layout_, PANGO_ELLIPSIZE_END);

  gtk_widget_add_events(window_, GDK_BUTTON_MOTION_MASK |
                                 GDK_POINTER_MOTION_MASK |
                                 GDK_BUTTON_PRESS_MASK |
                                 GDK_BUTTON_RELEASE_MASK);
  g_signal_connect(window_, "motion-notify-event",
                   G_CALLBACK(HandleMotionThunk), this);
  g_signal_connect(window_, "button-press-event",
                   G_CALLBACK(HandleButtonPressThunk), this);
  g_signal_connect(window_, "button-release-event",
                   G_CALLBACK(HandleButtonReleaseThunk), this);
  g_signal_connect(window_, "expose-event",
                   G_CALLBACK(HandleExposeThunk), this);

  registrar_.Add(this,
                 chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
                 content::Source<ThemeService>(theme_service_));
  theme_service_->InitThemesFor(this);

  // TODO(erg): There appears to be a bug somewhere in something which shows
  // itself when we're in NX. Previously, we called
  // gtk_util::ActAsRoundedWindow() to make this popup have rounded
  // corners. This worked on the standard xorg server (both locally and
  // remotely), but broke over NX. My current hypothesis is that it can't
  // handle shaping top-level windows during an expose event, but I'm not sure
  // how else to get accurate shaping information.
  //
  // r25080 (the original patch that added rounded corners here) should
  // eventually be cherry picked once I know what's going
  // on. http://crbug.com/22015.
}

OmniboxPopupViewGtk::~OmniboxPopupViewGtk() {
  // Explicitly destroy our model here, before we destroy our GTK widgets.
  // This is because the model destructor can call back into us, and we need
  // to make sure everything is still valid when it does.
  model_.reset();
  // layout_ may be NULL in unit tests.
  if (layout_) {
    g_object_unref(layout_);
    gtk_widget_destroy(window_);
  }
}

bool OmniboxPopupViewGtk::IsOpen() const {
  return opened_;
}

void OmniboxPopupViewGtk::InvalidateLine(size_t line) {
  if (line < GetHiddenMatchCount())
    return;
  // TODO(deanm): Is it possible to use some constant for the width, instead
  // of having to query the width of the window?
  GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window_));
  GdkRectangle line_rect = GetRectForLine(
      line, GetWindowRect(gdk_window).width()).ToGdkRectangle();
  gdk_window_invalidate_rect(gdk_window, &line_rect, FALSE);
}

void OmniboxPopupViewGtk::UpdatePopupAppearance() {
  const AutocompleteResult& result = GetResult();
  const size_t hidden_matches = GetHiddenMatchCount();
  if (result.size() <= hidden_matches) {
    Hide();
    return;
  }

  Show(result.size() - hidden_matches);
  gtk_widget_queue_draw(window_);
}

gfx::Rect OmniboxPopupViewGtk::GetTargetBounds() {
  if (!gtk_widget_get_realized(window_))
    return gfx::Rect();

  gfx::Rect retval = ui::GetWidgetScreenBounds(window_);

  // The widget bounds don't update synchronously so may be out of sync with
  // our last size request.
  GtkRequisition req;
  gtk_widget_size_request(window_, &req);
  retval.set_width(req.width);
  retval.set_height(req.height);

  return retval;
}

void OmniboxPopupViewGtk::PaintUpdatesNow() {
  // Paint our queued invalidations now, synchronously.
  GdkWindow* gdk_window = gtk_widget_get_window(window_);
  gdk_window_process_updates(gdk_window, FALSE);
}

void OmniboxPopupViewGtk::OnDragCanceled() {
  ignore_mouse_drag_ = true;
}

void OmniboxPopupViewGtk::Observe(int type,
                                  const content::NotificationSource& source,
                                  const content::NotificationDetails& details) {
  DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED);

  if (theme_service_->UsingNativeTheme()) {
    gtk_util::UndoForceFontSize(window_);

    border_color_ = theme_service_->GetBorderColor();

    gtk_util::GetTextColors(
        &background_color_, &selected_background_color_,
        &content_text_color_, &selected_content_text_color_);

    hovered_background_color_ = gtk_util::AverageColors(
        background_color_, selected_background_color_);
    url_text_color_ = NormalURLColor(content_text_color_);
    url_selected_text_color_ = SelectedURLColor(selected_content_text_color_,
                                                selected_background_color_);
  } else {
    gtk_util::ForceFontSizePixels(window_, font_.GetFontSize());

    border_color_ = kBorderColor;
    background_color_ = kBackgroundColor;
    selected_background_color_ = kSelectedBackgroundColor;
    hovered_background_color_ = kHoveredBackgroundColor;

    content_text_color_ = kContentTextColor;
    selected_content_text_color_ = kContentTextColor;
    url_text_color_ = kURLTextColor;
    url_selected_text_color_ = kURLTextColor;
  }

  // Calculate dimmed colors.
  content_dim_text_color_ =
      gtk_util::AverageColors(content_text_color_,
                              background_color_);
  selected_content_dim_text_color_ =
      gtk_util::AverageColors(selected_content_text_color_,
                              selected_background_color_);

  // Set the background color, so we don't need to paint it manually.
  gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &background_color_);
}

size_t OmniboxPopupViewGtk::LineFromY(int y) const {
  // model_ may be NULL in unit tests.
  if (model_)
    DCHECK_NE(0U, model_->result().size());
  size_t line = std::max(y - kBorderThickness, 0) / kHeightPerResult;
  return std::min(line + GetHiddenMatchCount(), GetResult().size() - 1);
}

gfx::Rect OmniboxPopupViewGtk::GetRectForLine(size_t line, int width) const {
  size_t visible_line = line - GetHiddenMatchCount();
  return gfx::Rect(kBorderThickness,
                   (visible_line * kHeightPerResult) + kBorderThickness,
                   width - (kBorderThickness * 2),
                   kHeightPerResult);
}

size_t OmniboxPopupViewGtk::GetHiddenMatchCount() const {
  return GetResult().ShouldHideTopMatch() ? 1 : 0;
}

const AutocompleteResult& OmniboxPopupViewGtk::GetResult() const {
  return model_->result();
}

// static
void OmniboxPopupViewGtk::SetupLayoutForMatch(
    PangoLayout* layout,
    const base::string16& text,
    const AutocompleteMatch::ACMatchClassifications& classifications,
    const GdkColor* base_color,
    const GdkColor* dim_color,
    const GdkColor* url_color,
    const std::string& prefix_text) {
  // In RTL, mark text with left-to-right embedding mark if there is no strong
  // RTL characters inside it, so the ending punctuation displays correctly
  // and the eliding ellipsis displays correctly. We only mark the text with
  // LRE. Wrapping it with LRE and PDF by calling AdjustStringForLocaleDirection
  // or WrapStringWithLTRFormatting will render the elllipsis at the left of the
  // elided pure LTR text.
  bool marked_with_lre = false;
  base::string16 localized_text = text;
  // Pango is really easy to overflow and send into a computational death
  // spiral that can corrupt the screen. Assume that we'll never have more than
  // 2000 characters, which should be a safe assumption until we all get robot
  // eyes. http://crbug.com/66576
  if (localized_text.length() > 2000)
    localized_text = localized_text.substr(0, 2000);
  bool is_rtl = base::i18n::IsRTL();
  if (is_rtl && !base::i18n::StringContainsStrongRTLChars(localized_text)) {
    localized_text.insert(0, 1, base::i18n::kLeftToRightEmbeddingMark);
    marked_with_lre = true;
  }

  // We can have a prefix, or insert additional characters while processing the
  // classifications.  We need to take this in to account when we translate the
  // UTF-16 offsets in the classification into text_utf8 byte offsets.
  size_t additional_offset = prefix_text.length();  // Length in utf-8 bytes.
  std::string text_utf8 = prefix_text + base::UTF16ToUTF8(localized_text);

  PangoAttrList* attrs = pango_attr_list_new();

  // TODO(deanm): This is a hack, just to handle coloring prefix_text.
  // Hopefully I can clean up the match situation a bit and this will
  // come out cleaner.  For now, apply the base color to the whole text
  // so that our prefix will have the base color applied.
  PangoAttribute* base_fg_attr = pango_attr_foreground_new(
      base_color->red, base_color->green, base_color->blue);
  pango_attr_list_insert(attrs, base_fg_attr);  // Ownership taken.

  // Walk through the classifications, they are linear, in order, and should
  // cover the entire text.  We create a bunch of overlapping attributes,
  // extending from the offset to the end of the string.  The ones created
  // later will override the previous ones, meaning we will still setup each
  // portion correctly, we just don't need to compute the end offset.
  for (ACMatchClassifications::const_iterator i = classifications.begin();
       i != classifications.end(); ++i) {
    size_t offset = GetUTF8Offset(localized_text, i->offset) +
                    additional_offset;

    // TODO(deanm): All the colors should probably blend based on whether this
    // result is selected or not.  This would include the green URLs.  Right
    // now the caller is left to blend only the base color.  Do we need to
    // handle things like DIM urls?  Turns out DIM means something different
    // than you'd think, all of the description text is not DIM, it is a
    // special case that is not very common, but we should figure out and
    // support it.
    const GdkColor* color = base_color;
    if (i->style & ACMatchClassification::URL) {
      color = url_color;
      // Insert a left to right embedding to make sure that URLs are shown LTR.
      if (is_rtl && !marked_with_lre) {
        std::string lre(kLRE);
        text_utf8.insert(offset, lre);
        additional_offset += lre.length();
      }
    }

    if (i->style & ACMatchClassification::DIM)
      color = dim_color;

    PangoAttribute* fg_attr = pango_attr_foreground_new(
        color->red, color->green, color->blue);
    fg_attr->start_index = offset;
    pango_attr_list_insert(attrs, fg_attr);  // Ownership taken.

    // Matched portions are bold, otherwise use the normal weight.
    PangoWeight weight = (i->style & ACMatchClassification::MATCH) ?
        PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL;
    PangoAttribute* weight_attr = pango_attr_weight_new(weight);
    weight_attr->start_index = offset;
    pango_attr_list_insert(attrs, weight_attr);  // Ownership taken.
  }

  pango_layout_set_text(layout, text_utf8.data(), text_utf8.length());
  pango_layout_set_attributes(layout, attrs);  // Ref taken.
  pango_attr_list_unref(attrs);
}

void OmniboxPopupViewGtk::Show(size_t num_results) {
  gint origin_x, origin_y;
  GdkWindow* gdk_window = gtk_widget_get_window(location_bar_);
  gdk_window_get_origin(gdk_window, &origin_x, &origin_y);
  GtkAllocation allocation;
  gtk_widget_get_allocation(location_bar_, &allocation);

  int horizontal_offset = 1;
  gtk_window_move(GTK_WINDOW(window_),
      origin_x + allocation.x - kBorderThickness + horizontal_offset,
      origin_y + allocation.y + allocation.height - kBorderThickness - 1 +
          kVerticalOffset);
  gtk_widget_set_size_request(window_,
      allocation.width + (kBorderThickness * 2) - (horizontal_offset * 2),
      (num_results * kHeightPerResult) + (kBorderThickness * 2));
  gtk_widget_show(window_);
  StackWindow();
  opened_ = true;
}

void OmniboxPopupViewGtk::Hide() {
  gtk_widget_hide(window_);
  opened_ = false;
}

void OmniboxPopupViewGtk::StackWindow() {
  gfx::NativeView omnibox_view = omnibox_view_->GetNativeView();
  DCHECK(GTK_IS_WIDGET(omnibox_view));
  GtkWidget* toplevel = gtk_widget_get_toplevel(omnibox_view);
  DCHECK(gtk_widget_is_toplevel(toplevel));
  ui::StackPopupWindow(window_, toplevel);
}

void OmniboxPopupViewGtk::AcceptLine(size_t line,
                                     WindowOpenDisposition disposition) {
  omnibox_view_->OpenMatch(GetResult().match_at(line), disposition, GURL(),
                           base::string16(), line);
}

gfx::Image OmniboxPopupViewGtk::IconForMatch(
    const AutocompleteMatch& match,
    bool selected,
    bool is_selected_keyword) {
  const gfx::Image image = model_->GetIconIfExtensionMatch(match);
  if (!image.IsEmpty())
    return image;

  int icon;
  if (is_selected_keyword)
    icon = IDR_OMNIBOX_TTS;
  else if (match.starred)
    icon = IDR_OMNIBOX_STAR;
  else
    icon = AutocompleteMatch::TypeToIcon(match.type);

  if (selected) {
    switch (icon) {
      case IDR_OMNIBOX_EXTENSION_APP:
        icon = IDR_OMNIBOX_EXTENSION_APP_DARK;
        break;
      case IDR_OMNIBOX_HTTP:
        icon = IDR_OMNIBOX_HTTP_DARK;
        break;
      case IDR_OMNIBOX_SEARCH:
        icon = IDR_OMNIBOX_SEARCH_DARK;
        break;
      case IDR_OMNIBOX_STAR:
        icon = IDR_OMNIBOX_STAR_DARK;
        break;
      case IDR_OMNIBOX_TTS:
        icon = IDR_OMNIBOX_TTS_DARK;
        break;
      default:
        NOTREACHED();
        break;
    }
  }

  return theme_service_->GetImageNamed(icon);
}

void OmniboxPopupViewGtk::GetVisibleMatchForInput(
    size_t index,
    const AutocompleteMatch** match,
    bool* is_selected_keyword) {
  const AutocompleteResult& result = GetResult();

  if (result.match_at(index).associated_keyword.get() &&
      model_->selected_line() == index &&
      model_->selected_line_state() == OmniboxPopupModel::KEYWORD) {
    *match = result.match_at(index).associated_keyword.get();
    *is_selected_keyword = true;
    return;
  }

  *match = &result.match_at(index);
  *is_selected_keyword = false;
}

gboolean OmniboxPopupViewGtk::HandleMotion(GtkWidget* widget,
                                           GdkEventMotion* event) {
  if (!IsOpen())
    return FALSE;

  // TODO(deanm): Windows has a bunch of complicated logic here.
  size_t line = LineFromY(static_cast<int>(event->y));
  // There is both a hovered and selected line, hovered just means your mouse
  // is over it, but selected is what's showing in the location edit.
  model_->SetHoveredLine(line);
  // Select the line if the user has the left mouse button down.
  if (!ignore_mouse_drag_ && (event->state & GDK_BUTTON1_MASK))
    model_->SetSelectedLine(line, false, false);
  return TRUE;
}

gboolean OmniboxPopupViewGtk::HandleButtonPress(GtkWidget* widget,
                                                GdkEventButton* event) {
  if (!IsOpen())
    return FALSE;

  ignore_mouse_drag_ = false;
  // Very similar to HandleMotion.
  size_t line = LineFromY(static_cast<int>(event->y));
  model_->SetHoveredLine(line);
  if (event->button == 1)
    model_->SetSelectedLine(line, false, false);
  return TRUE;
}

gboolean OmniboxPopupViewGtk::HandleButtonRelease(GtkWidget* widget,
                                                  GdkEventButton* event) {
  if (!IsOpen())
    return FALSE;

  if (ignore_mouse_drag_) {
    // See header comment about this flag.
    ignore_mouse_drag_ = false;
    return TRUE;
  }

  size_t line = LineFromY(static_cast<int>(event->y));
  switch (event->button) {
    case 1:  // Left click.
      AcceptLine(line, CURRENT_TAB);
      break;
    case 2:  // Middle click.
      AcceptLine(line, NEW_BACKGROUND_TAB);
      break;
    default:
      // Don't open the result.
      break;
  }
  return TRUE;
}

gboolean OmniboxPopupViewGtk::HandleExpose(GtkWidget* widget,
                                           GdkEventExpose* event) {
  bool ltr = !base::i18n::IsRTL();
  const AutocompleteResult& result = GetResult();

  gfx::Rect window_rect = GetWindowRect(event->window);
  gfx::Rect damage_rect = gfx::Rect(event->area);
  // Handle when our window is super narrow.  A bunch of the calculations
  // below would go negative, and really we're not going to fit anything
  // useful in such a small window anyway.  Just don't paint anything.
  // This means we won't draw the border, but, yeah, whatever.
  // TODO(deanm): Make the code more robust and remove this check.
  if (window_rect.width() < (kIconAreaWidth * 3))
    return TRUE;

  cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(widget));
  gdk_cairo_rectangle(cr, &event->area);
  cairo_clip(cr);

  // This assert is kinda ugly, but it would be more currently unneeded work
  // to support painting a border that isn't 1 pixel thick.  There is no point
  // in writing that code now, and explode if that day ever comes.
  COMPILE_ASSERT(kBorderThickness == 1, border_1px_implied);
  // Draw the 1px border around the entire window.
  gdk_cairo_set_source_color(cr, &border_color_);
  cairo_rectangle(cr, 0, 0, window_rect.width(), window_rect.height());
  cairo_stroke(cr);

  pango_layout_set_height(layout_, kHeightPerResult * PANGO_SCALE);

  for (size_t i = GetHiddenMatchCount(); i < result.size(); ++i) {
    gfx::Rect line_rect = GetRectForLine(i, window_rect.width());
    // Only repaint and layout damaged lines.
    if (!line_rect.Intersects(damage_rect))
      continue;

    const AutocompleteMatch* match = NULL;
    bool is_selected_keyword = false;
    GetVisibleMatchForInput(i, &match, &is_selected_keyword);
    bool is_selected = (model_->selected_line() == i);
    bool is_hovered = (model_->hovered_line() == i);
    if (is_selected || is_hovered) {
      gdk_cairo_set_source_color(cr, is_selected ? &selected_background_color_ :
                                 &hovered_background_color_);
      // This entry is selected or hovered, fill a rect with the color.
      cairo_rectangle(cr, line_rect.x(), line_rect.y(),
                      line_rect.width(), line_rect.height());
      cairo_fill(cr);
    }

    int icon_start_x = ltr ? kIconLeftPadding :
        (line_rect.width() - kIconLeftPadding - kIconWidth);
    // Draw the icon for this result.
    gtk_util::DrawFullImage(cr, widget,
                            IconForMatch(*match, is_selected,
                                         is_selected_keyword),
                            icon_start_x, line_rect.y() + kIconTopPadding);

    // Draw the results text vertically centered in the results space.
    // First draw the contents / url, but don't let it take up the whole width
    // if there is also a description to be shown.
    bool has_description = !match->description.empty();
    int text_width = window_rect.width() - (kIconAreaWidth + kRightPadding);
    int allocated_content_width = has_description ?
        static_cast<int>(text_width * kContentWidthPercentage) : text_width;
    pango_layout_set_width(layout_, allocated_content_width * PANGO_SCALE);

    // Note: We force to URL to LTR for all text directions.
    SetupLayoutForMatch(layout_, match->contents, match->contents_class,
                        is_selected ? &selected_content_text_color_ :
                            &content_text_color_,
                        is_selected ? &selected_content_dim_text_color_ :
                            &content_dim_text_color_,
                        is_selected ? &url_selected_text_color_ :
                            &url_text_color_,
                        std::string());

    int actual_content_width, actual_content_height;
    pango_layout_get_size(layout_,
        &actual_content_width, &actual_content_height);
    actual_content_width /= PANGO_SCALE;
    actual_content_height /= PANGO_SCALE;

    // DCHECK_LT(actual_content_height, kHeightPerResult);  // Font is too tall.
    // Center the text within the line.
    int content_y = std::max(line_rect.y(),
        line_rect.y() + ((kHeightPerResult - actual_content_height) / 2));

    cairo_save(cr);
    cairo_move_to(cr,
                  ltr ? kIconAreaWidth :
                        (text_width - actual_content_width),
                  content_y);
    pango_cairo_show_layout(cr, layout_);
    cairo_restore(cr);

    if (has_description) {
      pango_layout_set_width(layout_,
          (text_width - actual_content_width) * PANGO_SCALE);

      // In Windows, a boolean "force_dim" is passed as true for the
      // description.  Here, we pass the dim text color for both normal and dim,
      // to accomplish the same thing.
      SetupLayoutForMatch(layout_, match->description, match->description_class,
                          is_selected ? &selected_content_dim_text_color_ :
                              &content_dim_text_color_,
                          is_selected ? &selected_content_dim_text_color_ :
                              &content_dim_text_color_,
                          is_selected ? &url_selected_text_color_ :
                              &url_text_color_,
                          std::string(" - "));
      gint actual_description_width;
      pango_layout_get_size(layout_, &actual_description_width, NULL);

      cairo_save(cr);
      cairo_move_to(cr, ltr ?
                    (kIconAreaWidth + actual_content_width) :
                    (text_width - actual_content_width -
                     (actual_description_width / PANGO_SCALE)),
                    content_y);
      pango_cairo_show_layout(cr, layout_);
      cairo_restore(cr);
    }

    if (match->associated_keyword.get()) {
      // If this entry has an associated keyword, draw the arrow at the extreme
      // other side of the omnibox.
      icon_start_x = ltr ? (line_rect.width() - kIconLeftPadding - kIconWidth) :
          kIconLeftPadding;
      // Draw the icon for this result.
      gtk_util::DrawFullImage(cr, widget,
                              theme_service_->GetImageNamed(
                                  is_selected ? IDR_OMNIBOX_TTS_DARK :
                                  IDR_OMNIBOX_TTS),
                              icon_start_x, line_rect.y() + kIconTopPadding);
    }
  }

  cairo_destroy(cr);
  return TRUE;
}

/* [<][>][^][v][top][bottom][index][help] */