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