1 // Copyright (c) 2011 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/edit_search_engine_dialog.h"
6
7 #include <gtk/gtk.h>
8
9 #include "base/i18n/rtl.h"
10 #include "base/message_loop.h"
11 #include "base/utf_string_conversions.h"
12 #include "chrome/browser/net/url_fixer_upper.h"
13 #include "chrome/browser/profiles/profile.h"
14 #include "chrome/browser/search_engines/template_url.h"
15 #include "chrome/browser/search_engines/template_url_model.h"
16 #include "chrome/browser/ui/gtk/gtk_util.h"
17 #include "chrome/browser/ui/search_engines/edit_search_engine_controller.h"
18 #include "googleurl/src/gurl.h"
19 #include "grit/app_resources.h"
20 #include "grit/generated_resources.h"
21 #include "grit/theme_resources.h"
22 #include "ui/base/l10n/l10n_util.h"
23 #include "ui/base/resource/resource_bundle.h"
24
25 namespace {
26
GetDisplayURL(const TemplateURL & turl)27 std::string GetDisplayURL(const TemplateURL& turl) {
28 return turl.url() ? UTF16ToUTF8(turl.url()->DisplayURL()) : std::string();
29 }
30
31 // Forces text to lowercase when connected to an editable's "insert-text"
32 // signal. (Like views Textfield::STYLE_LOWERCASE.)
LowercaseInsertTextHandler(GtkEditable * editable,const gchar * text,gint length,gint * position,gpointer data)33 void LowercaseInsertTextHandler(GtkEditable *editable, const gchar *text,
34 gint length, gint *position, gpointer data) {
35 string16 original_text = UTF8ToUTF16(text);
36 string16 lower_text = l10n_util::ToLower(original_text);
37 if (lower_text != original_text) {
38 std::string result = UTF16ToUTF8(lower_text);
39 // Prevent ourselves getting called recursively about our own edit.
40 g_signal_handlers_block_by_func(G_OBJECT(editable),
41 reinterpret_cast<gpointer>(LowercaseInsertTextHandler), data);
42 gtk_editable_insert_text(editable, result.c_str(), result.size(), position);
43 g_signal_handlers_unblock_by_func(G_OBJECT(editable),
44 reinterpret_cast<gpointer>(LowercaseInsertTextHandler), data);
45 // We've inserted our modified version, stop the defalut handler from
46 // inserting the original.
47 g_signal_stop_emission_by_name(G_OBJECT(editable), "insert_text");
48 }
49 }
50
SetWidgetStyle(GtkWidget * entry,GtkStyle * label_style,GtkStyle * dialog_style)51 void SetWidgetStyle(GtkWidget* entry, GtkStyle* label_style,
52 GtkStyle* dialog_style) {
53 gtk_widget_modify_fg(entry, GTK_STATE_NORMAL,
54 &label_style->fg[GTK_STATE_NORMAL]);
55 gtk_widget_modify_fg(entry, GTK_STATE_INSENSITIVE,
56 &label_style->fg[GTK_STATE_INSENSITIVE]);
57 // GTK_NO_WINDOW widgets like GtkLabel don't draw their own background, so we
58 // combine the normal or insensitive foreground of the label style with the
59 // normal background of the window style to achieve the "normal label" and
60 // "insensitive label" colors.
61 gtk_widget_modify_base(entry, GTK_STATE_NORMAL,
62 &dialog_style->bg[GTK_STATE_NORMAL]);
63 gtk_widget_modify_base(entry, GTK_STATE_INSENSITIVE,
64 &dialog_style->bg[GTK_STATE_NORMAL]);
65 }
66
67 } // namespace
68
EditSearchEngineDialog(GtkWindow * parent_window,const TemplateURL * template_url,EditSearchEngineControllerDelegate * delegate,Profile * profile)69 EditSearchEngineDialog::EditSearchEngineDialog(
70 GtkWindow* parent_window,
71 const TemplateURL* template_url,
72 EditSearchEngineControllerDelegate* delegate,
73 Profile* profile)
74 : controller_(new EditSearchEngineController(template_url, delegate,
75 profile)) {
76 Init(parent_window, profile);
77 }
78
~EditSearchEngineDialog()79 EditSearchEngineDialog::~EditSearchEngineDialog() {}
80
Init(GtkWindow * parent_window,Profile * profile)81 void EditSearchEngineDialog::Init(GtkWindow* parent_window, Profile* profile) {
82 std::string dialog_name = l10n_util::GetStringUTF8(
83 controller_->template_url() ?
84 IDS_SEARCH_ENGINES_EDITOR_EDIT_WINDOW_TITLE :
85 IDS_SEARCH_ENGINES_EDITOR_NEW_WINDOW_TITLE);
86
87 dialog_ = gtk_dialog_new_with_buttons(
88 dialog_name.c_str(),
89 parent_window,
90 static_cast<GtkDialogFlags>(GTK_DIALOG_MODAL | GTK_DIALOG_NO_SEPARATOR),
91 GTK_STOCK_CANCEL,
92 GTK_RESPONSE_CANCEL,
93 NULL);
94
95 ok_button_ = gtk_dialog_add_button(GTK_DIALOG(dialog_),
96 controller_->template_url() ?
97 GTK_STOCK_SAVE :
98 GTK_STOCK_ADD,
99 GTK_RESPONSE_OK);
100 gtk_dialog_set_default_response(GTK_DIALOG(dialog_), GTK_RESPONSE_OK);
101
102 // The dialog layout hierarchy looks like this:
103 //
104 // \ GtkVBox |dialog_->vbox|
105 // +-\ GtkTable |controls|
106 // | +-\ row 0
107 // | | +- GtkLabel
108 // | | +-\ GtkHBox
109 // | | +- GtkEntry |title_entry_|
110 // | | +- GtkImage |title_image_|
111 // | +-\ row 1
112 // | | +- GtkLabel
113 // | | +-\ GtkHBox
114 // | | +- GtkEntry |keyword_entry_|
115 // | | +- GtkImage |keyword_image_|
116 // | +-\ row 2
117 // | +- GtkLabel
118 // | +-\ GtkHBox
119 // | +- GtkEntry |url_entry_|
120 // | +- GtkImage |url_image_|
121 // +- GtkLabel |description_label|
122
123 title_entry_ = gtk_entry_new();
124 gtk_entry_set_activates_default(GTK_ENTRY(title_entry_), TRUE);
125 g_signal_connect(title_entry_, "changed",
126 G_CALLBACK(OnEntryChangedThunk), this);
127
128 keyword_entry_ = gtk_entry_new();
129 gtk_entry_set_activates_default(GTK_ENTRY(keyword_entry_), TRUE);
130 g_signal_connect(keyword_entry_, "changed",
131 G_CALLBACK(OnEntryChangedThunk), this);
132 g_signal_connect(keyword_entry_, "insert-text",
133 G_CALLBACK(LowercaseInsertTextHandler), NULL);
134
135 url_entry_ = gtk_entry_new();
136 gtk_entry_set_activates_default(GTK_ENTRY(url_entry_), TRUE);
137 g_signal_connect(url_entry_, "changed",
138 G_CALLBACK(OnEntryChangedThunk), this);
139
140 title_image_ = gtk_image_new_from_pixbuf(NULL);
141 keyword_image_ = gtk_image_new_from_pixbuf(NULL);
142 url_image_ = gtk_image_new_from_pixbuf(NULL);
143
144 if (controller_->template_url()) {
145 gtk_entry_set_text(
146 GTK_ENTRY(title_entry_),
147 UTF16ToUTF8(controller_->template_url()->short_name()).c_str());
148 gtk_entry_set_text(
149 GTK_ENTRY(keyword_entry_),
150 UTF16ToUTF8(controller_->template_url()->keyword()).c_str());
151 gtk_entry_set_text(
152 GTK_ENTRY(url_entry_),
153 GetDisplayURL(*controller_->template_url()).c_str());
154 // We don't allow users to edit prepopulated URLs.
155 gtk_editable_set_editable(
156 GTK_EDITABLE(url_entry_),
157 controller_->template_url()->prepopulate_id() == 0);
158
159 if (controller_->template_url()->prepopulate_id() != 0) {
160 GtkWidget* fake_label = gtk_label_new("Fake label");
161 gtk_widget_set_sensitive(fake_label,
162 controller_->template_url()->prepopulate_id() == 0);
163 GtkStyle* label_style = gtk_widget_get_style(fake_label);
164 GtkStyle* dialog_style = gtk_widget_get_style(dialog_);
165 SetWidgetStyle(url_entry_, label_style, dialog_style);
166 gtk_widget_destroy(fake_label);
167 }
168 }
169
170 GtkWidget* controls = gtk_util::CreateLabeledControlsGroup(NULL,
171 l10n_util::GetStringUTF8(
172 IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_LABEL).c_str(),
173 gtk_util::CreateEntryImageHBox(title_entry_, title_image_),
174 l10n_util::GetStringUTF8(IDS_SEARCH_ENGINES_EDITOR_KEYWORD_LABEL).c_str(),
175 gtk_util::CreateEntryImageHBox(keyword_entry_, keyword_image_),
176 l10n_util::GetStringUTF8(IDS_SEARCH_ENGINES_EDITOR_URL_LABEL).c_str(),
177 gtk_util::CreateEntryImageHBox(url_entry_, url_image_),
178 NULL);
179 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog_)->vbox), controls,
180 FALSE, FALSE, 0);
181
182 // On RTL UIs (such as Arabic and Hebrew) the description text is not
183 // displayed correctly since it contains the substring "%s". This substring
184 // is not interpreted by the Unicode BiDi algorithm as an LTR string and
185 // therefore the end result is that the following right to left text is
186 // displayed: ".three two s% one" (where 'one', 'two', etc. are words in
187 // Hebrew).
188 //
189 // In order to fix this problem we transform the substring "%s" so that it
190 // is displayed correctly when rendered in an RTL context.
191 std::string description =
192 l10n_util::GetStringUTF8(IDS_SEARCH_ENGINES_EDITOR_URL_DESCRIPTION_LABEL);
193 if (base::i18n::IsRTL()) {
194 const std::string reversed_percent("s%");
195 std::string::size_type percent_index = description.find("%s");
196 if (percent_index != std::string::npos) {
197 description.replace(percent_index,
198 reversed_percent.length(),
199 reversed_percent);
200 }
201 }
202
203 GtkWidget* description_label = gtk_label_new(description.c_str());
204 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog_)->vbox), description_label,
205 FALSE, FALSE, 0);
206
207 gtk_box_set_spacing(GTK_BOX(GTK_DIALOG(dialog_)->vbox),
208 gtk_util::kContentAreaSpacing);
209
210 EnableControls();
211
212 gtk_util::ShowDialog(dialog_);
213
214 g_signal_connect(dialog_, "response", G_CALLBACK(OnResponseThunk), this);
215 g_signal_connect(dialog_, "destroy", G_CALLBACK(OnWindowDestroyThunk), this);
216 }
217
GetTitleInput() const218 string16 EditSearchEngineDialog::GetTitleInput() const {
219 return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(title_entry_)));
220 }
221
GetKeywordInput() const222 string16 EditSearchEngineDialog::GetKeywordInput() const {
223 return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(keyword_entry_)));
224 }
225
GetURLInput() const226 std::string EditSearchEngineDialog::GetURLInput() const {
227 return gtk_entry_get_text(GTK_ENTRY(url_entry_));
228 }
229
EnableControls()230 void EditSearchEngineDialog::EnableControls() {
231 gtk_widget_set_sensitive(ok_button_,
232 controller_->IsKeywordValid(GetKeywordInput()) &&
233 controller_->IsTitleValid(GetTitleInput()) &&
234 controller_->IsURLValid(GetURLInput()));
235 UpdateImage(keyword_image_, controller_->IsKeywordValid(GetKeywordInput()),
236 IDS_SEARCH_ENGINES_INVALID_KEYWORD_TT);
237 UpdateImage(url_image_, controller_->IsURLValid(GetURLInput()),
238 IDS_SEARCH_ENGINES_INVALID_URL_TT);
239 UpdateImage(title_image_, controller_->IsTitleValid(GetTitleInput()),
240 IDS_SEARCH_ENGINES_INVALID_TITLE_TT);
241 }
242
UpdateImage(GtkWidget * image,bool is_valid,int invalid_message_id)243 void EditSearchEngineDialog::UpdateImage(GtkWidget* image,
244 bool is_valid,
245 int invalid_message_id) {
246 if (is_valid) {
247 gtk_widget_set_has_tooltip(image, FALSE);
248 gtk_image_set_from_pixbuf(GTK_IMAGE(image),
249 ResourceBundle::GetSharedInstance().GetPixbufNamed(
250 IDR_INPUT_GOOD));
251 } else {
252 gtk_widget_set_tooltip_text(
253 image, l10n_util::GetStringUTF8(invalid_message_id).c_str());
254 gtk_image_set_from_pixbuf(GTK_IMAGE(image),
255 ResourceBundle::GetSharedInstance().GetPixbufNamed(
256 IDR_INPUT_ALERT));
257 }
258 }
259
OnEntryChanged(GtkEditable * editable)260 void EditSearchEngineDialog::OnEntryChanged(GtkEditable* editable) {
261 EnableControls();
262 }
263
OnResponse(GtkWidget * dialog,int response_id)264 void EditSearchEngineDialog::OnResponse(GtkWidget* dialog, int response_id) {
265 if (response_id == GTK_RESPONSE_OK) {
266 controller_->AcceptAddOrEdit(GetTitleInput(),
267 GetKeywordInput(),
268 GetURLInput());
269 } else {
270 controller_->CleanUpCancelledAdd();
271 }
272 gtk_widget_destroy(dialog_);
273 }
274
OnWindowDestroy(GtkWidget * widget)275 void EditSearchEngineDialog::OnWindowDestroy(GtkWidget* widget) {
276 MessageLoop::current()->DeleteSoon(FROM_HERE, this);
277 }
278