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.
5 #include "autofill_popup_view_gtk.h"
7 #include <gdk/gdkkeysyms.h>
9 #include "base/i18n/rtl.h"
10 #include "base/logging.h"
11 #include "base/utf_string_conversions.h"
12 #include "chrome/browser/autofill/autofill_external_delegate.h"
13 #include "chrome/browser/ui/gtk/gtk_util.h"
14 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
15 #include "content/public/browser/render_view_host.h"
16 #include "content/public/browser/web_contents.h"
17 #include "grit/ui_resources.h"
18 #include "third_party/WebKit/Source/WebKit/chromium/public/WebAutofillClient.h"
19 #include "ui/base/gtk/gtk_compat.h"
20 #include "ui/base/gtk/gtk_hig_constants.h"
21 #include "ui/base/gtk/gtk_windowing.h"
22 #include "ui/gfx/native_widget_types.h"
23 #include "ui/gfx/pango_util.h"
24 #include "ui/gfx/rect.h"
26 using WebKit::WebAutofillClient
;
29 const GdkColor kBorderColor
= GDK_COLOR_RGB(0xc7, 0xca, 0xce);
30 const GdkColor kHoveredBackgroundColor
= GDK_COLOR_RGB(0xcd, 0xcd, 0xcd);
31 const GdkColor kValueTextColor
= GDK_COLOR_RGB(0x00, 0x00, 0x00);
32 const GdkColor kLabelTextColor
= GDK_COLOR_RGB(0x7f, 0x7f, 0x7f);
34 // The vertical height of each row in pixels.
35 const int kRowHeight
= 24;
37 // The vertical height of a separator in pixels.
38 const int kSeparatorHeight
= 1;
40 // The amount of minimum padding between the Autofill value and label in pixels.
41 const int kLabelPadding
= 15;
43 // The amount of padding between icons in pixels.
44 const int kIconPadding
= 5;
46 // The amount of padding at the end of the popup in pixels.
47 const int kEndPadding
= 3;
49 // We have a 1 pixel border around the entire results popup.
50 const int kBorderThickness
= 1;
52 // Width of the Autofill icons in pixels.
53 const int kAutofillIconWidth
= 25;
55 // Height of the Autofill icons in pixels.
56 const int kAutofillIconHeight
= 16;
58 // Width of the delete icon in pixels.
59 const int kDeleteIconWidth
= 16;
61 // Height of the delete icon in pixels.
62 const int kDeleteIconHeight
= 16;
64 gfx::Rect
GetWindowRect(GdkWindow
* window
) {
65 return gfx::Rect(gdk_window_get_width(window
),
66 gdk_window_get_height(window
));
69 int GetRowHeight(int unique_id
) {
70 if (unique_id
== WebAutofillClient::MenuItemIDSeparator
)
71 return kSeparatorHeight
;
78 AutofillPopupViewGtk::AutofillPopupViewGtk(
79 content::WebContents
* web_contents
,
80 GtkThemeService
* theme_service
,
81 AutofillExternalDelegate
* external_delegate
,
83 : AutofillPopupView(web_contents
, external_delegate
),
85 window_(gtk_window_new(GTK_WINDOW_POPUP
)),
86 theme_service_(theme_service
),
87 render_view_host_(web_contents
->GetRenderViewHost()),
88 delete_icon_selected_(false) {
89 CHECK(parent
!= NULL
);
90 gtk_window_set_resizable(GTK_WINDOW(window_
), FALSE
);
91 gtk_widget_set_app_paintable(window_
, TRUE
);
92 gtk_widget_set_double_buffered(window_
, TRUE
);
94 // Setup the window to ensure it receives the expose event.
95 gtk_widget_add_events(window_
, GDK_BUTTON_MOTION_MASK
|
96 GDK_BUTTON_RELEASE_MASK
|
98 GDK_POINTER_MOTION_MASK
);
99 g_signal_connect(window_
, "expose-event",
100 G_CALLBACK(HandleExposeThunk
), this);
101 g_signal_connect(window_
, "leave-notify-event",
102 G_CALLBACK(HandleLeaveThunk
), this);
103 g_signal_connect(window_
, "motion-notify-event",
104 G_CALLBACK(HandleMotionThunk
), this);
105 g_signal_connect(window_
, "button-release-event",
106 G_CALLBACK(HandleButtonReleaseThunk
), this);
108 // Cache the layout so we don't have to create it for every expose.
109 layout_
= gtk_widget_create_pango_layout(window_
, NULL
);
112 AutofillPopupViewGtk::~AutofillPopupViewGtk() {
113 g_object_unref(layout_
);
114 gtk_widget_destroy(window_
);
117 void AutofillPopupViewGtk::ShowInternal() {
119 gtk_window_move(GTK_WINDOW(window_
), bounds_
.x(), bounds_
.y());
123 render_view_host_
->AddKeyboardListener(this);
125 gtk_widget_show(window_
);
127 GtkWidget
* toplevel
= gtk_widget_get_toplevel(parent_
);
128 CHECK(gtk_widget_is_toplevel(toplevel
));
129 ui::StackPopupWindow(window_
, toplevel
);
132 void AutofillPopupViewGtk::HideInternal() {
133 render_view_host_
->RemoveKeyboardListener(this);
135 gtk_widget_hide(window_
);
138 void AutofillPopupViewGtk::InvalidateRow(size_t row
) {
139 GdkRectangle row_rect
= GetRectForRow(row
, bounds_
.width()).ToGdkRectangle();
140 GdkWindow
* gdk_window
= gtk_widget_get_window(window_
);
141 gdk_window_invalidate_rect(gdk_window
, &row_rect
, FALSE
);
144 void AutofillPopupViewGtk::ResizePopup() {
145 bounds_
.set_width(GetPopupRequiredWidth());
146 bounds_
.set_height(GetPopupRequiredHeight());
148 gtk_widget_set_size_request(window_
, bounds_
.width(), bounds_
.height());
151 gboolean
AutofillPopupViewGtk::HandleButtonRelease(GtkWidget
* widget
,
152 GdkEventButton
* event
) {
153 // We only care about the left click.
154 if (event
->button
!= 1)
157 DCHECK_EQ(selected_line(), LineFromY(event
->y
));
159 if (DeleteIconIsSelected(event
->x
, event
->y
))
160 RemoveSelectedLine();
162 AcceptSelectedLine();
167 gboolean
AutofillPopupViewGtk::HandleExpose(GtkWidget
* widget
,
168 GdkEventExpose
* event
) {
169 gfx::Rect window_rect
= GetWindowRect(event
->window
);
170 gfx::Rect damage_rect
= gfx::Rect(event
->area
);
172 cairo_t
* cr
= gdk_cairo_create(GDK_DRAWABLE(gtk_widget_get_window(widget
)));
173 gdk_cairo_rectangle(cr
, &event
->area
);
176 // This assert is kinda ugly, but it would be more currently unneeded work
177 // to support painting a border that isn't 1 pixel thick. There is no point
178 // in writing that code now, and explode if that day ever comes.
179 COMPILE_ASSERT(kBorderThickness
== 1, border_1px_implied
);
180 // Draw the 1px border around the entire window.
181 gdk_cairo_set_source_color(cr
, &kBorderColor
);
182 cairo_rectangle(cr
, 0, 0, window_rect
.width(), window_rect
.height());
185 SetupLayout(window_rect
);
187 for (size_t i
= 0; i
< autofill_values().size(); ++i
) {
188 gfx::Rect line_rect
= GetRectForRow(i
, window_rect
.width());
189 // Only repaint and layout damaged lines.
190 if (!line_rect
.Intersects(damage_rect
))
193 if (autofill_unique_ids()[i
] == WebAutofillClient::MenuItemIDSeparator
)
194 DrawSeparator(cr
, line_rect
);
196 DrawAutofillEntry(cr
, i
, line_rect
);
204 gboolean
AutofillPopupViewGtk::HandleLeave(GtkWidget
* widget
,
205 GdkEventCrossing
* event
) {
211 gboolean
AutofillPopupViewGtk::HandleMotion(GtkWidget
* widget
,
212 GdkEventMotion
* event
) {
213 // TODO(csharp): Only select a line if the motion is still inside the popup.
214 // http://www.crbug.com/129559
215 int line
= LineFromY(event
->y
);
217 SetSelectedLine(line
);
219 bool delete_icon_selected
= DeleteIconIsSelected(event
->x
, event
->y
);
220 if (delete_icon_selected
!= delete_icon_selected_
) {
221 delete_icon_selected_
= delete_icon_selected
;
222 InvalidateRow(selected_line());
228 bool AutofillPopupViewGtk::HandleKeyPressEvent(GdkEventKey
* event
) {
229 // Filter modifier to only include accelerator modifiers.
230 guint modifier
= event
->state
& gtk_accelerator_get_default_mod_mask();
232 switch (event
->keyval
) {
234 SelectPreviousLine();
243 SetSelectedLine(autofill_values().size() - 1);
250 return (modifier
== GDK_SHIFT_MASK
) && RemoveSelectedLine();
253 return AcceptSelectedLine();
259 void AutofillPopupViewGtk::SetupLayout(const gfx::Rect
& window_rect
) {
260 int allocated_content_width
= window_rect
.width();
261 pango_layout_set_width(layout_
, allocated_content_width
* PANGO_SCALE
);
262 pango_layout_set_height(layout_
, kRowHeight
* PANGO_SCALE
);
265 void AutofillPopupViewGtk::SetLayoutText(const string16
& text
,
266 const gfx::Font
& font
,
267 const GdkColor text_color
) {
268 PangoAttrList
* attrs
= pango_attr_list_new();
270 PangoAttribute
* fg_attr
= pango_attr_foreground_new(text_color
.red
,
273 pango_attr_list_insert(attrs
, fg_attr
); // Ownership taken.
275 pango_layout_set_attributes(layout_
, attrs
); // Ref taken.
276 pango_attr_list_unref(attrs
);
278 gfx::ScopedPangoFontDescription
font_description(font
.GetNativeFont());
279 pango_layout_set_font_description(layout_
, font_description
.get());
281 gtk_util::SetLayoutText(layout_
, text
);
283 // We add one pixel to the width because if the text fills up the width
284 // pango will try to split it over 2 lines.
285 int required_width
= font
.GetStringWidth(text
) + 1;
287 pango_layout_set_width(layout_
, required_width
* PANGO_SCALE
);
290 void AutofillPopupViewGtk::DrawSeparator(cairo_t
* cairo_context
,
291 const gfx::Rect
& separator_rect
) {
292 cairo_save(cairo_context
);
293 cairo_move_to(cairo_context
, 0, separator_rect
.y());
294 cairo_line_to(cairo_context
,
295 separator_rect
.width(),
296 separator_rect
.y() + separator_rect
.height());
297 cairo_stroke(cairo_context
);
298 cairo_restore(cairo_context
);
301 void AutofillPopupViewGtk::DrawAutofillEntry(cairo_t
* cairo_context
,
303 const gfx::Rect
& entry_rect
) {
304 if (selected_line() == static_cast<int>(index
)) {
305 gdk_cairo_set_source_color(cairo_context
, &kHoveredBackgroundColor
);
306 cairo_rectangle(cairo_context
, entry_rect
.x(), entry_rect
.y(),
307 entry_rect
.width(), entry_rect
.height());
308 cairo_fill(cairo_context
);
312 SetLayoutText(autofill_values()[index
], value_font(), kValueTextColor
);
313 int value_text_width
= value_font().GetStringWidth(autofill_values()[index
]);
315 // Center the text within the line.
316 int value_content_y
= std::max(
318 entry_rect
.y() + ((kRowHeight
- value_font().GetHeight()) / 2));
320 bool is_rtl
= base::i18n::IsRTL();
321 cairo_save(cairo_context
);
322 cairo_move_to(cairo_context
,
323 is_rtl
? entry_rect
.width() - value_text_width
- kEndPadding
:
326 pango_cairo_show_layout(cairo_context
, layout_
);
327 cairo_restore(cairo_context
);
329 // Use this to figure out where all the other Autofill items should be placed.
330 int x_align_left
= is_rtl
? kEndPadding
: entry_rect
.width() - kEndPadding
;
332 // Draw the delete icon, if one is needed.
333 if (CanDelete(autofill_unique_ids()[index
])) {
334 x_align_left
+= is_rtl
? 0 : -kDeleteIconWidth
;
336 const gfx::Image
* delete_icon
;
337 if (static_cast<int>(index
) == selected_line() && delete_icon_selected_
)
338 delete_icon
= theme_service_
->GetImageNamed(IDR_CLOSE_BAR_H
);
340 delete_icon
= theme_service_
->GetImageNamed(IDR_CLOSE_BAR
);
342 // TODO(csharp): Create a custom resource for the delete icon.
343 // http://www.crbug.com/131801
344 cairo_save(cairo_context
);
345 gtk_util::DrawFullImage(
350 entry_rect
.y() + ((kRowHeight
- kDeleteIconHeight
) / 2));
351 cairo_restore(cairo_context
);
352 cairo_save(cairo_context
);
354 x_align_left
+= is_rtl
? kDeleteIconWidth
+ kIconPadding
: -kIconPadding
;
357 // Draw the Autofill icon, if one exists
358 if (!autofill_icons()[index
].empty()) {
359 int icon
= GetIconResourceID(autofill_icons()[index
]);
361 int icon_y
= entry_rect
.y() + ((kRowHeight
- kAutofillIconHeight
) / 2);
363 x_align_left
+= is_rtl
? 0 : -kAutofillIconWidth
;
365 cairo_save(cairo_context
);
366 gtk_util::DrawFullImage(cairo_context
,
368 *theme_service_
->GetImageNamed(icon
),
371 cairo_restore(cairo_context
);
373 x_align_left
+= is_rtl
? kAutofillIconWidth
+ kIconPadding
: -kIconPadding
;
376 // Draw the label text.
377 SetLayoutText(autofill_labels()[index
], label_font(), kLabelTextColor
);
379 is_rtl
? 0 : -label_font().GetStringWidth(autofill_labels()[index
]);
381 // Center the text within the line.
382 int label_content_y
= std::max(
384 entry_rect
.y() + ((kRowHeight
- label_font().GetHeight()) / 2));
386 cairo_save(cairo_context
);
387 cairo_move_to(cairo_context
, x_align_left
, label_content_y
);
388 pango_cairo_show_layout(cairo_context
, layout_
);
389 cairo_restore(cairo_context
);
392 void AutofillPopupViewGtk::SetBounds() {
393 gint origin_x
, origin_y
;
394 gdk_window_get_origin(gtk_widget_get_window(parent_
), &origin_x
, &origin_y
);
396 GdkScreen
* screen
= gtk_widget_get_screen(parent_
);
397 gint screen_height
= gdk_screen_get_height(screen
);
399 int bottom_of_field
= origin_y
+ element_bounds().y() +
400 element_bounds().height();
401 int popup_height
= GetPopupRequiredHeight();
403 // Find the correct top position of the popup so that is doesn't go off
405 int top_of_popup
= 0;
406 if (screen_height
< bottom_of_field
+ popup_height
) {
407 // The popup must appear above the field.
408 top_of_popup
= origin_y
+ element_bounds().y() - popup_height
;
410 // The popup can appear below the field.
411 top_of_popup
= bottom_of_field
;
415 origin_x
+ element_bounds().x(),
417 GetPopupRequiredWidth(),
421 int AutofillPopupViewGtk::GetPopupRequiredWidth() {
422 int popup_width
= element_bounds().width();
423 DCHECK_EQ(autofill_values().size(), autofill_labels().size());
424 for (size_t i
= 0; i
< autofill_values().size(); ++i
) {
425 int row_size
= kEndPadding
+
426 value_font().GetStringWidth(autofill_values()[i
]) +
428 label_font().GetStringWidth(autofill_labels()[i
]);
430 // Add the Autofill icon size, if required.
431 if (!autofill_icons()[i
].empty())
432 row_size
+= kAutofillIconWidth
+ kIconPadding
;
434 // Add delete icon, if required.
435 if (CanDelete(autofill_unique_ids()[i
]))
436 row_size
+= kDeleteIconWidth
+ kIconPadding
;
438 // Add the padding at the end
439 row_size
+= kEndPadding
;
441 popup_width
= std::max(popup_width
, row_size
);
447 int AutofillPopupViewGtk::GetPopupRequiredHeight() {
448 int popup_height
= 0;
450 for (size_t i
= 0; i
< autofill_unique_ids().size(); ++i
) {
451 popup_height
+= GetRowHeight(autofill_unique_ids()[i
]);
457 int AutofillPopupViewGtk::LineFromY(int y
) {
458 int current_height
= 0;
460 for (size_t i
= 0; i
< autofill_unique_ids().size(); ++i
) {
461 current_height
+= GetRowHeight(autofill_unique_ids()[i
]);
463 if (y
<= current_height
)
467 // The y value goes beyond the popup so stop the selection at the last line.
468 return autofill_unique_ids().size() - 1;
471 gfx::Rect
AutofillPopupViewGtk::GetRectForRow(size_t row
, int width
) {
473 for (size_t i
= 0; i
< row
; ++i
) {
474 top
+= GetRowHeight(autofill_unique_ids()[i
]);
477 return gfx::Rect(0, top
, width
, GetRowHeight(autofill_unique_ids()[row
]));
480 bool AutofillPopupViewGtk::DeleteIconIsSelected(int x
, int y
) {
481 if (!CanDelete(selected_line()))
485 for (int i
= 0; i
< selected_line(); ++i
) {
486 row_start_y
+= GetRowHeight(autofill_unique_ids()[i
]);
489 gfx::Rect delete_icon_bounds
= gfx::Rect(
490 GetPopupRequiredWidth() - kDeleteIconWidth
- kIconPadding
,
491 row_start_y
+ ((kRowHeight
- kDeleteIconHeight
) / 2),
495 return delete_icon_bounds
.Contains(x
, y
);