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
44namespace {
45
46const GdkColor kBorderColor = GDK_COLOR_RGB(0xc7, 0xca, 0xce);
47const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
48const GdkColor kSelectedBackgroundColor = GDK_COLOR_RGB(0xdf, 0xe6, 0xf6);
49const GdkColor kHoveredBackgroundColor = GDK_COLOR_RGB(0xef, 0xf2, 0xfa);
50
51const GdkColor kContentTextColor = GDK_COLOR_RGB(0x00, 0x00, 0x00);
52const GdkColor kURLTextColor = GDK_COLOR_RGB(0x00, 0x88, 0x00);
53
54// We have a 1 pixel border around the entire results popup.
55const int kBorderThickness = 1;
56
57// The vertical height of each result.
58const int kHeightPerResult = 24;
59
60// Width of the icons.
61const int kIconWidth = 17;
62
63// We want to vertically center the image in the result space.
64const int kIconTopPadding = 2;
65
66// Space between the left edge (including the border) and the text.
67const int kIconLeftPadding = 3 + kBorderThickness;
68
69// Space between the image and the text.
70const int kIconRightPadding = 5;
71
72// Space between the left edge (including the border) and the text.
73const int kIconAreaWidth =
74    kIconLeftPadding + kIconWidth + kIconRightPadding;
75
76// Space between the right edge (including the border) and the text.
77const 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.
82const float kContentWidthPercentage = 0.7;
83
84// How much to offset the popup from the bottom of the location bar.
85const 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.
89const int kEditFontAdjust = -1;
90
91// UTF-8 Left-to-right embedding.
92const char* kLRE = "\xe2\x80\xaa";
93
94// Return a Rect covering the whole area of |window|.
95gfx::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.
103gfx::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.
111size_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.
119GdkColor 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.
147GdkColor 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
173void 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
266OmniboxPopupViewGtk::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
328OmniboxPopupViewGtk::~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
341bool OmniboxPopupViewGtk::IsOpen() const {
342  return opened_;
343}
344
345void 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
354void 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
365gfx::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
381void 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
387void OmniboxPopupViewGtk::OnDragCanceled() {
388  ignore_mouse_drag_ = true;
389}
390
391void 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
436void 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
456void OmniboxPopupViewGtk::Hide() {
457  gtk_widget_hide(window_);
458  opened_ = false;
459}
460
461void 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
469size_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
474void 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
483gfx::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
525void 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
543gboolean 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
556gboolean 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
567gboolean 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
590gboolean 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