| OLD | NEW |
| (Empty) |
| 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/ui/gtk/web_intent_picker_gtk.h" | |
| 6 | |
| 7 #include <algorithm> | |
| 8 | |
| 9 #include "base/i18n/rtl.h" | |
| 10 #include "base/utf_string_conversions.h" | |
| 11 #include "chrome/browser/favicon/favicon_service.h" | |
| 12 #include "chrome/browser/profiles/profile.h" | |
| 13 #include "chrome/browser/tab_contents/tab_util.h" | |
| 14 #include "chrome/browser/ui/browser_finder.h" | |
| 15 #include "chrome/browser/ui/browser_window.h" | |
| 16 #include "chrome/browser/ui/gtk/browser_toolbar_gtk.h" | |
| 17 #include "chrome/browser/ui/gtk/browser_window_gtk.h" | |
| 18 #include "chrome/browser/ui/gtk/custom_button.h" | |
| 19 #include "chrome/browser/ui/gtk/event_utils.h" | |
| 20 #include "chrome/browser/ui/gtk/gtk_chrome_link_button.h" | |
| 21 #include "chrome/browser/ui/gtk/gtk_theme_service.h" | |
| 22 #include "chrome/browser/ui/gtk/gtk_util.h" | |
| 23 #include "chrome/browser/ui/gtk/location_bar_view_gtk.h" | |
| 24 #include "chrome/browser/ui/gtk/throbber_gtk.h" | |
| 25 #include "chrome/browser/ui/intents/web_intent_picker_controller.h" | |
| 26 #include "chrome/browser/ui/intents/web_intent_picker_delegate.h" | |
| 27 #include "chrome/browser/ui/intents/web_intent_picker_model.h" | |
| 28 #include "chrome/common/chrome_notification_types.h" | |
| 29 #include "content/public/browser/notification_source.h" | |
| 30 #include "content/public/browser/notification_types.h" | |
| 31 #include "content/public/browser/render_view_host.h" | |
| 32 #include "content/public/browser/render_widget_host_view.h" | |
| 33 #include "content/public/browser/web_contents.h" | |
| 34 #include "content/public/browser/web_contents_view.h" | |
| 35 #include "googleurl/src/gurl.h" | |
| 36 #include "grit/chromium_strings.h" | |
| 37 #include "grit/generated_resources.h" | |
| 38 #include "grit/google_chrome_strings.h" | |
| 39 #include "grit/theme_resources.h" | |
| 40 #include "ui/base/gtk/gtk_hig_constants.h" | |
| 41 #include "ui/base/gtk/gtk_signal_registrar.h" | |
| 42 #include "ui/base/l10n/l10n_util.h" | |
| 43 #include "ui/base/resource/resource_bundle.h" | |
| 44 #include "ui/base/text/text_elider.h" | |
| 45 #include "ui/gfx/gtk_util.h" | |
| 46 #include "ui/gfx/image/image.h" | |
| 47 | |
| 48 using content::WebContents; | |
| 49 | |
| 50 namespace { | |
| 51 | |
| 52 // The pixel size of the header label when using a non-native theme. | |
| 53 const int kHeaderLabelPixelSize = 15; | |
| 54 | |
| 55 // The pixel size of the font of the main content of the dialog. | |
| 56 const int kMainContentPixelSize = 13; | |
| 57 | |
| 58 // Indices of the extension row widgets. | |
| 59 enum { | |
| 60 kIconIndex, | |
| 61 kTitleLinkIndex, | |
| 62 kStarsIndex, | |
| 63 kInstallButtonIndex, | |
| 64 }; | |
| 65 | |
| 66 GtkThemeService *GetThemeService(WebContents* web_contents) { | |
| 67 Profile* profile = | |
| 68 Profile::FromBrowserContext(web_contents->GetBrowserContext()); | |
| 69 return GtkThemeService::GetFrom(profile); | |
| 70 } | |
| 71 | |
| 72 // Set the image of |button| to |pixbuf|. | |
| 73 void SetServiceButtonImage(GtkWidget* button, GdkPixbuf* pixbuf) { | |
| 74 gtk_button_set_image(GTK_BUTTON(button), gtk_image_new_from_pixbuf(pixbuf)); | |
| 75 gtk_button_set_image_position(GTK_BUTTON(button), GTK_POS_LEFT); | |
| 76 } | |
| 77 | |
| 78 void SetWidgetFontSizeCallback(GtkWidget* widget, gpointer data) { | |
| 79 if (GTK_IS_LABEL(widget)) { | |
| 80 int* size = static_cast<int*>(data); | |
| 81 gtk_util::ForceFontSizePixels(widget, *size); | |
| 82 return; | |
| 83 } | |
| 84 | |
| 85 if (GTK_IS_CONTAINER(widget)) | |
| 86 gtk_container_forall(GTK_CONTAINER(widget), SetWidgetFontSizeCallback, | |
| 87 data); | |
| 88 } | |
| 89 | |
| 90 void SetWidgetFontSize(GtkWidget* widget, int size) { | |
| 91 gtk_container_forall(GTK_CONTAINER(widget), SetWidgetFontSizeCallback, &size); | |
| 92 } | |
| 93 | |
| 94 // Get the index of the row containing |widget|. Assume the widget is the child | |
| 95 // of an hbox, which is a child of a vbox. The hbox represents a row, and the | |
| 96 // vbox the full table. | |
| 97 size_t GetExtensionWidgetRow(GtkWidget* widget) { | |
| 98 GtkWidget* hbox = gtk_widget_get_parent(widget); | |
| 99 DCHECK(hbox); | |
| 100 GtkWidget* vbox = gtk_widget_get_parent(hbox); | |
| 101 DCHECK(vbox); | |
| 102 GList* hbox_list = gtk_container_get_children(GTK_CONTAINER(vbox)); | |
| 103 gint index = g_list_index(hbox_list, hbox); | |
| 104 DCHECK(index != -1); | |
| 105 g_list_free(hbox_list); | |
| 106 | |
| 107 return index; | |
| 108 } | |
| 109 | |
| 110 // A gtk_container_foreach callback to enable/disable a widget. | |
| 111 void EnableWidgetCallback(GtkWidget* widget, gpointer data) { | |
| 112 gtk_widget_set_sensitive(widget, *static_cast<gboolean*>(data)); | |
| 113 } | |
| 114 | |
| 115 // Create a new widget displaying |rating| as |kNumStarsPerRating| star images. | |
| 116 // Rating should be in the range [0, kNumStarsPerRating]. | |
| 117 GtkWidget* CreateStarsWidget(double rating) { | |
| 118 const int kNumStarsPerRating = 5; // Number of stars in a rating. | |
| 119 const int kStarSpacing = 1; // Spacing between stars in pixels. | |
| 120 GtkWidget* hbox = gtk_hbox_new(FALSE, kStarSpacing); | |
| 121 | |
| 122 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
| 123 for (int i = 0; i < kNumStarsPerRating; ++i) { | |
| 124 GdkPixbuf* star = rb.GetNativeImageNamed( | |
| 125 WebIntentPicker::GetNthStarImageIdFromCWSRating(rating, i), | |
| 126 ui::ResourceBundle::RTL_ENABLED).ToGdkPixbuf(); | |
| 127 gtk_box_pack_start(GTK_BOX(hbox), gtk_image_new_from_pixbuf(star), | |
| 128 FALSE, FALSE, 0); | |
| 129 } | |
| 130 | |
| 131 return hbox; | |
| 132 } | |
| 133 | |
| 134 } // namespace | |
| 135 | |
| 136 // Create a new Widget for the "waiting for CWS" display. | |
| 137 | |
| 138 class WaitingDialog { | |
| 139 public: | |
| 140 explicit WaitingDialog(GtkThemeService* theme_service); | |
| 141 ~WaitingDialog(); | |
| 142 | |
| 143 GtkWidget* widget() const { return widget_.get(); } | |
| 144 private: | |
| 145 // Initialize the widget. | |
| 146 void Init(); | |
| 147 | |
| 148 // The actual GtkWidget | |
| 149 ui::OwnedWidgetGtk widget_; | |
| 150 | |
| 151 // Waiting throbber. | |
| 152 scoped_ptr<ThrobberGtk> throbber_; | |
| 153 | |
| 154 // Weak pointer to theme service. | |
| 155 GtkThemeService* theme_service_; | |
| 156 }; | |
| 157 | |
| 158 WaitingDialog::WaitingDialog(GtkThemeService* theme_service) | |
| 159 : theme_service_(theme_service) { | |
| 160 DCHECK(theme_service_); | |
| 161 Init(); | |
| 162 } | |
| 163 | |
| 164 WaitingDialog::~WaitingDialog() { | |
| 165 widget_.Destroy(); | |
| 166 } | |
| 167 | |
| 168 void WaitingDialog::Init() { | |
| 169 const int kDialogSpacing = 30; | |
| 170 | |
| 171 widget_.Own(gtk_vbox_new(FALSE, 0)); | |
| 172 GtkWidget* vbox = widget_.get(); | |
| 173 | |
| 174 // Create throbber | |
| 175 ThrobberGtk* throbber = new ThrobberGtk(theme_service_); | |
| 176 GtkWidget* throbber_alignment = gtk_alignment_new(0.5, 0.5, 0, 0); | |
| 177 gtk_alignment_set_padding(GTK_ALIGNMENT(throbber_alignment), kDialogSpacing, | |
| 178 kMainContentPixelSize, 0, 0); | |
| 179 gtk_container_add(GTK_CONTAINER(throbber_alignment), throbber->widget()); | |
| 180 gtk_box_pack_start(GTK_BOX(vbox), throbber_alignment, TRUE, TRUE, 0); | |
| 181 | |
| 182 // Add the message text. | |
| 183 GtkWidget* message_label = theme_service_->BuildLabel( | |
| 184 l10n_util::GetStringUTF8(IDS_INTENT_PICKER_WAIT_FOR_CWS).c_str(), | |
| 185 ui::kGdkBlack); | |
| 186 | |
| 187 GtkWidget* label_alignment = gtk_alignment_new(0.5, 0.5, 0, 0); | |
| 188 gtk_alignment_set_padding(GTK_ALIGNMENT(label_alignment), | |
| 189 kMainContentPixelSize, kDialogSpacing, 0, 0); | |
| 190 gtk_container_add(GTK_CONTAINER(label_alignment), message_label); | |
| 191 gtk_box_pack_start(GTK_BOX(vbox), label_alignment, TRUE, TRUE, 0); | |
| 192 | |
| 193 // TODO(groby): use IDR_SPEECH_INPUT_SPINNER. Pending fix for ThrobberGtk. | |
| 194 // Animate throbber | |
| 195 throbber->Start(); | |
| 196 } | |
| 197 | |
| 198 // static | |
| 199 WebIntentPicker* WebIntentPicker::Create(WebContents* web_contents, | |
| 200 WebIntentPickerDelegate* delegate, | |
| 201 WebIntentPickerModel* model) { | |
| 202 return new WebIntentPickerGtk(web_contents, delegate, model); | |
| 203 } | |
| 204 | |
| 205 WebIntentPickerGtk::WebIntentPickerGtk(WebContents* web_contents, | |
| 206 WebIntentPickerDelegate* delegate, | |
| 207 WebIntentPickerModel* model) | |
| 208 : web_contents_(web_contents), | |
| 209 delegate_(delegate), | |
| 210 model_(model), | |
| 211 contents_(NULL), | |
| 212 header_label_(NULL), | |
| 213 button_vbox_(NULL), | |
| 214 cws_label_(NULL), | |
| 215 extensions_vbox_(NULL), | |
| 216 service_hbox_(NULL), | |
| 217 window_(NULL) { | |
| 218 DCHECK(delegate_ != NULL); | |
| 219 | |
| 220 model_->set_observer(this); | |
| 221 InitContents(); | |
| 222 UpdateInstalledServices(); | |
| 223 UpdateCWSLabel(); | |
| 224 UpdateSuggestedExtensions(); | |
| 225 | |
| 226 GtkThemeService* theme_service = GetThemeService(web_contents); | |
| 227 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, | |
| 228 content::Source<ThemeService>(theme_service)); | |
| 229 theme_service->InitThemesFor(this); | |
| 230 | |
| 231 window_ = new ConstrainedWindowGtk(web_contents, this); | |
| 232 | |
| 233 if (model_->IsInlineDisposition()) | |
| 234 OnInlineDisposition(string16(), model_->inline_disposition_url()); | |
| 235 } | |
| 236 | |
| 237 WebIntentPickerGtk::~WebIntentPickerGtk() { | |
| 238 } | |
| 239 | |
| 240 void WebIntentPickerGtk::Close() { | |
| 241 window_->CloseWebContentsModalDialog(); | |
| 242 if (inline_disposition_web_contents_.get()) | |
| 243 inline_disposition_web_contents_->OnCloseStarted(); | |
| 244 } | |
| 245 | |
| 246 void WebIntentPickerGtk::SetActionString(const string16& action) { | |
| 247 header_label_text_ = action; | |
| 248 if (header_label_) | |
| 249 gtk_label_set_text(GTK_LABEL(header_label_), UTF16ToUTF8(action).c_str()); | |
| 250 } | |
| 251 | |
| 252 void WebIntentPickerGtk::OnExtensionInstallSuccess(const std::string& id) { | |
| 253 RemoveThrobber(); | |
| 254 } | |
| 255 | |
| 256 void WebIntentPickerGtk::OnExtensionInstallFailure(const std::string& id) { | |
| 257 // The throbber has an alignment as its parent, so it must be used instead of | |
| 258 // the throbber to find the extension row. | |
| 259 size_t index = | |
| 260 GetExtensionWidgetRow(gtk_widget_get_parent(throbber_->widget())); | |
| 261 GList* vbox_list = | |
| 262 gtk_container_get_children(GTK_CONTAINER(extensions_vbox_)); | |
| 263 GtkWidget* hbox = static_cast<GtkWidget*>(g_list_nth_data(vbox_list, index)); | |
| 264 | |
| 265 RemoveThrobber(); | |
| 266 gtk_widget_show_all(hbox); | |
| 267 g_list_free(vbox_list); | |
| 268 SetWidgetsEnabled(true); | |
| 269 } | |
| 270 | |
| 271 void WebIntentPickerGtk::OnModelChanged(WebIntentPickerModel* model) { | |
| 272 if (waiting_dialog_.get() && !model->IsWaitingForSuggestions()) { | |
| 273 waiting_dialog_.reset(); | |
| 274 InitMainContents(); | |
| 275 } | |
| 276 UpdateInstalledServices(); | |
| 277 UpdateCWSLabel(); | |
| 278 UpdateSuggestedExtensions(); | |
| 279 SetActionString(header_label_text_); | |
| 280 } | |
| 281 | |
| 282 void WebIntentPickerGtk::OnFaviconChanged(WebIntentPickerModel* model, | |
| 283 size_t index) { | |
| 284 UpdateInstalledServices(); | |
| 285 } | |
| 286 | |
| 287 void WebIntentPickerGtk::OnExtensionIconChanged( | |
| 288 WebIntentPickerModel* model, | |
| 289 const std::string& extension_id) { | |
| 290 UpdateSuggestedExtensions(); | |
| 291 } | |
| 292 | |
| 293 void WebIntentPickerGtk::OnInlineDisposition(const string16&, | |
| 294 const GURL& url) { | |
| 295 DCHECK(delegate_); | |
| 296 Profile* profile = | |
| 297 Profile::FromBrowserContext(web_contents_->GetBrowserContext()); | |
| 298 inline_disposition_web_contents_.reset( | |
| 299 delegate_->CreateWebContentsForInlineDisposition(profile, url)); | |
| 300 Browser* browser = chrome::FindBrowserWithWebContents(web_contents_); | |
| 301 inline_disposition_delegate_.reset( | |
| 302 new WebIntentInlineDispositionDelegate( | |
| 303 this, inline_disposition_web_contents_.get(), browser)); | |
| 304 | |
| 305 inline_disposition_web_contents_->GetController().LoadURL( | |
| 306 url, content::Referrer(), content::PAGE_TRANSITION_AUTO_TOPLEVEL, | |
| 307 std::string()); | |
| 308 | |
| 309 // Replace the picker contents with the inline disposition. | |
| 310 ClearContents(); | |
| 311 gtk_widget_set_size_request(contents_, -1, -1); | |
| 312 window_->BackgroundColorChanged(); | |
| 313 | |
| 314 GtkWidget* vbox = gtk_vbox_new(FALSE, 0); | |
| 315 GtkThemeService* theme_service = GetThemeService(web_contents_); | |
| 316 | |
| 317 service_hbox_ = gtk_hbox_new(FALSE, ui::kControlSpacing); | |
| 318 // TODO(gbillock): Eventually get the service icon button here. | |
| 319 | |
| 320 // Intent action label. | |
| 321 const WebIntentPickerModel::InstalledService* service = | |
| 322 model_->GetInstalledServiceWithURL(url); | |
| 323 GtkWidget* action_label = gtk_label_new(UTF16ToUTF8(service->title).c_str()); | |
| 324 gtk_util::ForceFontSizePixels(action_label, kMainContentPixelSize); | |
| 325 // Hardcode color; don't allow theming. | |
| 326 gtk_util::SetLabelColor(action_label, &ui::kGdkBlack); | |
| 327 | |
| 328 GtkWidget* label_alignment = gtk_alignment_new(0, 0.5f, 0, 0); | |
| 329 gtk_container_add(GTK_CONTAINER(label_alignment), action_label); | |
| 330 GtkWidget* indent_label = gtk_util::IndentWidget(label_alignment); | |
| 331 | |
| 332 gtk_box_pack_start(GTK_BOX(service_hbox_), indent_label, FALSE, TRUE, 0); | |
| 333 g_signal_connect(service_hbox_, "destroy", G_CALLBACK(gtk_widget_destroyed), | |
| 334 &service_hbox_); | |
| 335 | |
| 336 // Add link for "choose another service" if other suggestions are available | |
| 337 // or if more than one (the current) service is installed. | |
| 338 if (model_->show_use_another_service() && | |
| 339 (model_->GetInstalledServiceCount() > 1 || | |
| 340 model_->GetSuggestedExtensionCount())) { | |
| 341 GtkWidget* use_alternate_link = theme_service->BuildChromeLinkButton( | |
| 342 l10n_util::GetStringUTF8( | |
| 343 IDS_INTENT_PICKER_USE_ALTERNATE_SERVICE).c_str()); | |
| 344 gtk_chrome_link_button_set_use_gtk_theme( | |
| 345 GTK_CHROME_LINK_BUTTON(use_alternate_link), | |
| 346 theme_service->UsingNativeTheme()); | |
| 347 gtk_util::ForceFontSizePixels( | |
| 348 GTK_CHROME_LINK_BUTTON(use_alternate_link)->label, | |
| 349 kMainContentPixelSize); | |
| 350 g_signal_connect(use_alternate_link, "clicked", | |
| 351 G_CALLBACK(OnChooseAnotherServiceClickThunk), this); | |
| 352 GtkWidget* link_alignment = gtk_alignment_new(0, 0.5f, 0, 0); | |
| 353 gtk_container_add(GTK_CONTAINER(link_alignment), use_alternate_link); | |
| 354 gtk_box_pack_start(GTK_BOX(service_hbox_), link_alignment, TRUE, TRUE, 0); | |
| 355 } | |
| 356 AddCloseButton(service_hbox_); | |
| 357 | |
| 358 // The header box | |
| 359 gtk_container_add(GTK_CONTAINER(vbox), service_hbox_); | |
| 360 | |
| 361 // hbox for the web contents, so we can have spacing on the borders. | |
| 362 GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
| 363 gtk_alignment_set_padding( | |
| 364 GTK_ALIGNMENT(alignment), 0, ui::kContentAreaBorder, | |
| 365 ui::kContentAreaBorder, ui::kContentAreaBorder); | |
| 366 gfx::NativeView web_contents_widget = | |
| 367 inline_disposition_web_contents_->GetView()->GetNativeView(); | |
| 368 gtk_container_add(GTK_CONTAINER(alignment), web_contents_widget); | |
| 369 gtk_container_add(GTK_CONTAINER(vbox), alignment); | |
| 370 gtk_container_add(GTK_CONTAINER(contents_), vbox); | |
| 371 | |
| 372 gfx::Size size = GetMinInlineDispositionSize(); | |
| 373 gtk_widget_set_size_request(web_contents_widget, | |
| 374 size.width(), size.height()); | |
| 375 gtk_widget_show_all(contents_); | |
| 376 | |
| 377 inline_disposition_delegate_->SetRenderViewSizeLimits(); | |
| 378 inline_disposition_web_contents_->GetView()->SetInitialFocus(); | |
| 379 host_signals_.reset(new ui::GtkSignalRegistrar()); | |
| 380 host_signals_->Connect( | |
| 381 web_contents_->GetRenderViewHost()->GetView()->GetNativeView(), | |
| 382 "size-allocate", | |
| 383 G_CALLBACK(OnHostContentsSizeAllocateThunk), this); | |
| 384 } | |
| 385 | |
| 386 void WebIntentPickerGtk::OnInlineDispositionAutoResize(const gfx::Size& size) { | |
| 387 gfx::NativeView web_contents_widget = | |
| 388 inline_disposition_web_contents_->GetView()->GetNativeView(); | |
| 389 gtk_widget_set_size_request(web_contents_widget, size.width(), size.height()); | |
| 390 } | |
| 391 | |
| 392 gfx::Size WebIntentPickerGtk::GetMaxInlineDispositionSize() { | |
| 393 gfx::Rect tab_bounds(web_contents_->GetRenderViewHost()-> | |
| 394 GetView()->GetNativeView()->allocation); | |
| 395 GtkRequisition req = {}; | |
| 396 if (service_hbox_) | |
| 397 gtk_widget_size_request(service_hbox_, &req); | |
| 398 | |
| 399 tab_bounds.Inset(2 * ui::kContentAreaBorder, | |
| 400 2 * ui::kContentAreaBorder + req.height); | |
| 401 return tab_bounds.size(); | |
| 402 } | |
| 403 | |
| 404 void WebIntentPickerGtk::OnPendingAsyncCompleted() { | |
| 405 // Requests to both the WebIntentService and the Chrome Web Store have | |
| 406 // completed. If there are any services, installed or suggested, there's | |
| 407 // nothing to do. | |
| 408 if (model_->GetInstalledServiceCount() || | |
| 409 model_->GetSuggestedExtensionCount()) | |
| 410 return; | |
| 411 | |
| 412 // If there are no installed or suggested services at this point, | |
| 413 // inform the user about it. | |
| 414 | |
| 415 // Replace the picker contents with dialog box. | |
| 416 ClearContents(); | |
| 417 GtkWidget* sub_contents = CreateSubContents(contents_); | |
| 418 | |
| 419 AddCloseButton(contents_); | |
| 420 AddTitle(sub_contents); | |
| 421 | |
| 422 // Replace the dialog header. | |
| 423 DCHECK(header_label_); | |
| 424 gtk_label_set_text( | |
| 425 GTK_LABEL(header_label_), | |
| 426 l10n_util::GetStringUTF8(IDS_INTENT_PICKER_NO_SERVICES_TITLE).c_str()); | |
| 427 | |
| 428 // Add the message text. | |
| 429 GtkWidget* hbox = gtk_hbox_new(FALSE, 0); | |
| 430 gtk_box_pack_start(GTK_BOX(sub_contents), hbox, TRUE, TRUE, 0); | |
| 431 GtkThemeService* theme_service = GetThemeService(web_contents_); | |
| 432 GtkWidget* no_service_label = theme_service->BuildLabel( | |
| 433 l10n_util::GetStringUTF8(IDS_INTENT_PICKER_NO_SERVICES).c_str(), | |
| 434 ui::kGdkBlack); | |
| 435 gtk_label_set_line_wrap(GTK_LABEL(no_service_label), TRUE); | |
| 436 gtk_misc_set_alignment(GTK_MISC(no_service_label), 0, 0); | |
| 437 // Set the label width to the size of |sub_contents|, which we don't have | |
| 438 // access to yet, by calculating the main content width minus borders. | |
| 439 gtk_util::SetLabelWidth(no_service_label, | |
| 440 kWindowMinWidth - 2 * ui::kContentAreaBorder); | |
| 441 gtk_box_pack_start(GTK_BOX(hbox), no_service_label, TRUE, TRUE, 0); | |
| 442 | |
| 443 gtk_widget_show_all(contents_); | |
| 444 } | |
| 445 | |
| 446 void WebIntentPickerGtk::InvalidateDelegate() { | |
| 447 delegate_ = NULL; | |
| 448 } | |
| 449 | |
| 450 GtkWidget* WebIntentPickerGtk::GetWidgetRoot() { | |
| 451 return contents_; | |
| 452 } | |
| 453 | |
| 454 GtkWidget* WebIntentPickerGtk::GetFocusWidget() { | |
| 455 return contents_; | |
| 456 } | |
| 457 | |
| 458 void WebIntentPickerGtk::DeleteDelegate() { | |
| 459 // The delegate is deleted when the contents widget is destroyed. See | |
| 460 // OnDestroy. | |
| 461 if (delegate_) | |
| 462 delegate_->OnClosing(); | |
| 463 } | |
| 464 | |
| 465 bool WebIntentPickerGtk::GetBackgroundColor(GdkColor* color) { | |
| 466 if (inline_disposition_web_contents_.get()) { | |
| 467 *color = ui::kGdkWhite; | |
| 468 return true; | |
| 469 } | |
| 470 | |
| 471 return false; | |
| 472 } | |
| 473 | |
| 474 bool WebIntentPickerGtk::ShouldHaveBorderPadding() const { | |
| 475 return false; | |
| 476 } | |
| 477 | |
| 478 void WebIntentPickerGtk::Observe(int type, | |
| 479 const content::NotificationSource& source, | |
| 480 const content::NotificationDetails& details) { | |
| 481 DCHECK_EQ(type, chrome::NOTIFICATION_BROWSER_THEME_CHANGED); | |
| 482 if (header_label_) { | |
| 483 GtkThemeService* theme_service = GetThemeService(web_contents_); | |
| 484 if (theme_service->UsingNativeTheme()) | |
| 485 gtk_util::UndoForceFontSize(header_label_); | |
| 486 else | |
| 487 gtk_util::ForceFontSizePixels(header_label_, kHeaderLabelPixelSize); | |
| 488 } | |
| 489 UpdateInstalledServices(); | |
| 490 UpdateSuggestedExtensions(); | |
| 491 } | |
| 492 | |
| 493 void WebIntentPickerGtk::OnDestroy(GtkWidget* button) { | |
| 494 // Destroy this object when the contents widget is destroyed. It can't be | |
| 495 // "delete this" because this function happens in a callback. | |
| 496 MessageLoop::current()->DeleteSoon(FROM_HERE, this); | |
| 497 model_->set_observer(NULL); | |
| 498 window_ = NULL; | |
| 499 } | |
| 500 | |
| 501 void WebIntentPickerGtk::OnCloseButtonClick(GtkWidget* button) { | |
| 502 DCHECK(delegate_); | |
| 503 delegate_->OnUserCancelledPickerDialog(); | |
| 504 } | |
| 505 | |
| 506 void WebIntentPickerGtk::OnExtensionLinkClick(GtkWidget* link) { | |
| 507 DCHECK(delegate_); | |
| 508 size_t index = GetExtensionWidgetRow(link); | |
| 509 const WebIntentPickerModel::SuggestedExtension& extension = | |
| 510 model_->GetSuggestedExtensionAt(index); | |
| 511 delegate_->OnExtensionLinkClicked(extension.id, | |
| 512 event_utils::DispositionForCurrentButtonPressEvent()); | |
| 513 } | |
| 514 | |
| 515 void WebIntentPickerGtk::OnExtensionInstallButtonClick(GtkWidget* button) { | |
| 516 DCHECK(delegate_); | |
| 517 size_t index = GetExtensionWidgetRow(button); | |
| 518 const WebIntentPickerModel::SuggestedExtension& extension = | |
| 519 model_->GetSuggestedExtensionAt(index); | |
| 520 | |
| 521 delegate_->OnExtensionInstallRequested(extension.id); | |
| 522 SetWidgetsEnabled(false); | |
| 523 | |
| 524 // Re-enable the clicked extension row. | |
| 525 GList* vbox_list = | |
| 526 gtk_container_get_children(GTK_CONTAINER(extensions_vbox_)); | |
| 527 GtkWidget* hbox = static_cast<GtkWidget*>(g_list_nth_data(vbox_list, index)); | |
| 528 gtk_widget_set_sensitive(hbox, TRUE); | |
| 529 | |
| 530 // Hide the install button. | |
| 531 GList* hbox_list = gtk_container_get_children(GTK_CONTAINER(hbox)); | |
| 532 GtkWidget* install_button = | |
| 533 static_cast<GtkWidget*>(g_list_nth_data(hbox_list, kInstallButtonIndex)); | |
| 534 GtkAllocation allocation; | |
| 535 gtk_widget_get_allocation(install_button, &allocation); | |
| 536 gtk_widget_hide(install_button); | |
| 537 g_list_free(hbox_list); | |
| 538 g_list_free(vbox_list); | |
| 539 | |
| 540 // Show the throbber with the same size as the install button. | |
| 541 GtkWidget* throbber = AddThrobberToExtensionAt(index); | |
| 542 gtk_widget_set_size_request(throbber, allocation.width, allocation.height); | |
| 543 gtk_widget_show_all(throbber); | |
| 544 } | |
| 545 | |
| 546 void WebIntentPickerGtk::OnMoreSuggestionsLinkClick(GtkWidget* link) { | |
| 547 DCHECK(delegate_); | |
| 548 delegate_->OnSuggestionsLinkClicked( | |
| 549 event_utils::DispositionForCurrentButtonPressEvent()); | |
| 550 } | |
| 551 | |
| 552 void WebIntentPickerGtk::OnChooseAnotherServiceClick(GtkWidget* link) { | |
| 553 DCHECK(delegate_); | |
| 554 delegate_->OnChooseAnotherService(); | |
| 555 ResetContents(); | |
| 556 } | |
| 557 | |
| 558 void WebIntentPickerGtk::OnHostContentsSizeAllocate(GtkWidget* widget, | |
| 559 GdkRectangle* rectangle) { | |
| 560 if (!inline_disposition_delegate_.get()) | |
| 561 return; | |
| 562 inline_disposition_delegate_->SetRenderViewSizeLimits(); | |
| 563 } | |
| 564 | |
| 565 void WebIntentPickerGtk::OnServiceButtonClick(GtkWidget* button) { | |
| 566 DCHECK(delegate_); | |
| 567 GList* button_list = gtk_container_get_children(GTK_CONTAINER(button_vbox_)); | |
| 568 gint index = g_list_index(button_list, button); | |
| 569 DCHECK(index != -1); | |
| 570 g_list_free(button_list); | |
| 571 | |
| 572 const WebIntentPickerModel::InstalledService& installed_service = | |
| 573 model_->GetInstalledServiceAt(index); | |
| 574 | |
| 575 delegate_->OnServiceChosen(installed_service.url, | |
| 576 installed_service.disposition, | |
| 577 WebIntentPickerDelegate::kEnableDefaults); | |
| 578 } | |
| 579 | |
| 580 void WebIntentPickerGtk::InitContents() { | |
| 581 GtkThemeService* theme_service = GetThemeService(web_contents_); | |
| 582 | |
| 583 // Main contents vbox. | |
| 584 if (!contents_) { | |
| 585 contents_ = gtk_vbox_new(FALSE, 0); | |
| 586 g_signal_connect(contents_, "destroy", G_CALLBACK(&OnDestroyThunk), this); | |
| 587 } | |
| 588 | |
| 589 gtk_widget_set_size_request(contents_, kWindowMinWidth, -1); | |
| 590 | |
| 591 if (model_ && model_->IsWaitingForSuggestions()) { | |
| 592 ClearContents(); | |
| 593 AddCloseButton(contents_); | |
| 594 waiting_dialog_.reset(new WaitingDialog(theme_service)); | |
| 595 gtk_box_pack_start(GTK_BOX(contents_), waiting_dialog_->widget(), | |
| 596 TRUE, TRUE, 0); | |
| 597 } else { | |
| 598 InitMainContents(); | |
| 599 } | |
| 600 } | |
| 601 | |
| 602 void WebIntentPickerGtk::InitMainContents() { | |
| 603 GtkThemeService* theme_service = GetThemeService(web_contents_); | |
| 604 | |
| 605 ClearContents(); | |
| 606 | |
| 607 AddCloseButton(contents_); | |
| 608 GtkWidget* sub_contents = CreateSubContents(contents_); | |
| 609 | |
| 610 AddTitle(sub_contents); | |
| 611 | |
| 612 // Add separation between the installed services list and the app suggestions. | |
| 613 GtkWidget* button_alignment = gtk_alignment_new(0.5, 0, 0, 0); | |
| 614 gtk_alignment_set_padding(GTK_ALIGNMENT(button_alignment), 0, | |
| 615 kMainContentPixelSize * 2, 0, 0); | |
| 616 | |
| 617 // Vbox containing all service buttons. | |
| 618 button_vbox_ = gtk_vbox_new(FALSE, ui::kControlSpacing); | |
| 619 gtk_container_add(GTK_CONTAINER(button_alignment), button_vbox_); | |
| 620 gtk_box_pack_start(GTK_BOX(sub_contents), button_alignment, TRUE, TRUE, 0); | |
| 621 | |
| 622 // Chrome Web Store label. | |
| 623 cws_label_ = theme_service->BuildLabel( | |
| 624 l10n_util::GetStringUTF8(IDS_INTENT_PICKER_GET_MORE_SERVICES).c_str(), | |
| 625 ui::kGdkBlack); | |
| 626 gtk_box_pack_start(GTK_BOX(sub_contents), cws_label_, TRUE, TRUE, 0); | |
| 627 gtk_misc_set_alignment(GTK_MISC(cws_label_), 0, 0); | |
| 628 gtk_widget_set_no_show_all(cws_label_, TRUE); | |
| 629 | |
| 630 // Set the label width to the size of |sub_contents|, which we don't have | |
| 631 // access to yet, by calculating the main content width minus borders. | |
| 632 gtk_util::SetLabelWidth(cws_label_, | |
| 633 kWindowMinWidth - 2 * ui::kContentAreaBorder); | |
| 634 gtk_util::ForceFontSizePixels(cws_label_, kMainContentPixelSize); | |
| 635 | |
| 636 // Suggested extensions vbox. | |
| 637 extensions_vbox_ = gtk_vbox_new(FALSE, ui::kControlSpacing); | |
| 638 GtkWidget* indent_extensions = gtk_alignment_new(0.0, 0.5, 1.0, 1.0); | |
| 639 gtk_alignment_set_padding(GTK_ALIGNMENT(indent_extensions), 0, 0, | |
| 640 ui::kGroupIndent, ui::kGroupIndent); | |
| 641 gtk_container_add(GTK_CONTAINER(indent_extensions), extensions_vbox_); | |
| 642 gtk_widget_set_no_show_all(indent_extensions, TRUE); | |
| 643 gtk_box_pack_start(GTK_BOX(sub_contents), indent_extensions, TRUE, TRUE, 0); | |
| 644 | |
| 645 // CWS 'More Suggestions' link. | |
| 646 GtkWidget* link_alignment = gtk_alignment_new(0, 0.5f, 0, 0); | |
| 647 GtkWidget* more_hbox = gtk_hbox_new(FALSE, ui::kControlSpacing); | |
| 648 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
| 649 GdkPixbuf* cws_icon = | |
| 650 rb.GetNativeImageNamed(IDR_WEBSTORE_ICON_16).ToGdkPixbuf(); | |
| 651 gtk_box_pack_start(GTK_BOX(more_hbox), gtk_image_new_from_pixbuf(cws_icon), | |
| 652 FALSE, FALSE, 0); | |
| 653 GtkWidget* more_suggestions_link = theme_service->BuildChromeLinkButton( | |
| 654 l10n_util::GetStringUTF8(IDS_FIND_MORE_INTENT_HANDLER_MESSAGE).c_str()); | |
| 655 gtk_box_pack_start(GTK_BOX(more_hbox), more_suggestions_link, | |
| 656 FALSE, FALSE, 0); | |
| 657 gtk_container_add(GTK_CONTAINER(link_alignment), more_hbox); | |
| 658 gtk_chrome_link_button_set_use_gtk_theme( | |
| 659 GTK_CHROME_LINK_BUTTON(more_suggestions_link), | |
| 660 theme_service->UsingNativeTheme()); | |
| 661 gtk_util::ForceFontSizePixels( | |
| 662 GTK_CHROME_LINK_BUTTON(more_suggestions_link)->label, | |
| 663 kMainContentPixelSize); | |
| 664 g_signal_connect(more_suggestions_link, "clicked", | |
| 665 G_CALLBACK(OnMoreSuggestionsLinkClickThunk), this); | |
| 666 | |
| 667 GtkWidget* indent_link = gtk_util::IndentWidget(link_alignment); | |
| 668 gtk_box_pack_start(GTK_BOX(sub_contents), indent_link, TRUE, TRUE, 0); | |
| 669 | |
| 670 // Throbber, which will be added to the hierarchy when necessary. | |
| 671 throbber_.reset(new ThrobberGtk(theme_service)); | |
| 672 | |
| 673 gtk_widget_show_all(contents_); | |
| 674 } | |
| 675 | |
| 676 void WebIntentPickerGtk::ClearContents() { | |
| 677 // Wipe out all currently displayed widgets. | |
| 678 gtk_util::RemoveAllChildren(contents_); | |
| 679 header_label_ = NULL; | |
| 680 button_vbox_ = NULL; | |
| 681 cws_label_ = NULL; | |
| 682 extensions_vbox_ = NULL; | |
| 683 } | |
| 684 | |
| 685 void WebIntentPickerGtk::ResetContents() { | |
| 686 ClearContents(); | |
| 687 | |
| 688 // Reset potential inline disposition data. | |
| 689 inline_disposition_web_contents_.reset(); | |
| 690 inline_disposition_delegate_.reset(); | |
| 691 window_->BackgroundColorChanged(); | |
| 692 | |
| 693 // Re-initialize picker widgets and data. | |
| 694 InitMainContents(); | |
| 695 UpdateInstalledServices(); | |
| 696 UpdateCWSLabel(); | |
| 697 UpdateSuggestedExtensions(); | |
| 698 SetActionString(header_label_text_); | |
| 699 | |
| 700 gtk_widget_show_all(contents_); | |
| 701 } | |
| 702 | |
| 703 GtkWidget* WebIntentPickerGtk::CreateSubContents(GtkWidget* box) { | |
| 704 GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
| 705 gtk_alignment_set_padding( | |
| 706 GTK_ALIGNMENT(alignment), 0, ui::kContentAreaBorder, | |
| 707 ui::kContentAreaBorder, ui::kContentAreaBorder); | |
| 708 gtk_box_pack_end(GTK_BOX(box), alignment, TRUE, TRUE, 0); | |
| 709 | |
| 710 GtkWidget* sub_contents = gtk_vbox_new(FALSE, ui::kContentAreaSpacing); | |
| 711 gtk_container_add(GTK_CONTAINER(alignment), sub_contents); | |
| 712 return sub_contents; | |
| 713 } | |
| 714 | |
| 715 void WebIntentPickerGtk::AddCloseButton(GtkWidget* containingBox) { | |
| 716 | |
| 717 // Hbox containing the close button. | |
| 718 GtkWidget* close_hbox = gtk_hbox_new(FALSE, 0); | |
| 719 gtk_box_pack_start(GTK_BOX(containingBox), close_hbox, TRUE, TRUE, 0); | |
| 720 | |
| 721 close_button_.reset( | |
| 722 CustomDrawButton::CloseButton(GetThemeService(web_contents_))); | |
| 723 g_signal_connect(close_button_->widget(), "clicked", | |
| 724 G_CALLBACK(OnCloseButtonClickThunk), this); | |
| 725 gtk_widget_set_can_focus(close_button_->widget(), FALSE); | |
| 726 gtk_box_pack_end(GTK_BOX(close_hbox), close_button_->widget(), | |
| 727 FALSE, FALSE, 0); | |
| 728 } | |
| 729 | |
| 730 void WebIntentPickerGtk::AddTitle(GtkWidget* containingBox) { | |
| 731 // Hbox containing the header label. | |
| 732 GtkWidget* header_hbox = gtk_hbox_new(FALSE, 0); | |
| 733 gtk_box_pack_start(GTK_BOX(containingBox), header_hbox, TRUE, TRUE, 0); | |
| 734 | |
| 735 // Label text will be set in the call to SetActionString(). | |
| 736 header_label_ = GetThemeService(web_contents_)->BuildLabel( | |
| 737 std::string(), ui::kGdkBlack); | |
| 738 gtk_util::ForceFontSizePixels(header_label_, kHeaderLabelPixelSize); | |
| 739 gtk_box_pack_start(GTK_BOX(header_hbox), header_label_, TRUE, TRUE, 0); | |
| 740 gtk_misc_set_alignment(GTK_MISC(header_label_), 0, 0); | |
| 741 } | |
| 742 | |
| 743 void WebIntentPickerGtk::UpdateInstalledServices() { | |
| 744 if (!button_vbox_) | |
| 745 return; | |
| 746 | |
| 747 gtk_util::RemoveAllChildren(button_vbox_); | |
| 748 | |
| 749 if (model_->GetInstalledServiceCount() == 0) { | |
| 750 gtk_widget_hide(gtk_widget_get_parent(button_vbox_)); | |
| 751 return; | |
| 752 } | |
| 753 | |
| 754 for (size_t i = 0; i < model_->GetInstalledServiceCount(); ++i) { | |
| 755 const WebIntentPickerModel::InstalledService& installed_service = | |
| 756 model_->GetInstalledServiceAt(i); | |
| 757 | |
| 758 GtkWidget* button = gtk_button_new(); | |
| 759 gtk_widget_set_tooltip_text(button, installed_service.url.spec().c_str()); | |
| 760 gtk_button_set_label(GTK_BUTTON(button), | |
| 761 UTF16ToUTF8(installed_service.title).c_str()); | |
| 762 gtk_button_set_alignment(GTK_BUTTON(button), 0, 0); | |
| 763 | |
| 764 gtk_container_add(GTK_CONTAINER(button_vbox_), button); | |
| 765 g_signal_connect(button, "clicked", G_CALLBACK(OnServiceButtonClickThunk), | |
| 766 this); | |
| 767 | |
| 768 SetServiceButtonImage(button, installed_service.favicon.ToGdkPixbuf()); | |
| 769 | |
| 770 // Must be called after SetServiceButtonImage as the internal label widget | |
| 771 // is replaced in that call. | |
| 772 SetWidgetFontSize(button, kMainContentPixelSize); | |
| 773 } | |
| 774 | |
| 775 gtk_widget_show_all(button_vbox_); | |
| 776 gtk_widget_show(gtk_widget_get_parent(button_vbox_)); | |
| 777 } | |
| 778 | |
| 779 void WebIntentPickerGtk::UpdateCWSLabel() { | |
| 780 if (!button_vbox_) | |
| 781 return; | |
| 782 | |
| 783 gtk_widget_set_visible(gtk_widget_get_parent(button_vbox_), | |
| 784 model_->GetInstalledServiceCount() != 0); | |
| 785 | |
| 786 std::string label_text = UTF16ToUTF8(model_->GetSuggestionsLinkText()); | |
| 787 gtk_label_set_text(GTK_LABEL(cws_label_), label_text.c_str()); | |
| 788 gtk_widget_set_visible(cws_label_, !label_text.empty()); | |
| 789 } | |
| 790 | |
| 791 void WebIntentPickerGtk::UpdateSuggestedExtensions() { | |
| 792 if (!extensions_vbox_) | |
| 793 return; | |
| 794 | |
| 795 GtkThemeService* theme_service = GetThemeService(web_contents_); | |
| 796 | |
| 797 gtk_util::RemoveAllChildren(extensions_vbox_); | |
| 798 | |
| 799 if (model_->GetSuggestedExtensionCount() == 0) { | |
| 800 gtk_widget_hide(gtk_widget_get_parent(extensions_vbox_)); | |
| 801 return; | |
| 802 } | |
| 803 | |
| 804 gtk_widget_show(gtk_widget_get_parent(extensions_vbox_)); | |
| 805 | |
| 806 for (size_t i = 0; i < model_->GetSuggestedExtensionCount(); ++i) { | |
| 807 const WebIntentPickerModel::SuggestedExtension& extension = | |
| 808 model_->GetSuggestedExtensionAt(i); | |
| 809 | |
| 810 GtkWidget* hbox = gtk_hbox_new(FALSE, ui::kControlSpacing); | |
| 811 gtk_box_pack_start(GTK_BOX(extensions_vbox_), hbox, FALSE, FALSE, 0); | |
| 812 | |
| 813 // Icon. | |
| 814 GtkWidget* icon = gtk_image_new_from_pixbuf(extension.icon.ToGdkPixbuf()); | |
| 815 gtk_box_pack_start(GTK_BOX(hbox), icon, FALSE, FALSE, 0); | |
| 816 | |
| 817 // Title link. | |
| 818 string16 elided_title = ui::ElideText(extension.title, gfx::Font(), | |
| 819 kTitleLinkMaxWidth, ui::ELIDE_AT_END); | |
| 820 GtkWidget* title_link = theme_service->BuildChromeLinkButton( | |
| 821 UTF16ToUTF8(elided_title).c_str()); | |
| 822 gtk_chrome_link_button_set_use_gtk_theme(GTK_CHROME_LINK_BUTTON(title_link), | |
| 823 theme_service->UsingNativeTheme()); | |
| 824 gtk_util::ForceFontSizePixels(GTK_CHROME_LINK_BUTTON(title_link)->label, | |
| 825 kMainContentPixelSize); | |
| 826 g_signal_connect(title_link, "clicked", | |
| 827 G_CALLBACK(OnExtensionLinkClickThunk), this); | |
| 828 gtk_box_pack_start(GTK_BOX(hbox), title_link, FALSE, FALSE, 0); | |
| 829 | |
| 830 // Stars. | |
| 831 GtkWidget* stars = CreateStarsWidget(extension.average_rating); | |
| 832 gtk_box_pack_start(GTK_BOX(hbox), stars, FALSE, FALSE, 0); | |
| 833 | |
| 834 // Install button. | |
| 835 GtkWidget* install_button = gtk_button_new(); | |
| 836 gtk_button_set_label( | |
| 837 GTK_BUTTON(install_button), | |
| 838 l10n_util::GetStringUTF8(IDS_INTENT_PICKER_INSTALL_EXTENSION).c_str()); | |
| 839 GtkWidget* label = gtk_bin_get_child(GTK_BIN(install_button)); | |
| 840 gtk_util::ForceFontSizePixels(label, kMainContentPixelSize); | |
| 841 g_signal_connect(install_button, "clicked", | |
| 842 G_CALLBACK(OnExtensionInstallButtonClickThunk), this); | |
| 843 gtk_box_pack_end(GTK_BOX(hbox), install_button, FALSE, FALSE, 0); | |
| 844 } | |
| 845 | |
| 846 gtk_widget_show_all(extensions_vbox_); | |
| 847 gtk_widget_show(gtk_widget_get_parent(extensions_vbox_)); | |
| 848 } | |
| 849 | |
| 850 void WebIntentPickerGtk::SetWidgetsEnabled(bool enabled) { | |
| 851 gboolean data = enabled; | |
| 852 if (button_vbox_) | |
| 853 gtk_container_foreach(GTK_CONTAINER(button_vbox_), EnableWidgetCallback, | |
| 854 &data); | |
| 855 if (extensions_vbox_) | |
| 856 gtk_container_foreach(GTK_CONTAINER(extensions_vbox_), EnableWidgetCallback, | |
| 857 &data); | |
| 858 } | |
| 859 | |
| 860 GtkWidget* WebIntentPickerGtk::AddThrobberToExtensionAt(size_t index) { | |
| 861 // The throbber should be unparented. | |
| 862 DCHECK(!gtk_widget_get_parent(throbber_->widget())); | |
| 863 GList* vbox_list = | |
| 864 gtk_container_get_children(GTK_CONTAINER(extensions_vbox_)); | |
| 865 GtkWidget* hbox = static_cast<GtkWidget*>(g_list_nth_data(vbox_list, index)); | |
| 866 GtkWidget* alignment = gtk_alignment_new(0.5, 0.5, 0, 0); | |
| 867 gtk_container_add(GTK_CONTAINER(alignment), throbber_->widget()); | |
| 868 gtk_box_pack_end(GTK_BOX(hbox), alignment, FALSE, FALSE, 0); | |
| 869 g_list_free(vbox_list); | |
| 870 throbber_->Start(); | |
| 871 return alignment; | |
| 872 } | |
| 873 | |
| 874 void WebIntentPickerGtk::RemoveThrobber() { | |
| 875 GtkWidget* alignment = gtk_widget_get_parent(throbber_->widget()); | |
| 876 DCHECK(alignment); | |
| 877 gtk_container_remove(GTK_CONTAINER(alignment), throbber_->widget()); | |
| 878 gtk_widget_destroy(alignment); | |
| 879 throbber_->Stop(); | |
| 880 } | |
| OLD | NEW |