Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(592)

Side by Side Diff: chrome/browser/autofill/autofill_dialog_gtk.cc

Issue 2975003: Makes the auto fill dialogs match the mocks. (Closed)
Patch Set: Moves function declarations out of autofill_dialog.h Created 10 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/browser/autofill/autofill_dialog.h ('k') | chrome/browser/autofill/credit_card.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/autofill/autofill_dialog.h" 5 #include "chrome/browser/autofill/autofill_dialog.h"
6 6
7 #include <gtk/gtk.h> 7 #include <gtk/gtk.h>
8 8
9 #include <algorithm>
9 #include <string> 10 #include <string>
10 #include <vector> 11 #include <vector>
11 12
13 #include "app/gtk_signal.h"
12 #include "app/l10n_util.h" 14 #include "app/l10n_util.h"
13 #include "base/logging.h" 15 #include "base/logging.h"
14 #include "base/message_loop.h" 16 #include "base/message_loop.h"
15 #include "base/utf_string_conversions.h" 17 #include "base/utf_string_conversions.h"
16 #include "chrome/browser/autofill/autofill_manager.h" 18 #include "chrome/browser/autofill/autofill_manager.h"
17 #include "chrome/browser/autofill/autofill_profile.h" 19 #include "chrome/browser/autofill/autofill_profile.h"
18 #include "chrome/browser/autofill/credit_card.h" 20 #include "chrome/browser/autofill/credit_card.h"
19 #include "chrome/browser/autofill/form_group.h" 21 #include "chrome/browser/autofill/form_group.h"
20 #include "chrome/browser/autofill/personal_data_manager.h" 22 #include "chrome/browser/autofill/personal_data_manager.h"
21 #include "chrome/browser/autofill/phone_number.h" 23 #include "chrome/browser/autofill/phone_number.h"
22 #include "chrome/browser/browser.h" 24 #include "chrome/browser/browser.h"
23 #include "chrome/browser/browser_list.h" 25 #include "chrome/browser/browser_list.h"
24 #include "chrome/browser/gtk/gtk_chrome_link_button.h" 26 #include "chrome/browser/gtk/gtk_chrome_link_button.h"
27 #include "chrome/browser/gtk/gtk_tree.h"
25 #include "chrome/browser/gtk/gtk_util.h" 28 #include "chrome/browser/gtk/gtk_util.h"
26 #include "chrome/browser/gtk/options/options_layout_gtk.h" 29 #include "chrome/browser/metrics/user_metrics.h"
30 #include "chrome/browser/pref_member.h"
31 #include "chrome/browser/pref_service.h"
27 #include "chrome/browser/profile.h" 32 #include "chrome/browser/profile.h"
33 #include "chrome/common/notification_details.h"
34 #include "chrome/common/notification_observer.h"
35 #include "chrome/common/notification_type.h"
28 #include "chrome/common/pref_names.h" 36 #include "chrome/common/pref_names.h"
29 #include "gfx/gtk_util.h" 37 #include "gfx/gtk_util.h"
30 #include "grit/chromium_strings.h" 38 #include "grit/chromium_strings.h"
31 #include "grit/generated_resources.h" 39 #include "grit/generated_resources.h"
32 #include "grit/locale_settings.h" 40 #include "grit/locale_settings.h"
33 41
42 // Shows the editor for adding/editing an AutoFillProfile. If
43 // |auto_fill_profile| is NULL, a new AutoFillProfile should be created.
44 void ShowAutoFillProfileEditor(gfx::NativeView parent,
45 AutoFillDialogObserver* observer,
46 Profile* profile,
47 AutoFillProfile* auto_fill_profile);
48
49 // Shows the editor for adding/editing a CreditCard. If |credit_card| is NULL, a
50 // new CreditCard should be created.
51 void ShowAutoFillCreditCardEditor(gfx::NativeView parent,
52 AutoFillDialogObserver* observer,
53 Profile* profile,
54 CreditCard* credit_card);
55
34 namespace { 56 namespace {
35 57
36 // The name of the object property used to store an entry widget pointer on 58 // The resource id for the 'About Autofill' link button.
37 // another widget. 59 const gint kAutoFillDialogAboutLink = 1;
38 const char kButtonDataKey[] = "label-entry";
39
40 // How far we indent dialog widgets, in pixels.
41 const int kAutoFillDialogIndent = 5;
42
43 // The resource id for the 'Learn more' link button.
44 const gint kAutoFillDialogLearnMoreLink = 1;
45
46 // All of these widgets are GtkEntrys.
47 typedef struct _AddressWidgets {
48 GtkWidget* label;
49 GtkWidget* full_name;
50 GtkWidget* email;
51 GtkWidget* company_name;
52 GtkWidget* address_line1;
53 GtkWidget* address_line2;
54 GtkWidget* city;
55 GtkWidget* state;
56 GtkWidget* zipcode;
57 GtkWidget* country;
58 GtkWidget* phone;
59 GtkWidget* fax;
60 } AddressWidgets;
61
62 // All of these widgets are GtkEntrys except for |billing_address|, which is
63 // a GtkComboBox.
64 typedef struct _CreditCardWidgets {
65 GtkWidget* label;
66 GtkWidget* name_on_card;
67 GtkWidget* card_number;
68 GtkWidget* expiration_month;
69 GtkWidget* expiration_year;
70 GtkWidget* billing_address;
71 GtkWidget* phone;
72 string16 original_card_number;
73 } CreditCardWidgets;
74
75 // Adds an alignment around |widget| which indents the widget by |offset|.
76 GtkWidget* IndentWidget(GtkWidget* widget, int offset) {
77 GtkWidget* alignment = gtk_alignment_new(0, 0, 0, 0);
78 gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), 0, 0,
79 offset, 0);
80 gtk_container_add(GTK_CONTAINER(alignment), widget);
81 return alignment;
82 }
83
84 // Makes sure we use the gtk theme colors by loading the base color of an entry
85 // widget.
86 void SetWhiteBackground(GtkWidget* widget) {
87 GtkWidget* entry = gtk_entry_new();
88 gtk_widget_ensure_style(entry);
89 GtkStyle* style = gtk_widget_get_style(entry);
90 gtk_widget_modify_bg(widget, GTK_STATE_NORMAL,
91 &style->base[GTK_STATE_NORMAL]);
92 gtk_widget_destroy(entry);
93 }
94
95 string16 GetEntryText(GtkWidget* entry) {
96 return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(entry)));
97 }
98
99 void SetEntryText(GtkWidget* entry, const string16& text) {
100 gtk_entry_set_text(GTK_ENTRY(entry), UTF16ToUTF8(text).c_str());
101 }
102
103 void SetButtonData(GtkWidget* widget, GtkWidget* entry) {
104 g_object_set_data(G_OBJECT(widget), kButtonDataKey, entry);
105 }
106
107 GtkWidget* GetButtonData(GtkWidget* widget) {
108 return static_cast<GtkWidget*>(
109 g_object_get_data(G_OBJECT(widget), kButtonDataKey));
110 }
111
112 ////////////////////////////////////////////////////////////////////////////////
113 // Form Table helpers.
114 //
115 // The following functions can be used to create a form with labeled widgets.
116 //
117
118 // Creates a form table with dimensions |rows| x |cols|.
119 GtkWidget* InitFormTable(int rows, int cols) {
120 // We have two table rows per form table row.
121 GtkWidget* table = gtk_table_new(rows * 2, cols, false);
122 gtk_table_set_row_spacings(GTK_TABLE(table), gtk_util::kControlSpacing);
123 gtk_table_set_col_spacings(GTK_TABLE(table), gtk_util::kFormControlSpacing);
124
125 // Leave no space between the label and the widget.
126 for (int i = 0; i < rows; i++)
127 gtk_table_set_row_spacing(GTK_TABLE(table), i * 2, 0);
128
129 return table;
130 }
131
132 // Sets the label of the form widget at |row|,|col|. The label is |len| columns
133 // long.
134 void FormTableSetLabel(
135 GtkWidget* table, int row, int col, int len, int label_id) {
136 // We have two table rows per form table row.
137 row *= 2;
138
139 std::string text;
140 if (label_id)
141 text = l10n_util::GetStringUTF8(label_id);
142 GtkWidget* label = gtk_label_new(text.c_str());
143 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
144 gtk_table_attach(GTK_TABLE(table), label,
145 col, col + len, // Left col, right col.
146 row, row + 1, // Top row, bottom row.
147 GTK_FILL, GTK_FILL, // Options.
148 0, 0); // Padding.
149 }
150
151 // Sets the form widget at |row|,|col|. The widget fills up |len| columns. If
152 // |expand| is true, the widget will expand to fill all of the extra space in
153 // the table row.
154 void FormTableSetWidget(GtkWidget* table,
155 GtkWidget* widget,
156 int row, int col,
157 int len, bool expand) {
158 const GtkAttachOptions expand_option =
159 static_cast<GtkAttachOptions>(GTK_FILL | GTK_EXPAND);
160 GtkAttachOptions xoption = (expand) ? expand_option : GTK_FILL;
161
162 // We have two table rows per form table row.
163 row *= 2;
164 gtk_table_attach(GTK_TABLE(table), widget,
165 col, col + len, // Left col, right col.
166 row + 1, row + 2, // Top row, bottom row.
167 xoption, GTK_FILL, // Options.
168 0, 0); // Padding.
169 }
170
171 // Adds a labeled entry box to the form table at |row|,|col|. The entry widget
172 // fills up |len| columns. The returned widget is owned by |table| and should
173 // not be destroyed.
174 GtkWidget* FormTableAddEntry(
175 GtkWidget* table, int row, int col, int len, int label_id) {
176 FormTableSetLabel(table, row, col, len, label_id);
177
178 GtkWidget* entry = gtk_entry_new();
179 FormTableSetWidget(table, entry, row, col, len, false);
180
181 return entry;
182 }
183
184 // Adds a labeled entry box to the form table that will expand to fill extra
185 // space in the table row.
186 GtkWidget* FormTableAddExpandedEntry(
187 GtkWidget* table, int row, int col, int len, int label_id) {
188 FormTableSetLabel(table, row, col, len, label_id);
189
190 GtkWidget* entry = gtk_entry_new();
191 FormTableSetWidget(table, entry, row, col, len, true);
192
193 return entry;
194 }
195
196 // Adds a sized entry box to the form table. The entry widget width is set to
197 // |char_len|.
198 GtkWidget* FormTableAddSizedEntry(
199 GtkWidget* table, int row, int col, int char_len, int label_id) {
200 GtkWidget* entry = FormTableAddEntry(table, row, col, 1, label_id);
201 gtk_entry_set_width_chars(GTK_ENTRY(entry), char_len);
202 return entry;
203 }
204
205 // Like FormTableAddEntry, but connects to the 'changed' signal. |changed| is a
206 // callback to handle the 'changed' signal that is emitted when the user edits
207 // the entry. |expander| is the expander widget that will be sent to the
208 // callback as the user data.
209 GtkWidget* FormTableAddLabelEntry(
210 GtkWidget* table, int row, int col, int len, int label_id,
211 GtkWidget* expander, GCallback changed) {
212 FormTableSetLabel(table, row, col, len, label_id);
213
214 GtkWidget* entry = gtk_entry_new();
215 g_signal_connect(entry, "changed", changed, expander);
216 FormTableSetWidget(table, entry, row, col, len, false);
217
218 return entry;
219 }
220
221 } // namespace
222 60
223 //////////////////////////////////////////////////////////////////////////////// 61 ////////////////////////////////////////////////////////////////////////////////
224 // AutoFillDialog 62 // AutoFillDialog
225 // 63 //
226 // The contents of the AutoFill dialog. This dialog allows users to add, edit 64 // The contents of the AutoFill dialog. This dialog allows users to add, edit
227 // and remove AutoFill profiles. 65 // and remove AutoFill profiles.
228 class AutoFillDialog : public PersonalDataManager::Observer { 66 class AutoFillDialog : public PersonalDataManager::Observer,
67 public NotificationObserver {
229 public: 68 public:
230 AutoFillDialog(AutoFillDialogObserver* observer, 69 // Identifiers for columns in the store.
231 PersonalDataManager* personal_data_manager, 70 enum Column {
232 AutoFillProfile* imported_profile, 71 COL_TITLE = 0,
233 CreditCard* imported_credit_card); 72 COL_IS_HEADER,
73 COL_IS_SEPARATOR, // Identifies an empty row used to reserve visual space.
74 COL_WEIGHT,
75 COL_WEIGHT_SET,
76 COL_COUNT
77 };
78
79 // Used to identify the selection. See GetSelectionType.
80 enum SelectionType {
81 // Nothing is selected.
82 SELECTION_EMPTY = 0,
83
84 // At least one header/separator row is selected.
85 SELECTION_HEADER = 1 << 0,
86
87 // At least one non-header/separator row is selected.
88 SELECTION_SINGLE = 1 << 1,
89
90 // Multiple non-header/separator rows are selected.
91 SELECTION_MULTI = 1 << 2,
92 };
93
94 AutoFillDialog(Profile* profile, AutoFillDialogObserver* observer);
234 ~AutoFillDialog(); 95 ~AutoFillDialog();
235 96
236 // PersonalDataManager::Observer implementation: 97 // PersonalDataManager::Observer implementation:
237 void OnPersonalDataLoaded(); 98 void OnPersonalDataLoaded();
99 void OnPersonalDataChanged();
100
101 // NotificationObserver implementation:
102 virtual void Observe(NotificationType type,
103 const NotificationSource& source,
104 const NotificationDetails& details);
238 105
239 // Shows the AutoFill dialog. 106 // Shows the AutoFill dialog.
240 void Show(); 107 void Show();
241 108
242 private: 109 private:
243 // 'destroy' signal handler. Calls DeleteSoon on the global singleton dialog 110 // 'destroy' signal handler. Calls DeleteSoon on the global singleton dialog
244 // object. 111 // object.
245 static void OnDestroy(GtkWidget* widget, AutoFillDialog* autofill_dialog); 112 CHROMEGTK_CALLBACK_0(AutoFillDialog, void, OnDestroy);
246 113
247 // 'response' signal handler. Notifies the AutoFillDialogObserver that new 114 // 'response' signal handler. Notifies the AutoFillDialogObserver that new
248 // data is available if the response is GTK_RESPONSE_APPLY or GTK_RESPONSE_OK. 115 // data is available if the response is GTK_RESPONSE_APPLY or GTK_RESPONSE_OK.
249 // We close the dialog if the response is GTK_RESPONSE_OK or 116 // We close the dialog if the response is GTK_RESPONSE_OK or
250 // GTK_RESPONSE_CANCEL. 117 // GTK_RESPONSE_CANCEL.
251 static void OnResponse(GtkDialog* dialog, gint response_id, 118 CHROMEG_CALLBACK_1(AutoFillDialog, void, OnResponse, GtkDialog*, gint);
252 AutoFillDialog* autofill_dialog);
253 119
254 // 'clicked' signal handler. Adds a new address. 120 CHROMEGTK_CALLBACK_0(AutoFillDialog, void, OnAutoFillCheckToggled);
255 static void OnAddAddressClicked(GtkButton* button, AutoFillDialog* dialog); 121 CHROMEG_CALLBACK_2(AutoFillDialog, void, OnRowActivated, GtkTreeView*,
256 122 GtkTreePath*, GtkTreeViewColumn*);
257 // 'clicked' signal handler. Adds a new credit card. 123 CHROMEG_CALLBACK_0(AutoFillDialog, void, OnSelectionChanged,
258 static void OnAddCreditCardClicked(GtkButton* button, AutoFillDialog* dialog); 124 GtkTreeSelection*);
259 125 CHROMEG_CALLBACK_1(AutoFillDialog, gboolean, OnCheckRowIsSeparator,
260 // 'clicked' signal handler. Deletes the associated address. 126 GtkTreeModel*, GtkTreeIter*);
261 static void OnDeleteAddressClicked(GtkButton* button, AutoFillDialog* dialog); 127 CHROMEGTK_CALLBACK_0(AutoFillDialog, void, OnAddAddress);
262 128 CHROMEGTK_CALLBACK_0(AutoFillDialog, void, OnAddCreditCard);
263 // 'clicked' signal handler. Deletes the associated credit card. 129 CHROMEGTK_CALLBACK_0(AutoFillDialog, void, OnEdit);
264 static void OnDeleteCreditCardClicked(GtkButton* button, 130 CHROMEGTK_CALLBACK_0(AutoFillDialog, void, OnRemove);
265 AutoFillDialog* dialog); 131 CHROMEG_CALLBACK_3(AutoFillDialog, gboolean, OnSelectionFilter,
266 132 GtkTreeSelection*, GtkTreeModel*, GtkTreePath*, gboolean);
267 // 'changed' signal handler. Updates the title of the expander widget with
268 // the contents of the label entry widget.
269 static void OnLabelChanged(GtkEntry* label, GtkWidget* expander);
270 133
271 // Opens the 'Learn more' link in a new foreground tab. 134 // Opens the 'Learn more' link in a new foreground tab.
272 void OnLinkActivated(); 135 void OnLinkActivated();
273 136
274 // Loads AutoFill profiles and credit cards using the PersonalDataManager. 137 // Loads AutoFill profiles and credit cards using the PersonalDataManager.
275 void LoadAutoFillData(); 138 void LoadAutoFillData();
276 139
277 // Creates the dialog UI widgets. 140 // Creates the dialog UI widgets.
278 void InitializeWidgets(); 141 void InitializeWidgets();
279 142
280 // Initializes the group widgets and returns their container. |name_id| is 143 // Updates the state of the various widgets dependant upon the state of the
281 // the resource ID of the group label. |button_id| is the resource name of 144 // selection, loaded state and whether AutoFill is enabled.
282 // the button label. |clicked_callback| is a callback that handles the 145 void UpdateWidgetState();
283 // 'clicked' signal emitted when the user presses the 'Add' button.
284 GtkWidget* InitGroup(int label_id,
285 int button_id,
286 GCallback clicked_callback);
287 146
288 // Initializes the expander, frame and table widgets used to hold the address 147 // Returns a bitmask of the selection types.
289 // and credit card forms. |name_id| is the resource id of the label of the 148 int GetSelectionType();
290 // expander widget. The content vbox widget is returned in |content_vbox|.
291 // Returns the expander widget.
292 GtkWidget* InitGroupContentArea(int name_id, GtkWidget** content_vbox);
293 149
294 // Returns a GtkExpander that is added to the appropriate vbox. Each method 150 void AddAddressToTree(const AutoFillProfile& profile, GtkTreeIter* iter);
295 // adds the necessary widgets and layout required to fill out information
296 // for either an address or a credit card. The expander will be expanded by
297 // default if |expand| is true.
298 GtkWidget* AddNewAddress(bool expand);
299 GtkWidget* AddNewCreditCard(bool expand);
300 151
301 // Adds a new address filled out with information from |profile|. 152 void AddCreditCardToTree(const CreditCard& credit_card, GtkTreeIter* iter);
302 void AddAddress(const AutoFillProfile& profile);
303 153
304 // Adds a new credit card filled out with information from |credit_card|. 154 // Returns the set of selected profiles and cards. The values placed in
305 void AddCreditCard(const CreditCard& credit_card); 155 // the specified vectors are owned by PersonalDataManager.
156 void GetSelectedEntries(std::vector<AutoFillProfile*>* profiles,
157 std::vector<CreditCard*>* cards);
306 158
307 // Returns the index of |billing_address| in the list of profiles. Returns -1 159 Profile* profile_;
308 // if the address is not found.
309 int FindIndexOfAddress(string16 billing_address);
310 160
311 // Our observer. May not be NULL. 161 // Our observer. May not be NULL.
312 AutoFillDialogObserver* observer_; 162 AutoFillDialogObserver* observer_;
313 163
314 // The personal data manager, used to load AutoFill profiles and credit cards. 164 // The personal data manager, used to load AutoFill profiles and credit cards.
315 // Unowned pointer, may not be NULL. 165 // Unowned pointer, may not be NULL.
316 PersonalDataManager* personal_data_; 166 PersonalDataManager* personal_data_;
317 167
318 // The imported profile. May be NULL. 168 // Number of profiles we're displaying.
319 AutoFillProfile* imported_profile_; 169 int profile_count_;
320
321 // The imported credit card. May be NULL.
322 CreditCard* imported_credit_card_;
323
324 // The list of current AutoFill profiles.
325 std::vector<AutoFillProfile> profiles_;
326
327 // The list of current AutoFill credit cards.
328 std::vector<CreditCard> credit_cards_;
329
330 // The list of address widgets, used to modify the AutoFill profiles.
331 std::vector<AddressWidgets> address_widgets_;
332
333 // The list of credit card widgets, used to modify the stored credit cards.
334 std::vector<CreditCardWidgets> credit_card_widgets_;
335 170
336 // The AutoFill dialog. 171 // The AutoFill dialog.
337 GtkWidget* dialog_; 172 GtkWidget* dialog_;
338 173
339 // The addresses group. 174 BooleanPrefMember enable_form_autofill_;
340 GtkWidget* addresses_vbox_;
341 175
342 // The credit cards group. 176 GtkWidget* form_autofill_enable_check_;
343 GtkWidget* creditcards_vbox_; 177
178 // Displays the addresses then credit cards.
179 GtkListStore* list_store_;
180
181 // Displays the list_store_.
182 GtkWidget* tree_;
183
184 GtkWidget* add_address_button_;
185 GtkWidget* add_credit_card_button_;
186 GtkWidget* edit_button_;
187 GtkWidget* remove_button_;
344 188
345 DISALLOW_COPY_AND_ASSIGN(AutoFillDialog); 189 DISALLOW_COPY_AND_ASSIGN(AutoFillDialog);
346 }; 190 };
347 191
348 // The singleton AutoFill dialog object. 192 // The singleton AutoFill dialog object.
349 static AutoFillDialog* dialog = NULL; 193 static AutoFillDialog* dialog = NULL;
350 194
351 AutoFillDialog::AutoFillDialog(AutoFillDialogObserver* observer, 195 AutoFillDialog::AutoFillDialog(Profile* profile,
352 PersonalDataManager* personal_data_manager, 196 AutoFillDialogObserver* observer)
353 AutoFillProfile* imported_profile, 197 : profile_(profile),
354 CreditCard* imported_credit_card) 198 observer_(observer),
355 : observer_(observer), 199 personal_data_(profile->GetPersonalDataManager()),
356 personal_data_(personal_data_manager), 200 profile_count_(0) {
357 imported_profile_(imported_profile),
358 imported_credit_card_(imported_credit_card) {
359 DCHECK(observer_); 201 DCHECK(observer_);
360 DCHECK(personal_data_); 202 DCHECK(personal_data_);
361 203
204 enable_form_autofill_.Init(prefs::kAutoFillEnabled, profile->GetPrefs(),
205 this);
206
207 personal_data_->SetObserver(this);
208
362 InitializeWidgets(); 209 InitializeWidgets();
363 LoadAutoFillData(); 210 LoadAutoFillData();
364 211
365 gtk_util::ShowDialogWithLocalizedSize(dialog_, 212 gtk_util::ShowDialogWithLocalizedSize(dialog_,
366 IDS_AUTOFILL_DIALOG_WIDTH_CHARS, 213 IDS_AUTOFILL_DIALOG_WIDTH_CHARS,
367 IDS_AUTOFILL_DIALOG_HEIGHT_LINES, 214 IDS_AUTOFILL_DIALOG_HEIGHT_LINES,
368 true); 215 true);
369 } 216 }
370 217
371 AutoFillDialog::~AutoFillDialog() { 218 AutoFillDialog::~AutoFillDialog() {
372 // Removes observer if we are observing Profile load. Does nothing otherwise. 219 // Removes observer if we are observing Profile load. Does nothing otherwise.
373 if (personal_data_) 220 personal_data_->RemoveObserver(this);
374 personal_data_->RemoveObserver(this);
375 } 221 }
376 222
377 ///////////////////////////////////////////////////////////////////////////// 223 /////////////////////////////////////////////////////////////////////////////
378 // PersonalDataManager::Observer implementation: 224 // PersonalDataManager::Observer implementation:
379 void AutoFillDialog::OnPersonalDataLoaded() { 225 void AutoFillDialog::OnPersonalDataLoaded() {
380 personal_data_->RemoveObserver(this);
381 LoadAutoFillData(); 226 LoadAutoFillData();
382 } 227 }
383 228
229 void AutoFillDialog::OnPersonalDataChanged() {
230 LoadAutoFillData();
231 }
232
233 void AutoFillDialog::Observe(NotificationType type,
234 const NotificationSource& source,
235 const NotificationDetails& details) {
236 DCHECK_EQ(NotificationType::PREF_CHANGED, type.value);
237 const std::wstring* pref_name = Details<std::wstring>(details).ptr();
238 if (!pref_name || *pref_name == prefs::kAutoFillEnabled) {
239 gtk_toggle_button_set_active(
240 GTK_TOGGLE_BUTTON(form_autofill_enable_check_),
241 enable_form_autofill_.GetValue() ? TRUE : FALSE);
242 UpdateWidgetState();
243 }
244 }
245
384 void AutoFillDialog::Show() { 246 void AutoFillDialog::Show() {
385 gtk_util::PresentWindow(dialog_, gtk_get_current_event_time()); 247 gtk_util::PresentWindow(dialog_, gtk_get_current_event_time());
386 } 248 }
387 249
388 // static 250 void AutoFillDialog::OnDestroy(GtkWidget* widget) {
389 void AutoFillDialog::OnDestroy(GtkWidget* widget,
390 AutoFillDialog* autofill_dialog) {
391 dialog = NULL; 251 dialog = NULL;
392 MessageLoop::current()->DeleteSoon(FROM_HERE, autofill_dialog); 252 MessageLoop::current()->DeleteSoon(FROM_HERE, this);
393 } 253 }
394 254
395 static AutoFillProfile AutoFillProfileFromWidgetValues( 255 void AutoFillDialog::OnResponse(GtkDialog* dialog, gint response_id) {
396 const AddressWidgets& widgets) {
397 // TODO(jhawkins): unique id?
398 AutoFillProfile profile(GetEntryText(widgets.label), 0);
399 profile.SetInfo(AutoFillType(NAME_FULL),
400 GetEntryText(widgets.full_name));
401 profile.SetInfo(AutoFillType(EMAIL_ADDRESS),
402 GetEntryText(widgets.email));
403 profile.SetInfo(AutoFillType(COMPANY_NAME),
404 GetEntryText(widgets.company_name));
405 profile.SetInfo(AutoFillType(ADDRESS_HOME_LINE1),
406 GetEntryText(widgets.address_line1));
407 profile.SetInfo(AutoFillType(ADDRESS_HOME_LINE2),
408 GetEntryText(widgets.address_line2));
409 profile.SetInfo(AutoFillType(ADDRESS_HOME_CITY),
410 GetEntryText(widgets.city));
411 profile.SetInfo(AutoFillType(ADDRESS_HOME_STATE),
412 GetEntryText(widgets.state));
413 profile.SetInfo(AutoFillType(ADDRESS_HOME_ZIP),
414 GetEntryText(widgets.zipcode));
415 profile.SetInfo(AutoFillType(ADDRESS_HOME_COUNTRY),
416 GetEntryText(widgets.country));
417
418 string16 number, city_code, country_code;
419 PhoneNumber::ParsePhoneNumber(
420 GetEntryText(widgets.phone), &number, &city_code, &country_code);
421 profile.SetInfo(AutoFillType(PHONE_HOME_COUNTRY_CODE), country_code);
422 profile.SetInfo(AutoFillType(PHONE_HOME_CITY_CODE), city_code);
423 profile.SetInfo(AutoFillType(PHONE_HOME_NUMBER), number);
424
425 PhoneNumber::ParsePhoneNumber(
426 GetEntryText(widgets.fax), &number, &city_code, &country_code);
427 profile.SetInfo(AutoFillType(PHONE_FAX_COUNTRY_CODE), country_code);
428 profile.SetInfo(AutoFillType(PHONE_FAX_CITY_CODE), city_code);
429 profile.SetInfo(AutoFillType(PHONE_FAX_NUMBER), number);
430
431 return profile;
432 }
433
434 static CreditCard CreditCardFromWidgetValues(
435 const CreditCardWidgets& widgets) {
436 // TODO(jhawkins): unique id?
437 CreditCard credit_card(GetEntryText(widgets.label), 0);
438 credit_card.SetInfo(AutoFillType(CREDIT_CARD_NAME),
439 GetEntryText(widgets.name_on_card));
440 credit_card.SetInfo(AutoFillType(CREDIT_CARD_EXP_MONTH),
441 GetEntryText(widgets.expiration_month));
442 credit_card.SetInfo(AutoFillType(CREDIT_CARD_EXP_4_DIGIT_YEAR),
443 GetEntryText(widgets.expiration_year));
444
445 // If the CC number starts with an asterisk, then we know that the user has
446 // not modified the credit card number at the least, so use the original CC
447 // number in this case.
448 string16 cc_number = GetEntryText(widgets.card_number);
449 if (!cc_number.empty() && cc_number[0] == '*')
450 credit_card.SetInfo(AutoFillType(CREDIT_CARD_NUMBER),
451 widgets.original_card_number);
452 else
453 credit_card.SetInfo(AutoFillType(CREDIT_CARD_NUMBER),
454 GetEntryText(widgets.card_number));
455
456 std::string text =
457 gtk_combo_box_get_active_text(GTK_COMBO_BOX(widgets.billing_address));
458 if (text !=
459 l10n_util::GetStringUTF8(IDS_AUTOFILL_DIALOG_CHOOSE_EXISTING_ADDRESS)) {
460 // TODO(jhawkins): Should we validate the billing address combobox?
461 credit_card.set_billing_address(UTF8ToUTF16(text));
462 }
463
464 return credit_card;
465 }
466
467 // static
468 void AutoFillDialog::OnResponse(GtkDialog* dialog, gint response_id,
469 AutoFillDialog* autofill_dialog) {
470 if (response_id == GTK_RESPONSE_APPLY || response_id == GTK_RESPONSE_OK) {
471 autofill_dialog->profiles_.clear();
472 for (std::vector<AddressWidgets>::const_iterator iter =
473 autofill_dialog->address_widgets_.begin();
474 iter != autofill_dialog->address_widgets_.end();
475 ++iter) {
476 AutoFillProfile profile = AutoFillProfileFromWidgetValues(*iter);
477 autofill_dialog->profiles_.push_back(profile);
478 }
479
480 autofill_dialog->credit_cards_.clear();
481 for (std::vector<CreditCardWidgets>::const_iterator iter =
482 autofill_dialog->credit_card_widgets_.begin();
483 iter != autofill_dialog->credit_card_widgets_.end();
484 ++iter) {
485 CreditCard credit_card = CreditCardFromWidgetValues(*iter);
486 autofill_dialog->credit_cards_.push_back(credit_card);
487 }
488
489 autofill_dialog->observer_->OnAutoFillDialogApply(
490 &autofill_dialog->profiles_,
491 &autofill_dialog->credit_cards_);
492 }
493
494 if (response_id == GTK_RESPONSE_OK || 256 if (response_id == GTK_RESPONSE_OK ||
495 response_id == GTK_RESPONSE_CANCEL || 257 response_id == GTK_RESPONSE_CANCEL ||
496 response_id == GTK_RESPONSE_DELETE_EVENT) { 258 response_id == GTK_RESPONSE_DELETE_EVENT) {
497 gtk_widget_destroy(GTK_WIDGET(dialog)); 259 gtk_widget_destroy(GTK_WIDGET(dialog));
498 } 260 }
499 261
500 if (response_id == kAutoFillDialogLearnMoreLink) 262 if (response_id == kAutoFillDialogAboutLink)
501 autofill_dialog->OnLinkActivated(); 263 OnLinkActivated();
502 } 264 }
503 265
504 // static 266 void AutoFillDialog::OnAutoFillCheckToggled(GtkWidget* widget) {
505 void AutoFillDialog::OnAddAddressClicked(GtkButton* button, 267 bool enabled = gtk_toggle_button_get_active(
506 AutoFillDialog* dialog) { 268 GTK_TOGGLE_BUTTON(form_autofill_enable_check_));
507 GtkWidget* new_address = dialog->AddNewAddress(true); 269 if (enabled) {
508 gtk_box_pack_start(GTK_BOX(dialog->addresses_vbox_), new_address, 270 UserMetrics::RecordAction(UserMetricsAction("Options_FormAutofill_Enable"),
509 FALSE, FALSE, 0); 271 profile_);
510 gtk_widget_show_all(new_address); 272 } else {
273 UserMetrics::RecordAction(UserMetricsAction("Options_FormAutofill_Disable"),
274 profile_);
275 }
276 enable_form_autofill_.SetValue(enabled);
277 profile_->GetPrefs()->ScheduleSavePersistentPrefs();
278 UpdateWidgetState();
511 } 279 }
512 280
513 // static 281 void AutoFillDialog::OnRowActivated(GtkTreeView* tree_view,
514 void AutoFillDialog::OnAddCreditCardClicked(GtkButton* button, 282 GtkTreePath* path,
515 AutoFillDialog* dialog) { 283 GtkTreeViewColumn* column) {
516 GtkWidget* new_creditcard = dialog->AddNewCreditCard(true); 284 if (GetSelectionType() == SELECTION_SINGLE)
517 gtk_box_pack_start(GTK_BOX(dialog->creditcards_vbox_), new_creditcard, 285 OnEdit(NULL);
518 FALSE, FALSE, 0);
519 gtk_widget_show_all(new_creditcard);
520 } 286 }
521 287
522 // static 288 void AutoFillDialog::OnSelectionChanged(GtkTreeSelection* selection) {
523 void AutoFillDialog::OnDeleteAddressClicked(GtkButton* button, 289 UpdateWidgetState();
524 AutoFillDialog* dialog) { 290 }
525 GtkWidget* entry = GetButtonData(GTK_WIDGET(button));
526 string16 label = GetEntryText(entry);
527 291
528 // TODO(jhawkins): Base this on ID. 292 gboolean AutoFillDialog::OnCheckRowIsSeparator(GtkTreeModel* model,
293 GtkTreeIter* iter) {
294 gboolean is_separator;
295 gtk_tree_model_get(model, iter, COL_IS_SEPARATOR, &is_separator, -1);
296 return is_separator;
297 }
529 298
530 // Remove the profile. 299 void AutoFillDialog::OnAddAddress(GtkWidget* widget) {
531 for (std::vector<AutoFillProfile>::iterator iter = dialog->profiles_.begin(); 300 ShowAutoFillProfileEditor(NULL, observer_, profile_, NULL);
532 iter != dialog->profiles_.end(); 301 }
533 ++iter) { 302
534 if (iter->Label() == label) { 303 void AutoFillDialog::OnAddCreditCard(GtkWidget* widget) {
535 dialog->profiles_.erase(iter); 304 ShowAutoFillCreditCardEditor(NULL, observer_, profile_, NULL);
536 break; 305 }
306
307 void AutoFillDialog::OnEdit(GtkWidget* widget) {
308 DCHECK_EQ(SELECTION_SINGLE, GetSelectionType());
309
310 std::vector<AutoFillProfile*> profiles;
311 std::vector<CreditCard*> cards;
312
313 GetSelectedEntries(&profiles, &cards);
314
315 if (profiles.size() == 1)
316 ShowAutoFillProfileEditor(dialog_, observer_, profile_, profiles[0]);
317 else if (cards.size() == 1)
318 ShowAutoFillCreditCardEditor(dialog_, observer_, profile_, cards[0]);
319 }
320
321 void AutoFillDialog::OnRemove(GtkWidget* widget) {
322 PersonalDataManager* data_manager = profile_->GetPersonalDataManager();
323 std::vector<AutoFillProfile*> selected_profiles;
324 std::vector<CreditCard*> selected_cards;
325
326 GetSelectedEntries(&selected_profiles, &selected_cards);
327
328 std::vector<AutoFillProfile> profiles;
329 for (std::vector<AutoFillProfile*>::const_iterator i =
330 data_manager->profiles().begin();
331 i != data_manager->profiles().end(); ++i) {
332 if (std::find(selected_profiles.begin(), selected_profiles.end(), *i) ==
333 selected_profiles.end()) {
334 profiles.push_back(**i);
537 } 335 }
538 } 336 }
539 337
540 // Remove the set of address widgets. 338 std::vector<CreditCard> cards;
541 for (std::vector<AddressWidgets>::iterator iter = 339 for (std::vector<CreditCard*>::const_iterator i =
542 dialog->address_widgets_.begin(); 340 data_manager->credit_cards().begin();
543 iter != dialog->address_widgets_.end(); 341 i != data_manager->credit_cards().end(); ++i) {
544 ++iter) { 342 if (std::find(selected_cards.begin(), selected_cards.end(), *i) ==
545 if (iter->label == entry) { 343 selected_cards.end()) {
546 dialog->address_widgets_.erase(iter); 344 cards.push_back(**i);
547 break;
548 } 345 }
549 } 346 }
550 347
551 // Get back to the expander widget. 348 observer_->OnAutoFillDialogApply(&profiles, &cards);
552 GtkWidget* expander = gtk_widget_get_ancestor(GTK_WIDGET(button),
553 GTK_TYPE_EXPANDER);
554 DCHECK(expander);
555
556 // Destroying the widget will also remove it from the parent container.
557 gtk_widget_destroy(expander);
558 } 349 }
559 350
560 // static 351 gboolean AutoFillDialog::OnSelectionFilter(GtkTreeSelection* selection,
561 void AutoFillDialog::OnDeleteCreditCardClicked(GtkButton* button, 352 GtkTreeModel* model,
562 AutoFillDialog* dialog) { 353 GtkTreePath* path,
563 GtkWidget* entry = GetButtonData(GTK_WIDGET(button)); 354 gboolean path_currently_selected) {
564 string16 label = GetEntryText(entry); 355 GtkTreeIter iter;
565 356 if (!gtk_tree_model_get_iter(model, &iter, path)) {
566 // TODO(jhawkins): Base this on ID. 357 NOTREACHED();
567 358 return TRUE;
568 // Remove the credit card.
569 for (std::vector<CreditCard>::iterator iter = dialog->credit_cards_.begin();
570 iter != dialog->credit_cards_.end();
571 ++iter) {
572 if (iter->Label() == label) {
573 dialog->credit_cards_.erase(iter);
574 break;
575 }
576 } 359 }
577 360 gboolean is_header;
578 // Remove the set of credit widgets. 361 gboolean is_separator;
579 for (std::vector<CreditCardWidgets>::iterator iter = 362 gtk_tree_model_get(model, &iter, COL_IS_HEADER, &is_header,
580 dialog->credit_card_widgets_.begin(); 363 COL_IS_SEPARATOR, &is_separator, -1);
581 iter != dialog->credit_card_widgets_.end(); 364 return !is_header && !is_separator;
582 ++iter) {
583 if (iter->label == entry) {
584 dialog->credit_card_widgets_.erase(iter);
585 break;
586 }
587 }
588
589 // Get back to the expander widget.
590 GtkWidget* expander = gtk_widget_get_ancestor(GTK_WIDGET(button),
591 GTK_TYPE_EXPANDER);
592 DCHECK(expander);
593
594 // Destroying the widget will also remove it from the parent container.
595 gtk_widget_destroy(expander);
596 }
597
598 // static
599 void AutoFillDialog::OnLabelChanged(GtkEntry* label, GtkWidget* expander) {
600 gtk_expander_set_label(GTK_EXPANDER(expander), gtk_entry_get_text(label));
601 } 365 }
602 366
603 void AutoFillDialog::OnLinkActivated() { 367 void AutoFillDialog::OnLinkActivated() {
604 Browser* browser = BrowserList::GetLastActive(); 368 Browser* browser = BrowserList::GetLastActive();
605 browser->OpenURL(GURL(kAutoFillLearnMoreUrl), GURL(), NEW_FOREGROUND_TAB, 369 browser->OpenURL(GURL(kAutoFillLearnMoreUrl), GURL(), NEW_FOREGROUND_TAB,
606 PageTransition::TYPED); 370 PageTransition::TYPED);
607 } 371 }
608 372
609 void AutoFillDialog::LoadAutoFillData() { 373 void AutoFillDialog::LoadAutoFillData() {
610 if (!personal_data_->IsDataLoaded()) { 374 if (!personal_data_->IsDataLoaded()) {
611 personal_data_->SetObserver(this); 375 UpdateWidgetState();
612 return; 376 return;
613 } 377 }
614 378
615 if (imported_profile_) { 379 // Rebuild the underyling store.
616 profiles_.push_back(*imported_profile_); 380 gtk_list_store_clear(list_store_);
617 AddAddress(*imported_profile_);
618 } else {
619 for (std::vector<AutoFillProfile*>::const_iterator iter =
620 personal_data_->profiles().begin();
621 iter != personal_data_->profiles().end(); ++iter) {
622 // The profile list is terminated by a NULL entry;
623 if (!*iter)
624 break;
625 381
626 AutoFillProfile* profile = *iter; 382 GtkTreeIter iter;
627 profiles_.push_back(*profile); 383 // Address title.
628 AddAddress(*profile); 384 gtk_list_store_append(list_store_, &iter);
629 } 385 gtk_list_store_set(
386 list_store_, &iter,
387 COL_WEIGHT, PANGO_WEIGHT_BOLD,
388 COL_WEIGHT_SET, TRUE,
389 COL_TITLE,
390 l10n_util::GetStringUTF8(IDS_AUTOFILL_ADDRESSES_GROUP_NAME).c_str(),
391 COL_IS_HEADER, TRUE,
392 -1);
393 // Address separator.
394 gtk_list_store_append(list_store_, &iter);
395 gtk_list_store_set(
396 list_store_, &iter,
397 COL_IS_SEPARATOR, TRUE,
398 -1);
399
400 // The addresses.
401 profile_count_ = 0;
402 for (std::vector<AutoFillProfile*>::const_iterator i =
403 personal_data_->profiles().begin();
404 i != personal_data_->profiles().end(); ++i) {
405 AddAddressToTree(*(*i), &iter);
406 profile_count_++;
630 } 407 }
631 408
632 if (imported_credit_card_) { 409 // Blank row between addresses and credit cards.
633 credit_cards_.push_back(*imported_credit_card_); 410 gtk_list_store_append(list_store_, &iter);
634 AddCreditCard(*imported_credit_card_); 411 gtk_list_store_set(
635 } else { 412 list_store_, &iter,
636 for (std::vector<CreditCard*>::const_iterator iter = 413 COL_IS_HEADER, TRUE,
637 personal_data_->credit_cards().begin(); 414 -1);
638 iter != personal_data_->credit_cards().end(); ++iter) {
639 // The credit card list is terminated by a NULL entry;
640 if (!*iter)
641 break;
642 415
643 CreditCard* credit_card = *iter; 416 // Credit card title.
644 credit_cards_.push_back(*credit_card); 417 gtk_list_store_append(list_store_, &iter);
645 AddCreditCard(*credit_card); 418 gtk_list_store_set(
646 } 419 list_store_, &iter,
420 COL_WEIGHT, PANGO_WEIGHT_BOLD,
421 COL_WEIGHT_SET, TRUE,
422 COL_TITLE,
423 l10n_util::GetStringUTF8(IDS_AUTOFILL_CREDITCARDS_GROUP_NAME).c_str(),
424 COL_IS_HEADER, TRUE,
425 -1);
426 // Credit card separator.
427 gtk_list_store_append(list_store_, &iter);
428 gtk_list_store_set(
429 list_store_, &iter,
430 COL_IS_SEPARATOR, TRUE,
431 -1);
432
433 // The credit cards.
434 for (std::vector<CreditCard*>::const_iterator i =
435 personal_data_->credit_cards().begin();
436 i != personal_data_->credit_cards().end(); ++i) {
437 AddCreditCardToTree(*(*i), &iter);
647 } 438 }
439
440 UpdateWidgetState();
648 } 441 }
649 442
650 void AutoFillDialog::InitializeWidgets() { 443 void AutoFillDialog::InitializeWidgets() {
651 dialog_ = gtk_dialog_new_with_buttons( 444 dialog_ = gtk_dialog_new_with_buttons(
652 l10n_util::GetStringUTF8(IDS_AUTOFILL_OPTIONS).c_str(), 445 l10n_util::GetStringUTF8(IDS_AUTOFILL_OPTIONS).c_str(),
653 // AutoFill dialog is shared between all browser windows. 446 // AutoFill dialog is shared between all browser windows.
654 NULL, 447 NULL,
655 // Non-modal. 448 // Non-modal.
656 GTK_DIALOG_NO_SEPARATOR, 449 GTK_DIALOG_NO_SEPARATOR,
657 GTK_STOCK_APPLY,
658 GTK_RESPONSE_APPLY,
659 GTK_STOCK_CANCEL,
660 GTK_RESPONSE_CANCEL,
661 GTK_STOCK_OK, 450 GTK_STOCK_OK,
662 GTK_RESPONSE_OK, 451 GTK_RESPONSE_OK,
663 NULL); 452 NULL);
664 453
665 gtk_box_set_spacing(GTK_BOX(GTK_DIALOG(dialog_)->vbox), 454 GtkBox* vbox = GTK_BOX(GTK_DIALOG(dialog_)->vbox);
666 gtk_util::kContentAreaSpacing); 455 gtk_box_set_spacing(vbox, gtk_util::kControlSpacing);
667 g_signal_connect(dialog_, "response", G_CALLBACK(OnResponse), this); 456 g_signal_connect(dialog_, "response", G_CALLBACK(OnResponseThunk), this);
668 g_signal_connect(dialog_, "destroy", G_CALLBACK(OnDestroy), this); 457 g_signal_connect(dialog_, "destroy", G_CALLBACK(OnDestroyThunk), this);
458
459 form_autofill_enable_check_ = gtk_check_button_new_with_label(
460 l10n_util::GetStringUTF8(IDS_OPTIONS_AUTOFILL_ENABLE).c_str());
461 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(form_autofill_enable_check_),
462 enable_form_autofill_.GetValue());
463 g_signal_connect(G_OBJECT(form_autofill_enable_check_), "toggled",
464 G_CALLBACK(OnAutoFillCheckToggledThunk), this);
465 gtk_box_pack_start(vbox, form_autofill_enable_check_, FALSE, FALSE, 0);
669 466
670 // Allow the contents to be scrolled. 467 // Allow the contents to be scrolled.
671 GtkWidget* scrolled_window = gtk_scrolled_window_new(NULL, NULL); 468 GtkWidget* scrolled_window = gtk_scrolled_window_new(NULL, NULL);
672 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), 469 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
673 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); 470 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
674 gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog_)->vbox), scrolled_window); 471 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled_window),
472 GTK_SHADOW_ETCHED_IN);
675 473
676 // We create an event box so that we can color the frame background white. 474 list_store_ = gtk_list_store_new(COL_COUNT,
677 GtkWidget* frame_event_box = gtk_event_box_new(); 475 G_TYPE_STRING,
678 SetWhiteBackground(frame_event_box); 476 G_TYPE_BOOLEAN,
679 gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrolled_window), 477 G_TYPE_BOOLEAN,
680 frame_event_box); 478 G_TYPE_INT,
479 G_TYPE_BOOLEAN);
480 tree_ = gtk_tree_view_new_with_model(GTK_TREE_MODEL(list_store_));
481 g_object_unref(list_store_);
482 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_), FALSE);
483 gtk_tree_view_set_row_separator_func(GTK_TREE_VIEW(tree_),
484 OnCheckRowIsSeparatorThunk, this, NULL);
485 g_signal_connect(tree_, "row-activated", G_CALLBACK(OnRowActivatedThunk),
486 this);
487 gtk_container_add(GTK_CONTAINER(scrolled_window), tree_);
681 488
682 // The frame outline of the content area. 489 GtkWidget* h_box1 = gtk_hbox_new(FALSE, gtk_util::kControlSpacing);
683 GtkWidget* frame = gtk_frame_new(NULL); 490 gtk_box_pack_start(vbox, h_box1, TRUE, TRUE, 0);
684 gtk_container_add(GTK_CONTAINER(frame_event_box), frame); 491 gtk_box_pack_start(GTK_BOX(h_box1), scrolled_window, TRUE, TRUE, 0);
685 492
686 // The content vbox. 493 GtkWidget* controls_box = gtk_vbox_new(FALSE, gtk_util::kControlSpacing);
687 GtkWidget* outer_vbox = gtk_vbox_new(false, 0); 494 gtk_box_pack_start(GTK_BOX(h_box1), controls_box, FALSE, FALSE, 0);
688 gtk_box_set_spacing(GTK_BOX(outer_vbox), gtk_util::kContentAreaSpacing);
689 gtk_container_add(GTK_CONTAINER(frame), outer_vbox);
690 495
691 addresses_vbox_ = InitGroup(IDS_AUTOFILL_ADDRESSES_GROUP_NAME, 496 add_address_button_ = gtk_button_new_with_label(
692 IDS_AUTOFILL_ADD_ADDRESS_BUTTON, 497 l10n_util::GetStringUTF8(IDS_AUTOFILL_ADD_ADDRESS_BUTTON).c_str());
693 G_CALLBACK(OnAddAddressClicked)); 498 g_signal_connect(add_address_button_, "clicked",
694 gtk_box_pack_start_defaults(GTK_BOX(outer_vbox), addresses_vbox_); 499 G_CALLBACK(OnAddAddressThunk), this);
500 gtk_box_pack_start(GTK_BOX(controls_box), add_address_button_, FALSE, FALSE,
501 0);
695 502
696 creditcards_vbox_ = InitGroup(IDS_AUTOFILL_CREDITCARDS_GROUP_NAME, 503 add_credit_card_button_ = gtk_button_new_with_label(
697 IDS_AUTOFILL_ADD_CREDITCARD_BUTTON, 504 l10n_util::GetStringUTF8(IDS_AUTOFILL_ADD_CREDITCARD_BUTTON).c_str());
698 G_CALLBACK(OnAddCreditCardClicked)); 505 g_signal_connect(add_credit_card_button_, "clicked",
699 gtk_box_pack_start_defaults(GTK_BOX(outer_vbox), creditcards_vbox_); 506 G_CALLBACK(OnAddCreditCardThunk), this);
507 gtk_box_pack_start(GTK_BOX(controls_box), add_credit_card_button_, FALSE,
508 FALSE, 0);
509
510 edit_button_ = gtk_button_new_with_label(
511 l10n_util::GetStringUTF8(IDS_AUTOFILL_EDIT_BUTTON).c_str());
512 g_signal_connect(edit_button_, "clicked", G_CALLBACK(OnEditThunk), this);
513 gtk_box_pack_start(GTK_BOX(controls_box), edit_button_, FALSE, FALSE, 0);
514
515 remove_button_ = gtk_button_new_with_label(
516 l10n_util::GetStringUTF8(IDS_AUTOFILL_DELETE_BUTTON).c_str());
517 g_signal_connect(remove_button_, "clicked", G_CALLBACK(OnRemoveThunk), this);
518 gtk_box_pack_start(GTK_BOX(controls_box), remove_button_, FALSE, FALSE, 0);
519
520 GtkTreeViewColumn* column = gtk_tree_view_column_new_with_attributes(
521 "",
522 gtk_cell_renderer_text_new(),
523 "text", COL_TITLE,
524 "weight", COL_WEIGHT,
525 "weight-set", COL_WEIGHT_SET,
526 NULL);
527 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_), column);
528
529 GtkTreeSelection* selection =
530 gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_));
531 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
532 gtk_tree_selection_set_select_function(selection, OnSelectionFilterThunk,
533 this, NULL);
534 g_signal_connect(selection, "changed", G_CALLBACK(OnSelectionChangedThunk),
535 this);
700 536
701 GtkWidget* link = gtk_chrome_link_button_new( 537 GtkWidget* link = gtk_chrome_link_button_new(
702 l10n_util::GetStringUTF8(IDS_AUTOFILL_LEARN_MORE).c_str()); 538 l10n_util::GetStringUTF8(IDS_AUTOFILL_HELP_LABEL).c_str());
703 gtk_dialog_add_action_widget(GTK_DIALOG(dialog_), link, 539 gtk_dialog_add_action_widget(GTK_DIALOG(dialog_), link,
704 kAutoFillDialogLearnMoreLink); 540 kAutoFillDialogAboutLink);
705 541
706 // Setting the link widget to secondary positions the button on the left side 542 // Setting the link widget to secondary positions the button on the left side
707 // of the action area (vice versa for RTL layout). 543 // of the action area (vice versa for RTL layout).
708 gtk_button_box_set_child_secondary( 544 gtk_button_box_set_child_secondary(
709 GTK_BUTTON_BOX(GTK_DIALOG(dialog_)->action_area), link, TRUE); 545 GTK_BUTTON_BOX(GTK_DIALOG(dialog_)->action_area), link, TRUE);
710 } 546 }
711 547
712 GtkWidget* AutoFillDialog::InitGroup(int name_id, 548 void AutoFillDialog::UpdateWidgetState() {
713 int button_id, 549 if (!personal_data_->IsDataLoaded() || !enable_form_autofill_.GetValue()) {
714 GCallback clicked_callback) { 550 gtk_widget_set_sensitive(add_address_button_, FALSE);
715 GtkWidget* vbox = gtk_vbox_new(false, gtk_util::kControlSpacing); 551 gtk_widget_set_sensitive(add_credit_card_button_, FALSE);
716 552 gtk_widget_set_sensitive(edit_button_, FALSE);
717 // Group label. 553 gtk_widget_set_sensitive(remove_button_, FALSE);
718 GtkWidget* label = gtk_util::CreateBoldLabel( 554 gtk_widget_set_sensitive(tree_, FALSE);
719 l10n_util::GetStringUTF8(name_id)); 555 } else {
720 gtk_box_pack_start(GTK_BOX(vbox), 556 gtk_widget_set_sensitive(add_address_button_, TRUE);
721 IndentWidget(label, kAutoFillDialogIndent), 557 gtk_widget_set_sensitive(add_credit_card_button_, TRUE);
722 FALSE, FALSE, 0); 558 int selection_type = GetSelectionType();
723 559 gtk_widget_set_sensitive(edit_button_, selection_type == SELECTION_SINGLE);
724 // Separator. 560 // Enable the remove button if at least one non-header row is selected.
725 GtkWidget* separator = gtk_hseparator_new(); 561 gtk_widget_set_sensitive(remove_button_,
726 gtk_box_pack_start(GTK_BOX(vbox), separator, FALSE, FALSE, 0); 562 (selection_type & SELECTION_SINGLE) != 0);
727 563 gtk_widget_set_sensitive(tree_, TRUE);
728 // Add profile button. 564 }
729 GtkWidget* button = gtk_button_new_with_label(
730 l10n_util::GetStringUTF8(button_id).c_str());
731 g_signal_connect(button, "clicked", clicked_callback, this);
732 gtk_box_pack_end_defaults(GTK_BOX(vbox),
733 IndentWidget(button, kAutoFillDialogIndent));
734
735 return vbox;
736 } 565 }
737 566
738 GtkWidget* AutoFillDialog::InitGroupContentArea(int name_id, 567 static void RowIteratorFunction(GtkTreeModel* model,
739 GtkWidget** content_vbox) { 568 GtkTreePath* path,
740 GtkWidget* expander = gtk_expander_new( 569 GtkTreeIter* iter,
741 l10n_util::GetStringUTF8(name_id).c_str()); 570 gpointer data) {
571 int* type = reinterpret_cast<int*>(data);
572 bool is_header = false;
573 GValue value = { 0 };
574 gtk_tree_model_get_value(model, iter, AutoFillDialog::COL_IS_HEADER, &value);
575 is_header = g_value_get_boolean(&value);
576 g_value_unset(&value);
742 577
743 GtkWidget* frame = gtk_frame_new(NULL); 578 if (!is_header) {
744 gtk_container_add(GTK_CONTAINER(expander), frame); 579 // Is it a separator?
580 GValue value = { 0 };
581 gtk_tree_model_get_value(model, iter, AutoFillDialog::COL_IS_SEPARATOR,
582 &value);
583 is_header = g_value_get_boolean(&value);
584 g_value_unset(&value);
585 }
745 586
746 GtkWidget* vbox = gtk_vbox_new(false, 0); 587 if (is_header) {
747 gtk_box_set_spacing(GTK_BOX(vbox), gtk_util::kControlSpacing); 588 *type |= AutoFillDialog::SELECTION_HEADER;
748 GtkWidget* vbox_alignment = gtk_alignment_new(0, 0, 0, 0); 589 } else {
749 gtk_alignment_set_padding(GTK_ALIGNMENT(vbox_alignment), 590 if ((*type & AutoFillDialog::SELECTION_SINGLE) == 0)
750 gtk_util::kControlSpacing, 591 *type |= AutoFillDialog::SELECTION_SINGLE;
751 gtk_util::kControlSpacing, 592 else
752 gtk_util::kGroupIndent, 593 *type |= AutoFillDialog::SELECTION_MULTI;
753 0); 594 }
754 gtk_container_add(GTK_CONTAINER(vbox_alignment), vbox);
755 gtk_container_add(GTK_CONTAINER(frame), vbox_alignment);
756
757 *content_vbox = vbox;
758 return expander;
759 } 595 }
760 596
761 GtkWidget* AutoFillDialog::AddNewAddress(bool expand) { 597 int AutoFillDialog::GetSelectionType() {
762 AddressWidgets widgets = {0}; 598 int state = SELECTION_EMPTY;
763 GtkWidget* vbox; 599 gtk_tree_selection_selected_foreach(
764 GtkWidget* address = InitGroupContentArea(IDS_AUTOFILL_NEW_ADDRESS, &vbox); 600 gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_)), RowIteratorFunction,
765 601 &state);
766 gtk_expander_set_expanded(GTK_EXPANDER(address), expand); 602 return state;
767
768 GtkWidget* table = InitFormTable(5, 3);
769 gtk_box_pack_start_defaults(GTK_BOX(vbox), table);
770
771 widgets.label = FormTableAddLabelEntry(table, 0, 0, 1,
772 IDS_AUTOFILL_DIALOG_LABEL,
773 address, G_CALLBACK(OnLabelChanged));
774 widgets.full_name = FormTableAddEntry(table, 1, 0, 1,
775 IDS_AUTOFILL_DIALOG_FULL_NAME);
776 widgets.email = FormTableAddEntry(table, 2, 0, 1,
777 IDS_AUTOFILL_DIALOG_EMAIL);
778 widgets.company_name = FormTableAddEntry(table, 2, 1, 1,
779 IDS_AUTOFILL_DIALOG_COMPANY_NAME);
780 widgets.address_line1 = FormTableAddEntry(table, 3, 0, 2,
781 IDS_AUTOFILL_DIALOG_ADDRESS_LINE_1);
782 widgets.address_line2 = FormTableAddEntry(table, 4, 0, 2,
783 IDS_AUTOFILL_DIALOG_ADDRESS_LINE_2);
784
785 GtkWidget* address_table = InitFormTable(1, 4);
786 gtk_box_pack_start_defaults(GTK_BOX(vbox), address_table);
787
788 widgets.city = FormTableAddEntry(address_table, 0, 0, 1,
789 IDS_AUTOFILL_DIALOG_CITY);
790 widgets.state = FormTableAddEntry(address_table, 0, 1, 1,
791 IDS_AUTOFILL_DIALOG_STATE);
792 widgets.zipcode = FormTableAddSizedEntry(address_table, 0, 2, 7,
793 IDS_AUTOFILL_DIALOG_ZIP_CODE);
794 widgets.country = FormTableAddSizedEntry(address_table, 0, 3, 10,
795 IDS_AUTOFILL_DIALOG_COUNTRY);
796
797 GtkWidget* phone_table = InitFormTable(1, 2);
798 gtk_box_pack_start_defaults(GTK_BOX(vbox), phone_table);
799
800 widgets.phone =
801 FormTableAddEntry(phone_table, 0, 0, 1, IDS_AUTOFILL_DIALOG_PHONE);
802 widgets.fax =
803 FormTableAddEntry(phone_table, 0, 1, 1, IDS_AUTOFILL_DIALOG_FAX);
804
805 GtkWidget* button = gtk_button_new_with_label(
806 l10n_util::GetStringUTF8(IDS_AUTOFILL_DELETE_BUTTON).c_str());
807 g_signal_connect(button, "clicked", G_CALLBACK(OnDeleteAddressClicked), this);
808 SetButtonData(button, widgets.label);
809 GtkWidget* alignment = gtk_alignment_new(0, 0, 0, 0);
810 gtk_container_add(GTK_CONTAINER(alignment), button);
811 gtk_box_pack_start_defaults(GTK_BOX(vbox), alignment);
812
813 address_widgets_.push_back(widgets);
814 return address;
815 } 603 }
816 604
817 GtkWidget* AutoFillDialog::AddNewCreditCard(bool expand) { 605 void AutoFillDialog::AddAddressToTree(const AutoFillProfile& profile,
818 CreditCardWidgets widgets = {0}; 606 GtkTreeIter* iter) {
819 GtkWidget* vbox; 607 gtk_list_store_append(list_store_, iter);
820 GtkWidget* credit_card = InitGroupContentArea(IDS_AUTOFILL_NEW_CREDITCARD, 608 gtk_list_store_set(
821 &vbox); 609 list_store_, iter,
822 610 COL_WEIGHT, PANGO_WEIGHT_NORMAL,
823 gtk_expander_set_expanded(GTK_EXPANDER(credit_card), expand); 611 COL_WEIGHT_SET, TRUE,
824 612 COL_TITLE, UTF16ToUTF8(profile.PreviewSummary()).c_str(),
825 GtkWidget* label_table = InitFormTable(1, 2); 613 -1);
826 gtk_box_pack_start_defaults(GTK_BOX(vbox), label_table);
827
828 widgets.label = FormTableAddLabelEntry(label_table, 0, 0, 1,
829 IDS_AUTOFILL_DIALOG_LABEL, credit_card,
830 G_CALLBACK(OnLabelChanged));
831
832 GtkWidget* name_cc_table = InitFormTable(2, 6);
833 gtk_box_pack_start_defaults(GTK_BOX(vbox), name_cc_table);
834
835 widgets.name_on_card = FormTableAddExpandedEntry(
836 name_cc_table, 0, 0, 3, IDS_AUTOFILL_DIALOG_NAME_ON_CARD);
837 widgets.card_number = FormTableAddExpandedEntry(
838 name_cc_table, 1, 0, 3, IDS_AUTOFILL_DIALOG_CREDIT_CARD_NUMBER);
839 widgets.expiration_month = FormTableAddSizedEntry(name_cc_table, 1, 3, 2, 0);
840 widgets.expiration_year = FormTableAddSizedEntry(name_cc_table, 1, 4, 4, 0);
841
842 FormTableSetLabel(name_cc_table, 1, 3, 2,
843 IDS_AUTOFILL_DIALOG_EXPIRATION_DATE);
844
845 gtk_table_set_col_spacing(GTK_TABLE(name_cc_table), 3, 2);
846
847 GtkWidget* addresses_table = InitFormTable(2, 5);
848 gtk_box_pack_start_defaults(GTK_BOX(vbox), addresses_table);
849
850 FormTableSetLabel(addresses_table, 0, 0, 3,
851 IDS_AUTOFILL_DIALOG_BILLING_ADDRESS);
852
853 GtkWidget* billing = gtk_combo_box_new_text();
854 widgets.billing_address = billing;
855 std::string combo_text = l10n_util::GetStringUTF8(
856 IDS_AUTOFILL_DIALOG_CHOOSE_EXISTING_ADDRESS);
857 gtk_combo_box_append_text(GTK_COMBO_BOX(billing), combo_text.c_str());
858 gtk_combo_box_set_active(GTK_COMBO_BOX(billing), 0);
859 FormTableSetWidget(addresses_table, billing, 0, 0, 2, false);
860
861 for (std::vector<AddressWidgets>::const_iterator iter =
862 address_widgets_.begin();
863 iter != address_widgets_.end(); ++iter) {
864 // TODO(jhawkins): Validate the label and DCHECK on !empty().
865 std::string text = gtk_entry_get_text(GTK_ENTRY(iter->label));
866 if (!text.empty())
867 gtk_combo_box_append_text(GTK_COMBO_BOX(widgets.billing_address),
868 text.c_str());
869 }
870
871 GtkWidget* phone_table = InitFormTable(1, 1);
872 gtk_box_pack_start_defaults(GTK_BOX(vbox), phone_table);
873
874 widgets.phone =
875 FormTableAddEntry(phone_table, 0, 0, 1, IDS_AUTOFILL_DIALOG_PHONE);
876
877 GtkWidget* button = gtk_button_new_with_label(
878 l10n_util::GetStringUTF8(IDS_AUTOFILL_DELETE_BUTTON).c_str());
879 g_signal_connect(button, "clicked",
880 G_CALLBACK(OnDeleteCreditCardClicked), this);
881 SetButtonData(button, widgets.label);
882 GtkWidget* alignment = gtk_alignment_new(0, 0, 0, 0);
883 gtk_container_add(GTK_CONTAINER(alignment), button);
884 gtk_box_pack_start_defaults(GTK_BOX(vbox), alignment);
885
886 credit_card_widgets_.push_back(widgets);
887 return credit_card;
888 } 614 }
889 615
890 void AutoFillDialog::AddAddress(const AutoFillProfile& profile) { 616 void AutoFillDialog::AddCreditCardToTree(const CreditCard& credit_card,
891 GtkWidget* address = AddNewAddress(false); 617 GtkTreeIter* iter) {
892 gtk_expander_set_label(GTK_EXPANDER(address), 618 gtk_list_store_append(list_store_, iter);
893 UTF16ToUTF8(profile.Label()).c_str()); 619 gtk_list_store_set(
894 620 list_store_, iter,
895 // We just pushed the widgets to the back of the vector. 621 COL_WEIGHT, PANGO_WEIGHT_NORMAL,
896 const AddressWidgets& widgets = address_widgets_.back(); 622 COL_WEIGHT_SET, TRUE,
897 SetEntryText(widgets.label, profile.Label()); 623 COL_TITLE, UTF16ToUTF8(credit_card.PreviewSummary()).c_str(),
898 SetEntryText(widgets.full_name, 624 -1);
899 profile.GetFieldText(AutoFillType(NAME_FULL)));
900 SetEntryText(widgets.email,
901 profile.GetFieldText(AutoFillType(EMAIL_ADDRESS)));
902 SetEntryText(widgets.company_name,
903 profile.GetFieldText(AutoFillType(COMPANY_NAME)));
904 SetEntryText(widgets.address_line1,
905 profile.GetFieldText(AutoFillType(ADDRESS_HOME_LINE1)));
906 SetEntryText(widgets.address_line2,
907 profile.GetFieldText(AutoFillType(ADDRESS_HOME_LINE2)));
908 SetEntryText(widgets.city,
909 profile.GetFieldText(AutoFillType(ADDRESS_HOME_CITY)));
910 SetEntryText(widgets.state,
911 profile.GetFieldText(AutoFillType(ADDRESS_HOME_STATE)));
912 SetEntryText(widgets.zipcode,
913 profile.GetFieldText(AutoFillType(ADDRESS_HOME_ZIP)));
914 SetEntryText(widgets.country,
915 profile.GetFieldText(AutoFillType(ADDRESS_HOME_COUNTRY)));
916 SetEntryText(widgets.phone,
917 profile.GetFieldText(AutoFillType(PHONE_HOME_WHOLE_NUMBER)));
918 SetEntryText(widgets.fax,
919 profile.GetFieldText(AutoFillType(PHONE_FAX_WHOLE_NUMBER)));
920
921 gtk_box_pack_start(GTK_BOX(addresses_vbox_), address, FALSE, FALSE, 0);
922 gtk_widget_show_all(address);
923 } 625 }
924 626
925 void AutoFillDialog::AddCreditCard(const CreditCard& credit_card) { 627 void AutoFillDialog::GetSelectedEntries(
926 GtkWidget* credit_card_widget = AddNewCreditCard(false); 628 std::vector<AutoFillProfile*>* profiles,
927 gtk_expander_set_label(GTK_EXPANDER(credit_card_widget), 629 std::vector<CreditCard*>* cards) {
928 UTF16ToUTF8(credit_card.Label()).c_str()); 630 std::set<int> selection;
631 gtk_tree::GetSelectedIndices(
632 gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_)), &selection);
929 633
930 // We just pushed the widgets to the back of the vector. 634 for (std::set<int>::const_iterator i = selection.begin();
931 const CreditCardWidgets& widgets = credit_card_widgets_.back(); 635 i != selection.end(); ++i) {
932 SetEntryText(widgets.label, credit_card.Label()); 636 // 2 is the number of header rows.
933 SetEntryText(widgets.name_on_card, 637 int index = *i - 2;
934 credit_card.GetFieldText(AutoFillType(CREDIT_CARD_NAME))); 638 if (index >= 0 &&
935 // Set obfuscated number if not empty. 639 index < static_cast<int>(personal_data_->profiles().size())) {
936 credit_card_widgets_.back().original_card_number = 640 profiles->push_back(personal_data_->profiles()[index]);
937 credit_card.GetFieldText(AutoFillType(CREDIT_CARD_NUMBER)); 641 continue;
938 string16 credit_card_number; 642 }
939 if (!widgets.original_card_number.empty())
940 credit_card_number = credit_card.ObfuscatedNumber();
941 SetEntryText(widgets.card_number, credit_card_number);
942 SetEntryText(widgets.expiration_month,
943 credit_card.GetFieldText(AutoFillType(CREDIT_CARD_EXP_MONTH)));
944 SetEntryText(
945 widgets.expiration_year,
946 credit_card.GetFieldText(AutoFillType(CREDIT_CARD_EXP_4_DIGIT_YEAR)));
947 643
948 // Two cases to consider: 644 // Empty row, header and separator are next.
949 // address not found - This means the address is not set and 645 index -= profile_count_ + 3;
950 // FindIndexOfAddress returns -1. -1 + 1 = 0, meaning the first item will 646 if (index >= 0 && index <
951 // be selected, "Choose existing address". 647 static_cast<int>(personal_data_->credit_cards().size())) {
952 // 648 cards->push_back(personal_data_->credit_cards()[index]);
953 // address is found - The index returned needs to be offset by one in order 649 }
954 // to compensate for the first entry, "Choose existing address". 650 }
955 int index = FindIndexOfAddress(credit_card.billing_address()) + 1;
956 gtk_combo_box_set_active(GTK_COMBO_BOX(widgets.billing_address), index);
957
958 gtk_box_pack_start(GTK_BOX(creditcards_vbox_), credit_card_widget,
959 FALSE, FALSE, 0);
960 gtk_widget_show_all(credit_card_widget);
961 } 651 }
962 652
963 int AutoFillDialog::FindIndexOfAddress(string16 billing_address) { 653 } // namespace
964 int index = 0;
965 for (std::vector<AddressWidgets>::const_iterator iter =
966 address_widgets_.begin();
967 iter != address_widgets_.end(); ++iter, ++index) {
968 std::string text = gtk_entry_get_text(GTK_ENTRY(iter->label));
969 if (UTF8ToUTF16(text) == billing_address)
970 return index;
971 }
972
973 return -1;
974 }
975 654
976 /////////////////////////////////////////////////////////////////////////////// 655 ///////////////////////////////////////////////////////////////////////////////
977 // Factory/finder method: 656 // Factory/finder method:
978 657
979 void ShowAutoFillDialog(gfx::NativeView parent, 658 void ShowAutoFillDialog(gfx::NativeView parent,
980 AutoFillDialogObserver* observer, 659 AutoFillDialogObserver* observer,
981 Profile* profile) { 660 Profile* profile) {
982 DCHECK(profile); 661 DCHECK(profile);
983 662
984 if (!dialog) { 663 if (!dialog)
985 dialog = new AutoFillDialog(observer, 664 dialog = new AutoFillDialog(profile, observer);
986 profile->GetPersonalDataManager(),
987 NULL,
988 NULL);
989 }
990 dialog->Show(); 665 dialog->Show();
991 } 666 }
OLDNEW
« no previous file with comments | « chrome/browser/autofill/autofill_dialog.h ('k') | chrome/browser/autofill/credit_card.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698