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 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.) 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 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 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 79 EditSearchEngineDialog::~EditSearchEngineDialog() {} 80 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 218 string16 EditSearchEngineDialog::GetTitleInput() const { 219 return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(title_entry_))); 220 } 221 222 string16 EditSearchEngineDialog::GetKeywordInput() const { 223 return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(keyword_entry_))); 224 } 225 226 std::string EditSearchEngineDialog::GetURLInput() const { 227 return gtk_entry_get_text(GTK_ENTRY(url_entry_)); 228 } 229 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 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 260 void EditSearchEngineDialog::OnEntryChanged(GtkEditable* editable) { 261 EnableControls(); 262 } 263 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 275 void EditSearchEngineDialog::OnWindowDestroy(GtkWidget* widget) { 276 MessageLoop::current()->DeleteSoon(FROM_HERE, this); 277 } 278