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