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/speech/speech_input_bubble.h" 6 7 #include "base/utf_string_conversions.h" 8 #include "chrome/browser/ui/gtk/gtk_chrome_link_button.h" 9 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 10 #include "chrome/browser/ui/gtk/gtk_util.h" 11 #include "chrome/browser/ui/gtk/info_bubble_gtk.h" 12 #include "chrome/browser/ui/gtk/owned_widget_gtk.h" 13 #include "content/browser/tab_contents/tab_contents.h" 14 #include "grit/generated_resources.h" 15 #include "grit/theme_resources.h" 16 #include "media/audio/audio_manager.h" 17 #include "ui/base/l10n/l10n_util.h" 18 #include "ui/base/resource/resource_bundle.h" 19 #include "ui/gfx/gtk_util.h" 20 #include "ui/gfx/rect.h" 21 22 namespace { 23 24 const int kBubbleControlVerticalSpacing = 5; 25 const int kBubbleControlHorizontalSpacing = 20; 26 const int kIconHorizontalPadding = 10; 27 const int kButtonBarHorizontalSpacing = 10; 28 29 // Use black for text labels since the bubble has white background. 30 const GdkColor kLabelTextColor = gtk_util::kGdkBlack; 31 32 // Implementation of SpeechInputBubble for GTK. This shows a speech input 33 // info bubble on screen. 34 class SpeechInputBubbleGtk 35 : public SpeechInputBubbleBase, 36 public InfoBubbleGtkDelegate { 37 public: 38 SpeechInputBubbleGtk(TabContents* tab_contents, 39 Delegate* delegate, 40 const gfx::Rect& element_rect); 41 ~SpeechInputBubbleGtk(); 42 43 private: 44 // InfoBubbleDelegate methods. 45 virtual void InfoBubbleClosing(InfoBubbleGtk* info_bubble, 46 bool closed_by_escape); 47 48 // SpeechInputBubble methods. 49 virtual void Show(); 50 virtual void Hide(); 51 virtual void UpdateLayout(); 52 virtual void UpdateImage(); 53 54 CHROMEGTK_CALLBACK_0(SpeechInputBubbleGtk, void, OnCancelClicked); 55 CHROMEGTK_CALLBACK_0(SpeechInputBubbleGtk, void, OnTryAgainClicked); 56 CHROMEGTK_CALLBACK_0(SpeechInputBubbleGtk, void, OnMicSettingsClicked); 57 58 Delegate* delegate_; 59 InfoBubbleGtk* info_bubble_; 60 gfx::Rect element_rect_; 61 bool did_invoke_close_; 62 63 GtkWidget* label_; 64 GtkWidget* cancel_button_; 65 GtkWidget* try_again_button_; 66 GtkWidget* icon_; 67 GtkWidget* icon_container_; 68 GtkWidget* mic_settings_; 69 70 DISALLOW_COPY_AND_ASSIGN(SpeechInputBubbleGtk); 71 }; 72 73 SpeechInputBubbleGtk::SpeechInputBubbleGtk(TabContents* tab_contents, 74 Delegate* delegate, 75 const gfx::Rect& element_rect) 76 : SpeechInputBubbleBase(tab_contents), 77 delegate_(delegate), 78 info_bubble_(NULL), 79 element_rect_(element_rect), 80 did_invoke_close_(false), 81 label_(NULL), 82 cancel_button_(NULL), 83 try_again_button_(NULL), 84 icon_(NULL), 85 icon_container_(NULL), 86 mic_settings_(NULL) { 87 } 88 89 SpeechInputBubbleGtk::~SpeechInputBubbleGtk() { 90 // The |Close| call below invokes our |InfoBubbleClosing| method. Since we 91 // were destroyed by the caller we don't need to call them back, hence set 92 // this flag here. 93 did_invoke_close_ = true; 94 Hide(); 95 } 96 97 void SpeechInputBubbleGtk::InfoBubbleClosing(InfoBubbleGtk* info_bubble, 98 bool closed_by_escape) { 99 info_bubble_ = NULL; 100 if (!did_invoke_close_) 101 delegate_->InfoBubbleFocusChanged(); 102 } 103 104 void SpeechInputBubbleGtk::OnCancelClicked(GtkWidget* widget) { 105 delegate_->InfoBubbleButtonClicked(BUTTON_CANCEL); 106 } 107 108 void SpeechInputBubbleGtk::OnTryAgainClicked(GtkWidget* widget) { 109 delegate_->InfoBubbleButtonClicked(BUTTON_TRY_AGAIN); 110 } 111 112 void SpeechInputBubbleGtk::OnMicSettingsClicked(GtkWidget* widget) { 113 AudioManager::GetAudioManager()->ShowAudioInputSettings(); 114 } 115 116 void SpeechInputBubbleGtk::Show() { 117 if (info_bubble_) 118 return; // Nothing further to do since the bubble is already visible. 119 120 // We use a vbox to arrange the controls (label, image, button bar) vertically 121 // and the button bar is a hbox holding the 2 buttons (try again and cancel). 122 // To get horizontal space around them we place this vbox with padding in a 123 // GtkAlignment below. 124 GtkWidget* vbox = gtk_vbox_new(FALSE, 0); 125 126 // The icon with a some padding on the left and right. 127 icon_container_ = gtk_alignment_new(0, 0, 0, 0); 128 icon_ = gtk_image_new(); 129 gtk_container_add(GTK_CONTAINER(icon_container_), icon_); 130 gtk_box_pack_start(GTK_BOX(vbox), icon_container_, FALSE, FALSE, 131 kBubbleControlVerticalSpacing); 132 133 label_ = gtk_label_new(NULL); 134 gtk_util::SetLabelColor(label_, &kLabelTextColor); 135 gtk_box_pack_start(GTK_BOX(vbox), label_, FALSE, FALSE, 136 kBubbleControlVerticalSpacing); 137 138 if (AudioManager::GetAudioManager()->CanShowAudioInputSettings()) { 139 mic_settings_ = gtk_chrome_link_button_new( 140 l10n_util::GetStringUTF8(IDS_SPEECH_INPUT_MIC_SETTINGS).c_str()); 141 gtk_box_pack_start(GTK_BOX(vbox), mic_settings_, FALSE, FALSE, 142 kBubbleControlVerticalSpacing); 143 g_signal_connect(mic_settings_, "clicked", 144 G_CALLBACK(&OnMicSettingsClickedThunk), this); 145 } 146 147 GtkWidget* button_bar = gtk_hbox_new(FALSE, kButtonBarHorizontalSpacing); 148 gtk_box_pack_start(GTK_BOX(vbox), button_bar, FALSE, FALSE, 149 kBubbleControlVerticalSpacing); 150 151 cancel_button_ = gtk_button_new_with_label( 152 l10n_util::GetStringUTF8(IDS_CANCEL).c_str()); 153 gtk_box_pack_start(GTK_BOX(button_bar), cancel_button_, TRUE, FALSE, 0); 154 g_signal_connect(cancel_button_, "clicked", 155 G_CALLBACK(&OnCancelClickedThunk), this); 156 157 try_again_button_ = gtk_button_new_with_label( 158 l10n_util::GetStringUTF8(IDS_SPEECH_INPUT_TRY_AGAIN).c_str()); 159 gtk_box_pack_start(GTK_BOX(button_bar), try_again_button_, TRUE, FALSE, 0); 160 g_signal_connect(try_again_button_, "clicked", 161 G_CALLBACK(&OnTryAgainClickedThunk), this); 162 163 GtkWidget* content = gtk_alignment_new(0, 0, 0, 0); 164 gtk_alignment_set_padding(GTK_ALIGNMENT(content), 165 kBubbleControlVerticalSpacing, kBubbleControlVerticalSpacing, 166 kBubbleControlHorizontalSpacing, kBubbleControlHorizontalSpacing); 167 gtk_container_add(GTK_CONTAINER(content), vbox); 168 169 GtkThemeService* theme_provider = GtkThemeService::GetFrom( 170 tab_contents()->profile()); 171 gfx::Rect rect( 172 element_rect_.x() + element_rect_.width() - kBubbleTargetOffsetX, 173 element_rect_.y() + element_rect_.height(), 1, 1); 174 info_bubble_ = InfoBubbleGtk::Show(tab_contents()->GetNativeView(), 175 &rect, 176 content, 177 InfoBubbleGtk::ARROW_LOCATION_TOP_LEFT, 178 false, // match_system_theme 179 true, // grab_input 180 theme_provider, 181 this); 182 183 UpdateLayout(); 184 } 185 186 void SpeechInputBubbleGtk::Hide() { 187 if (info_bubble_) 188 info_bubble_->Close(); 189 } 190 191 void SpeechInputBubbleGtk::UpdateLayout() { 192 if (!info_bubble_) 193 return; 194 195 if (display_mode() == DISPLAY_MODE_MESSAGE) { 196 // Message text and the Try Again + Cancel buttons are visible, hide the 197 // icon. 198 gtk_label_set_text(GTK_LABEL(label_), 199 UTF16ToUTF8(message_text()).c_str()); 200 gtk_widget_show(label_); 201 gtk_widget_show(try_again_button_); 202 if (mic_settings_) 203 gtk_widget_show(mic_settings_); 204 gtk_widget_hide(icon_); 205 } else { 206 // Heading text, icon and cancel button are visible, hide the Try Again 207 // button. 208 gtk_label_set_text(GTK_LABEL(label_), 209 l10n_util::GetStringUTF8(IDS_SPEECH_INPUT_BUBBLE_HEADING).c_str()); 210 if (display_mode() == DISPLAY_MODE_RECORDING) { 211 gtk_widget_show(label_); 212 } else { 213 gtk_widget_hide(label_); 214 } 215 UpdateImage(); 216 gtk_widget_show(icon_); 217 gtk_widget_hide(try_again_button_); 218 if (mic_settings_) 219 gtk_widget_hide(mic_settings_); 220 if (display_mode() == DISPLAY_MODE_WARM_UP) { 221 gtk_widget_hide(cancel_button_); 222 223 // The text label and cancel button are hidden in this mode, but we want 224 // the popup to appear the same size as it would once recording starts, 225 // so as to reduce UI jank when recording starts. So we calculate the 226 // difference in size between the two sets of controls and add that as 227 // padding around the icon here. 228 GtkRequisition cancel_size; 229 gtk_widget_get_child_requisition(cancel_button_, &cancel_size); 230 GtkRequisition label_size; 231 gtk_widget_get_child_requisition(label_, &label_size); 232 SkBitmap* volume = ResourceBundle::GetSharedInstance().GetBitmapNamed( 233 IDR_SPEECH_INPUT_MIC_EMPTY); 234 int desired_width = std::max(volume->width(), cancel_size.width) + 235 kIconHorizontalPadding * 2; 236 int desired_height = volume->height() + label_size.height + 237 cancel_size.height + 238 kBubbleControlVerticalSpacing * 2; 239 int diff_width = desired_width - icon_image().width(); 240 int diff_height = desired_height - icon_image().height(); 241 gtk_alignment_set_padding(GTK_ALIGNMENT(icon_container_), 242 diff_height / 2, diff_height - diff_height / 2, 243 diff_width / 2, diff_width - diff_width / 2); 244 } else { 245 // Reset the padding done above. 246 gtk_alignment_set_padding(GTK_ALIGNMENT(icon_container_), 0, 0, 247 kIconHorizontalPadding, kIconHorizontalPadding); 248 gtk_widget_show(cancel_button_); 249 } 250 } 251 } 252 253 void SpeechInputBubbleGtk::UpdateImage() { 254 SkBitmap image = icon_image(); 255 if (image.isNull() || !info_bubble_) 256 return; 257 258 GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(&image); 259 gtk_image_set_from_pixbuf(GTK_IMAGE(icon_), pixbuf); 260 g_object_unref(pixbuf); 261 } 262 263 } // namespace 264 265 SpeechInputBubble* SpeechInputBubble::CreateNativeBubble( 266 TabContents* tab_contents, 267 Delegate* delegate, 268 const gfx::Rect& element_rect) { 269 return new SpeechInputBubbleGtk(tab_contents, delegate, element_rect); 270 } 271 272