1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/speech/speech_recognition_bubble.h"
6
7#include "base/strings/utf_string_conversions.h"
8#include "chrome/browser/profiles/profile.h"
9#include "chrome/browser/ui/browser.h"
10#include "chrome/browser/ui/browser_finder.h"
11#include "chrome/browser/ui/gtk/browser_toolbar_gtk.h"
12#include "chrome/browser/ui/gtk/browser_window_gtk.h"
13#include "chrome/browser/ui/gtk/bubble/bubble_gtk.h"
14#include "chrome/browser/ui/gtk/gtk_chrome_link_button.h"
15#include "chrome/browser/ui/gtk/gtk_theme_service.h"
16#include "chrome/browser/ui/gtk/gtk_util.h"
17#include "chrome/browser/ui/gtk/location_bar_view_gtk.h"
18#include "content/public/browser/resource_context.h"
19#include "content/public/browser/speech_recognition_manager.h"
20#include "content/public/browser/web_contents.h"
21#include "content/public/browser/web_contents_view.h"
22#include "grit/generated_resources.h"
23#include "grit/theme_resources.h"
24#include "ui/base/gtk/gtk_hig_constants.h"
25#include "ui/base/gtk/owned_widget_gtk.h"
26#include "ui/base/l10n/l10n_util.h"
27#include "ui/base/resource/resource_bundle.h"
28#include "ui/gfx/gtk_util.h"
29#include "ui/gfx/rect.h"
30
31using content::WebContents;
32
33namespace {
34
35const int kBubbleControlVerticalSpacing = 5;
36const int kBubbleControlHorizontalSpacing = 20;
37const int kIconHorizontalPadding = 10;
38const int kButtonBarHorizontalSpacing = 10;
39
40// Use black for text labels since the bubble has white background.
41const GdkColor& kLabelTextColor = ui::kGdkBlack;
42
43// Implementation of SpeechRecognitionBubble for GTK. This shows a speech
44// recognition bubble on screen.
45class SpeechRecognitionBubbleGtk : public SpeechRecognitionBubbleBase,
46                                   public BubbleDelegateGtk {
47 public:
48  SpeechRecognitionBubbleGtk(int render_process_id, int render_view_id,
49                             Delegate* delegate,
50                             const gfx::Rect& element_rect);
51  virtual ~SpeechRecognitionBubbleGtk();
52
53 private:
54  // SpeechRecognitionBubbleBase:
55  virtual void Show() OVERRIDE;
56  virtual void Hide() OVERRIDE;
57  virtual void UpdateLayout() OVERRIDE;
58  virtual void UpdateImage() OVERRIDE;
59
60  // BubbleDelegateGtk:
61  virtual void BubbleClosing(BubbleGtk* bubble, bool closed_by_escape) OVERRIDE;
62
63  CHROMEGTK_CALLBACK_0(SpeechRecognitionBubbleGtk, void, OnCancelClicked);
64  CHROMEGTK_CALLBACK_0(SpeechRecognitionBubbleGtk, void, OnTryAgainClicked);
65  CHROMEGTK_CALLBACK_0(SpeechRecognitionBubbleGtk, void, OnMicSettingsClicked);
66
67  Delegate* delegate_;
68  BubbleGtk* bubble_;
69  gfx::Rect element_rect_;
70  bool did_invoke_close_;
71
72  GtkWidget* label_;
73  GtkWidget* cancel_button_;
74  GtkWidget* try_again_button_;
75  GtkWidget* icon_;
76  GtkWidget* icon_container_;
77  GtkWidget* mic_settings_;
78
79  DISALLOW_COPY_AND_ASSIGN(SpeechRecognitionBubbleGtk);
80};
81
82SpeechRecognitionBubbleGtk::SpeechRecognitionBubbleGtk(
83    int render_process_id, int render_view_id, Delegate* delegate,
84    const gfx::Rect& element_rect)
85    : SpeechRecognitionBubbleBase(render_process_id, render_process_id),
86      delegate_(delegate),
87      bubble_(NULL),
88      element_rect_(element_rect),
89      did_invoke_close_(false),
90      label_(NULL),
91      cancel_button_(NULL),
92      try_again_button_(NULL),
93      icon_(NULL),
94      icon_container_(NULL),
95      mic_settings_(NULL) {
96}
97
98SpeechRecognitionBubbleGtk::~SpeechRecognitionBubbleGtk() {
99  // The |Close| call below invokes our |BubbleClosing| method. Since we were
100  // destroyed by the caller we don't need to call them back, hence set this
101  // flag here.
102  did_invoke_close_ = true;
103  Hide();
104}
105
106void SpeechRecognitionBubbleGtk::OnCancelClicked(GtkWidget* widget) {
107  delegate_->InfoBubbleButtonClicked(BUTTON_CANCEL);
108}
109
110void SpeechRecognitionBubbleGtk::OnTryAgainClicked(GtkWidget* widget) {
111  delegate_->InfoBubbleButtonClicked(BUTTON_TRY_AGAIN);
112}
113
114void SpeechRecognitionBubbleGtk::OnMicSettingsClicked(GtkWidget* widget) {
115  content::SpeechRecognitionManager::GetInstance()->ShowAudioInputSettings();
116  Hide();
117}
118
119void SpeechRecognitionBubbleGtk::Show() {
120  if (bubble_ || !GetWebContents())
121    return;  // Nothing further to do since the bubble is already visible.
122
123  // We use a vbox to arrange the controls (label, image, button bar) vertically
124  // and the button bar is a hbox holding the 2 buttons (try again and cancel).
125  // To get horizontal space around them we place this vbox with padding in a
126  // GtkAlignment below.
127  GtkWidget* vbox = gtk_vbox_new(FALSE, 0);
128
129  // The icon with a some padding on the left and right.
130  icon_container_ = gtk_alignment_new(0, 0, 0, 0);
131  icon_ = gtk_image_new();
132  gtk_container_add(GTK_CONTAINER(icon_container_), icon_);
133  gtk_box_pack_start(GTK_BOX(vbox), icon_container_, FALSE, FALSE,
134                     kBubbleControlVerticalSpacing);
135
136  label_ = gtk_label_new(NULL);
137  gtk_util::SetLabelColor(label_, &kLabelTextColor);
138  gtk_box_pack_start(GTK_BOX(vbox), label_, FALSE, FALSE,
139                     kBubbleControlVerticalSpacing);
140
141  Profile* profile = Profile::FromBrowserContext(
142      GetWebContents()->GetBrowserContext());
143
144  // TODO(tommi): The audio_manager property can only be accessed from the
145  // IO thread, so we can't call CanShowAudioInputSettings directly here if
146  // we can show the input settings.  For now, we always show the link (like
147  // we do on other platforms).
148  if (true) {
149    mic_settings_ = gtk_chrome_link_button_new(
150        l10n_util::GetStringUTF8(IDS_SPEECH_INPUT_MIC_SETTINGS).c_str());
151    gtk_box_pack_start(GTK_BOX(vbox), mic_settings_, FALSE, FALSE,
152                       kBubbleControlVerticalSpacing);
153    g_signal_connect(mic_settings_, "clicked",
154                     G_CALLBACK(&OnMicSettingsClickedThunk), this);
155  }
156
157  GtkWidget* button_bar = gtk_hbox_new(FALSE, kButtonBarHorizontalSpacing);
158  gtk_box_pack_start(GTK_BOX(vbox), button_bar, FALSE, FALSE,
159                     kBubbleControlVerticalSpacing);
160
161  cancel_button_ = gtk_button_new_with_label(
162      l10n_util::GetStringUTF8(IDS_CANCEL).c_str());
163  gtk_box_pack_start(GTK_BOX(button_bar), cancel_button_, TRUE, FALSE, 0);
164  g_signal_connect(cancel_button_, "clicked",
165                   G_CALLBACK(&OnCancelClickedThunk), this);
166
167  try_again_button_ = gtk_button_new_with_label(
168      l10n_util::GetStringUTF8(IDS_SPEECH_INPUT_TRY_AGAIN).c_str());
169  gtk_box_pack_start(GTK_BOX(button_bar), try_again_button_, TRUE, FALSE, 0);
170  g_signal_connect(try_again_button_, "clicked",
171                   G_CALLBACK(&OnTryAgainClickedThunk), this);
172
173  GtkWidget* content = gtk_alignment_new(0, 0, 0, 0);
174  gtk_alignment_set_padding(GTK_ALIGNMENT(content),
175      kBubbleControlVerticalSpacing, kBubbleControlVerticalSpacing,
176      kBubbleControlHorizontalSpacing, kBubbleControlHorizontalSpacing);
177  gtk_container_add(GTK_CONTAINER(content), vbox);
178
179  GtkThemeService* theme_provider = GtkThemeService::GetFrom(profile);
180  GtkWidget* reference_widget = GetWebContents()->GetView()->GetNativeView();
181  gfx::Rect container_rect;
182  GetWebContents()->GetView()->GetContainerBounds(&container_rect);
183  gfx::Rect target_rect(element_rect_.right() - kBubbleTargetOffsetX,
184      element_rect_.bottom(), 1, 1);
185
186  if (target_rect.x() < 0 || target_rect.y() < 0 ||
187      target_rect.x() > container_rect.width() ||
188      target_rect.y() > container_rect.height()) {
189    // Target is not in screen view, so point to wrench.
190    Browser* browser = chrome::FindBrowserWithWebContents(GetWebContents());
191    BrowserWindowGtk* browser_window =
192        BrowserWindowGtk::GetBrowserWindowForNativeWindow(
193            browser->window()->GetNativeWindow());
194    reference_widget = browser_window->GetToolbar()->GetLocationBarView()
195        ->location_icon_widget();
196    target_rect = gtk_util::WidgetBounds(reference_widget);
197  }
198  bubble_ = BubbleGtk::Show(reference_widget,
199                            &target_rect,
200                            content,
201                            BubbleGtk::ANCHOR_TOP_LEFT,
202                            BubbleGtk::POPUP_WINDOW | BubbleGtk::GRAB_INPUT,
203                            theme_provider,
204                            this);
205
206  UpdateLayout();
207}
208
209void SpeechRecognitionBubbleGtk::Hide() {
210  if (bubble_)
211    bubble_->Close();
212}
213
214void SpeechRecognitionBubbleGtk::UpdateLayout() {
215  if (!bubble_ || !GetWebContents())
216    return;
217
218  if (display_mode() == DISPLAY_MODE_MESSAGE) {
219    // Message text and the Try Again + Cancel buttons are visible, hide the
220    // icon.
221    gtk_label_set_text(GTK_LABEL(label_),
222                       UTF16ToUTF8(message_text()).c_str());
223    gtk_widget_show(label_);
224    gtk_widget_show(try_again_button_);
225    if (mic_settings_)
226      gtk_widget_show(mic_settings_);
227    gtk_widget_hide(icon_);
228  } else {
229    // Heading text, icon and cancel button are visible, hide the Try Again
230    // button.
231    gtk_label_set_text(GTK_LABEL(label_),
232        l10n_util::GetStringUTF8(IDS_SPEECH_INPUT_BUBBLE_HEADING).c_str());
233    if (display_mode() == DISPLAY_MODE_RECORDING) {
234      gtk_widget_show(label_);
235    } else {
236      gtk_widget_hide(label_);
237    }
238    UpdateImage();
239    gtk_widget_show(icon_);
240    gtk_widget_hide(try_again_button_);
241    if (mic_settings_)
242      gtk_widget_hide(mic_settings_);
243    if (display_mode() == DISPLAY_MODE_WARM_UP) {
244      gtk_widget_hide(cancel_button_);
245
246      // The text label and cancel button are hidden in this mode, but we want
247      // the popup to appear the same size as it would once recording starts,
248      // so as to reduce UI jank when recording starts. So we calculate the
249      // difference in size between the two sets of controls and add that as
250      // padding around the icon here.
251      GtkRequisition cancel_size;
252      gtk_widget_get_child_requisition(cancel_button_, &cancel_size);
253      GtkRequisition label_size;
254      gtk_widget_get_child_requisition(label_, &label_size);
255      gfx::ImageSkia* volume = ResourceBundle::GetSharedInstance().
256          GetImageSkiaNamed(IDR_SPEECH_INPUT_MIC_EMPTY);
257      int desired_width = std::max(volume->width(), cancel_size.width) +
258                          kIconHorizontalPadding * 2;
259      int desired_height = volume->height() + label_size.height +
260                           cancel_size.height +
261                           kBubbleControlVerticalSpacing * 2;
262      int diff_width = desired_width - icon_image().width();
263      int diff_height = desired_height - icon_image().height();
264      gtk_alignment_set_padding(GTK_ALIGNMENT(icon_container_),
265                                diff_height / 2, diff_height - diff_height / 2,
266                                diff_width / 2, diff_width - diff_width / 2);
267    } else {
268      // Reset the padding done above.
269      gtk_alignment_set_padding(GTK_ALIGNMENT(icon_container_), 0, 0,
270                                kIconHorizontalPadding, kIconHorizontalPadding);
271      gtk_widget_show(cancel_button_);
272    }
273  }
274}
275
276void SpeechRecognitionBubbleGtk::UpdateImage() {
277  gfx::ImageSkia image = icon_image();
278  if (image.isNull() || !bubble_)
279    return;
280
281  GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(*image.bitmap());
282  gtk_image_set_from_pixbuf(GTK_IMAGE(icon_), pixbuf);
283  g_object_unref(pixbuf);
284}
285
286void SpeechRecognitionBubbleGtk::BubbleClosing(BubbleGtk* bubble,
287                                               bool closed_by_escape) {
288  bubble_ = NULL;
289  if (!did_invoke_close_)
290    delegate_->InfoBubbleFocusChanged();
291}
292
293}  // namespace
294
295SpeechRecognitionBubble* SpeechRecognitionBubble::CreateNativeBubble(
296    int render_process_id, int render_view_id,
297    SpeechRecognitionBubble::Delegate* delegate,
298    const gfx::Rect& element_rect) {
299  return new SpeechRecognitionBubbleGtk(render_process_id, render_view_id,
300      delegate, element_rect);
301}
302