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