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