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 |