1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU Lesser General Public License 4 * (version 2.1 or later). See the COPYING file in this distribution. 5 */ 6 7namespace PublishingUI { 8 9public class ConcreteDialogPane : Spit.Publishing.DialogPane, GLib.Object { 10 protected Gtk.Box pane_widget = null; 11 protected Gtk.Builder builder = null; 12 13 public ConcreteDialogPane() { 14 builder = AppWindow.create_builder(); 15 } 16 17 public Gtk.Widget get_widget() { 18 return pane_widget; 19 } 20 21 public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { 22 return Spit.Publishing.DialogPane.GeometryOptions.NONE; 23 } 24 25 public void on_pane_installed() { 26 } 27 28 public void on_pane_uninstalled() { 29 } 30} 31 32public class StaticMessagePane : ConcreteDialogPane { 33 private Gtk.Label msg_label = null; 34 35 public StaticMessagePane(string message_string, bool enable_markup = false) { 36 base(); 37 msg_label = builder.get_object("static_msg_label") as Gtk.Label; 38 pane_widget = builder.get_object("static_msg_pane_widget") as Gtk.Box; 39 40 if (enable_markup) { 41 msg_label.set_markup(message_string); 42 msg_label.set_line_wrap(true); 43 msg_label.set_use_markup(true); 44 } else { 45 msg_label.set_label(message_string); 46 } 47 } 48} 49 50public class LoginWelcomePane : ConcreteDialogPane { 51 private Gtk.Button login_button = null; 52 private Gtk.Label not_logged_in_label = null; 53 54 public signal void login_requested(); 55 56 public LoginWelcomePane(string service_welcome_message) { 57 base(); 58 pane_widget = builder.get_object("welcome_pane_widget") as Gtk.Box; 59 login_button = builder.get_object("login_button") as Gtk.Button; 60 not_logged_in_label = builder.get_object("not_logged_in_label") as Gtk.Label; 61 62 login_button.clicked.connect(on_login_clicked); 63 not_logged_in_label.set_use_markup(true); 64 not_logged_in_label.set_markup(service_welcome_message); 65 } 66 67 private void on_login_clicked() { 68 login_requested(); 69 } 70} 71 72public class ProgressPane : ConcreteDialogPane { 73 private Gtk.ProgressBar progress_bar = null; 74 75 public ProgressPane() { 76 base(); 77 pane_widget = (Gtk.Box) builder.get_object("progress_pane_widget"); 78 progress_bar = (Gtk.ProgressBar) builder.get_object("publishing_progress_bar"); 79 } 80 81 public void set_text(string text) { 82 progress_bar.set_text(text); 83 } 84 85 public void set_progress(double progress) { 86 progress_bar.set_fraction(progress); 87 } 88 89 public void set_status(string status_text, double progress) { 90 if (status_text != progress_bar.get_text()) 91 progress_bar.set_text(status_text); 92 93 set_progress(progress); 94 } 95} 96 97public class SuccessPane : StaticMessagePane { 98 public SuccessPane(Spit.Publishing.Publisher.MediaType published_media, int num_uploaded = 1) { 99 string? message_string = null; 100 101 // Here, we check whether more than one item is being uploaded, and if so, display 102 // an alternate message. 103 if (published_media == Spit.Publishing.Publisher.MediaType.VIDEO) { 104 message_string = ngettext ("The selected video was successfully published.", 105 "The selected videos were successfully published.", 106 num_uploaded); 107 } 108 else if (published_media == Spit.Publishing.Publisher.MediaType.PHOTO) { 109 message_string = ngettext ("The selected photo was successfully published.", 110 "The selected photos were successfully published.", 111 num_uploaded); 112 } 113 else if (published_media == (Spit.Publishing.Publisher.MediaType.PHOTO 114 | Spit.Publishing.Publisher.MediaType.VIDEO)) { 115 message_string = _("The selected photos/videos were successfully published."); 116 } 117 else { 118 assert_not_reached (); 119 } 120 121 base(message_string); 122 } 123} 124 125public class AccountFetchWaitPane : StaticMessagePane { 126 public AccountFetchWaitPane() { 127 base(_("Fetching account information…")); 128 } 129} 130 131public class LoginWaitPane : StaticMessagePane { 132 public LoginWaitPane() { 133 base(_("Logging in…")); 134 } 135} 136 137public class PublishingDialog : Gtk.Dialog { 138 private const int LARGE_WINDOW_WIDTH = 860; 139 private const int LARGE_WINDOW_HEIGHT = 688; 140 private const int COLOSSAL_WINDOW_WIDTH = 1024; 141 private const int COLOSSAL_WINDOW_HEIGHT = 688; 142 private const int STANDARD_WINDOW_WIDTH = 632; 143 private const int STANDARD_WINDOW_HEIGHT = 540; 144 private const int BORDER_REGION_WIDTH = 16; 145 private const int BORDER_REGION_HEIGHT = 100; 146 147 public const int STANDARD_CONTENT_LABEL_WIDTH = 500; 148 public const int STANDARD_ACTION_BUTTON_WIDTH = 128; 149 150 private static PublishingDialog active_instance = null; 151 152 private Gtk.ListStore service_selector_box_model; 153 private Gtk.ComboBox service_selector_box; 154 private Gtk.Box central_area_layouter; 155 private Gtk.Button close_cancel_button; 156 private Spit.Publishing.DialogPane active_pane; 157 private Spit.Publishing.Publishable[] publishables; 158 private Spit.Publishing.ConcretePublishingHost host; 159 private Spit.PluggableInfo info; 160 161 protected PublishingDialog(Gee.Collection<MediaSource> to_publish) { 162 assert(to_publish.size > 0); 163 164 bool use_header = Resources.use_header_bar() == 1; 165 Object(use_header_bar: Resources.use_header_bar()); 166 if (use_header) { 167 ((Gtk.HeaderBar) get_header_bar()).set_show_close_button(false); 168 } else { 169 get_content_area().set_spacing(6); 170 } 171 172 resizable = false; 173 modal = true; 174 set_transient_for(AppWindow.get_instance()); 175 delete_event.connect(on_window_close); 176 177 publishables = new Spit.Publishing.Publishable[0]; 178 bool has_photos = false; 179 bool has_videos = false; 180 foreach (MediaSource media in to_publish) { 181 Spit.Publishing.Publishable publishable = 182 new Publishing.Glue.MediaSourcePublishableWrapper(media); 183 if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.PHOTO) 184 has_photos = true; 185 else if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) 186 has_videos = true; 187 else 188 assert_not_reached(); 189 190 publishables += publishable; 191 } 192 193 string title = null; 194 string label = null; 195 196 if (has_photos && !has_videos) { 197 title = _("Publish Photos"); 198 label = _("Publish photos _to:"); 199 } else if (!has_photos && has_videos) { 200 title = _("Publish Videos"); 201 label = _("Publish videos _to"); 202 } else { 203 title = _("Publish Photos and Videos"); 204 label = _("Publish photos and videos _to"); 205 } 206 set_title(title); 207 208 service_selector_box_model = new Gtk.ListStore(2, typeof(Gdk.Pixbuf), typeof(string)); 209 service_selector_box = new Gtk.ComboBox.with_model(service_selector_box_model); 210 211 Gtk.CellRendererPixbuf renderer_pix = new Gtk.CellRendererPixbuf(); 212 service_selector_box.pack_start(renderer_pix,true); 213 service_selector_box.add_attribute(renderer_pix, "pixbuf", 0); 214 215 Gtk.CellRendererText renderer_text = new Gtk.CellRendererText(); 216 service_selector_box.pack_start(renderer_text,true); 217 service_selector_box.add_attribute(renderer_text, "text", 1); 218 219 service_selector_box.set_active(0); 220 221 // get the name of the service the user last used 222 string? last_used_service = Config.Facade.get_instance().get_last_used_service(); 223 224 Spit.Publishing.Service[] loaded_services = load_services(has_photos, has_videos); 225 226 Gtk.TreeIter iter; 227 228 foreach (Spit.Publishing.Service service in loaded_services) { 229 service_selector_box_model.append(out iter); 230 231 string curr_service_id = service.get_id(); 232 233 service.get_info(ref info); 234 235 if (null != info.icons && 0 < info.icons.length) { 236 // check if the icons object is set -- if set use that icon 237 service_selector_box_model.set(iter, 0, info.icons[0], 1, 238 service.get_pluggable_name()); 239 240 // in case the icons object is not set on the next iteration 241 info.icons[0] = Resources.get_icon(Resources.ICON_GENERIC_PLUGIN); 242 } else { 243 // if icons object is null or zero length use a generic icon 244 service_selector_box_model.set(iter, 0, Resources.get_icon( 245 Resources.ICON_GENERIC_PLUGIN), 1, service.get_pluggable_name()); 246 } 247 248 if (last_used_service == null) { 249 service_selector_box.set_active_iter(iter); 250 last_used_service = service.get_id(); 251 } else if (last_used_service == curr_service_id) { 252 service_selector_box.set_active_iter(iter); 253 } 254 } 255 256 service_selector_box.changed.connect(on_service_changed); 257 258 if (!use_header) 259 { 260 var service_selector_box_label = new Gtk.Label.with_mnemonic(label); 261 service_selector_box_label.set_mnemonic_widget(service_selector_box); 262 service_selector_box_label.halign = Gtk.Align.START; 263 service_selector_box_label.valign = Gtk.Align.CENTER; 264 265 /* the wrapper is not an extraneous widget -- it's necessary to prevent the service 266 selection box from growing and shrinking whenever its parent's size changes. 267 When wrapped inside a Gtk.Alignment, the Alignment grows and shrinks instead of 268 the service selection box. */ 269 service_selector_box.halign = Gtk.Align.END; 270 service_selector_box.valign = Gtk.Align.CENTER; 271 service_selector_box.hexpand = false; 272 service_selector_box.vexpand = false; 273 274 Gtk.Box service_selector_layouter = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8); 275 service_selector_layouter.set_border_width(12); 276 service_selector_layouter.hexpand = true; 277 service_selector_layouter.add(service_selector_box_label); 278 service_selector_layouter.pack_start(service_selector_box, true, true, 0); 279 280 /* 'service area' is the selector assembly plus the horizontal rule dividing it from the 281 rest of the dialog */ 282 Gtk.Box service_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); 283 service_area_layouter.add(service_selector_layouter); 284 service_area_layouter.add(new Gtk.Separator(Gtk.Orientation.HORIZONTAL)); 285 service_area_layouter.halign = Gtk.Align.FILL; 286 service_area_layouter.valign = Gtk.Align.START; 287 service_area_layouter.hexpand = true; 288 service_area_layouter.vexpand = false; 289 290 get_content_area().pack_start(service_area_layouter, false, false, 0); 291 } 292 293 central_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); 294 295 get_content_area().pack_start(central_area_layouter, true, true, 0); 296 297 if (use_header) { 298 close_cancel_button = new Gtk.Button.with_mnemonic("_Cancel"); 299 close_cancel_button.set_can_default(true); 300 301 ((Gtk.HeaderBar) get_header_bar()).pack_start(close_cancel_button); 302 ((Gtk.HeaderBar) get_header_bar()).pack_end(service_selector_box); 303 } 304 else { 305 add_button (_("_Cancel"), Gtk.ResponseType.CANCEL); 306 close_cancel_button = get_widget_for_response (Gtk.ResponseType.CANCEL) as Gtk.Button; 307 } 308 close_cancel_button.clicked.connect(on_close_cancel_clicked); 309 310 set_standard_window_mode(); 311 312 show_all(); 313 } 314 315 private static Spit.Publishing.Service[] load_all_services() { 316 Spit.Publishing.Service[] loaded_services = new Spit.Publishing.Service[0]; 317 318 // load publishing services from plug-ins 319 Gee.Collection<Spit.Pluggable> pluggables = Plugins.get_pluggables_for_type( 320 typeof(Spit.Publishing.Service)); 321 322 debug("PublisingDialog: discovered %d pluggable publishing services.", pluggables.size); 323 324 foreach (Spit.Pluggable pluggable in pluggables) { 325 int pluggable_interface = pluggable.get_pluggable_interface( 326 Spit.Publishing.CURRENT_INTERFACE, Spit.Publishing.CURRENT_INTERFACE); 327 if (pluggable_interface != Spit.Publishing.CURRENT_INTERFACE) { 328 warning("Unable to load publisher %s: reported interface %d.", 329 Plugins.get_pluggable_module_id(pluggable), pluggable_interface); 330 331 continue; 332 } 333 334 Spit.Publishing.Service service = 335 (Spit.Publishing.Service) pluggable; 336 337 debug("PublishingDialog: discovered pluggable publishing service '%s'.", 338 service.get_pluggable_name()); 339 340 loaded_services += service; 341 } 342 343 // Sort publishing services by name. 344 Posix.qsort(loaded_services, loaded_services.length, sizeof(Spit.Publishing.Service), 345 (a, b) => {return utf8_cs_compare((*((Spit.Publishing.Service**) a))->get_pluggable_name(), 346 (*((Spit.Publishing.Service**) b))->get_pluggable_name()); 347 }); 348 349 return loaded_services; 350 } 351 352 private static Spit.Publishing.Service[] load_services(bool has_photos, bool has_videos) { 353 assert (has_photos || has_videos); 354 355 Spit.Publishing.Service[] filtered_services = new Spit.Publishing.Service[0]; 356 Spit.Publishing.Service[] all_services = load_all_services(); 357 358 foreach (Spit.Publishing.Service service in all_services) { 359 360 if (has_photos && !has_videos) { 361 if ((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.PHOTO) != 0) 362 filtered_services += service; 363 } else if (!has_photos && has_videos) { 364 if ((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.VIDEO) != 0) 365 filtered_services += service; 366 } else { 367 if (((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.PHOTO) != 0) && 368 ((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.VIDEO) != 0)) 369 filtered_services += service; 370 } 371 } 372 373 return filtered_services; 374 } 375 376 // Because of this bug: http://trac.yorba.org/ticket/3623, we use some extreme measures. The 377 // bug occurs because, in some cases, when publishing is started asynchronous network 378 // transactions are performed. The mechanism inside libsoup that we use to perform asynchronous 379 // network transactions isn't based on threads but is instead based on the GLib event loop. So 380 // whenever we run a network transaction, the GLib event loop gets spun. One consequence of 381 // this is that PublishingDialog.go( ) can be called multiple times. Note that since events 382 // are processed sequentially, PublishingDialog.go( ) is never called re-entrantly. It just 383 // gets called twice back-to-back in quick succession. So use a timer to do a short circuit 384 // return if this call to go( ) follows immediately on the heels of another call to go( ). 385 private static Timer since_last_start = null; 386 private static bool elapsed_is_valid = false; 387 public static void go(Gee.Collection<MediaSource> to_publish) { 388 if (active_instance != null) 389 return; 390 391 if (since_last_start == null) { 392 // GLib.Timers start themselves automatically when they're created, so stop our 393 // new timer and reset it to zero 'til were ready to start timing. 394 since_last_start = new Timer(); 395 since_last_start.stop(); 396 since_last_start.reset(); 397 elapsed_is_valid = false; 398 } else { 399 double elapsed = since_last_start.elapsed(); 400 if ((elapsed < 0.05) && (elapsed_is_valid)) 401 return; 402 } 403 404 Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>(); 405 Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>(); 406 MediaSourceCollection.filter_media(to_publish, photos, videos); 407 408 Spit.Publishing.Service[] avail_services = 409 load_services((photos.size > 0), (videos.size > 0)); 410 411 if (avail_services.length == 0) { 412 // There are no enabled publishing services that accept this media type, 413 // warn the user. 414 AppWindow.error_message_with_title(_("Unable to publish"), 415 _("Shotwell cannot publish the selected items because you do not have a compatible publishing plugin enabled. To correct this, choose <b>Edit %s Preferences</b> and enable one or more of the publishing plugins on the <b>Plugins</b> tab.").printf("▸"), 416 null, false); 417 418 return; 419 } 420 421 // If we get down here, it means that at least one publishing service 422 // was found that could accept this type of media, so continue normally. 423 424 debug("PublishingDialog.go( )"); 425 426 active_instance = new PublishingDialog(to_publish); 427 428 active_instance.run(); 429 430 active_instance = null; 431 432 // start timing just before we return 433 since_last_start.start(); 434 elapsed_is_valid = true; 435 } 436 437 private bool on_window_close(Gdk.EventAny evt) { 438 host.stop_publishing(); 439 host = null; 440 hide(); 441 destroy(); 442 443 return true; 444 } 445 446 private void on_service_changed() { 447 Gtk.TreeIter iter; 448 bool have_active_iter = false; 449 have_active_iter = service_selector_box.get_active_iter(out iter); 450 451 // this occurs when the user removes the last active publisher 452 if (!have_active_iter) { 453 // default to the first in the list (as good as any) 454 service_selector_box.set_active(0); 455 456 // and get active again 457 service_selector_box.get_active_iter(out iter); 458 } 459 460 Value service_name_val; 461 service_selector_box_model.get_value(iter, 1, out service_name_val); 462 463 string service_name = (string) service_name_val; 464 465 Spit.Publishing.Service? selected_service = null; 466 Spit.Publishing.Service[] services = load_all_services(); 467 foreach (Spit.Publishing.Service service in services) { 468 if (service.get_pluggable_name() == service_name) { 469 selected_service = service; 470 break; 471 } 472 } 473 assert(selected_service != null); 474 475 Config.Facade.get_instance().set_last_used_service(selected_service.get_id()); 476 477 host = new Spit.Publishing.ConcretePublishingHost(selected_service, this, publishables); 478 host.start_publishing(); 479 } 480 481 private void on_close_cancel_clicked() { 482 debug("PublishingDialog: on_close_cancel_clicked( ): invoked."); 483 484 host.stop_publishing(); 485 host = null; 486 hide(); 487 destroy(); 488 } 489 490 private void set_large_window_mode() { 491 set_size_request(LARGE_WINDOW_WIDTH, LARGE_WINDOW_HEIGHT); 492 central_area_layouter.set_size_request(LARGE_WINDOW_WIDTH - BORDER_REGION_WIDTH, 493 LARGE_WINDOW_HEIGHT - BORDER_REGION_HEIGHT); 494 resizable = false; 495 } 496 497 private void set_colossal_window_mode() { 498 set_size_request(COLOSSAL_WINDOW_WIDTH, COLOSSAL_WINDOW_HEIGHT); 499 central_area_layouter.set_size_request(COLOSSAL_WINDOW_WIDTH - BORDER_REGION_WIDTH, 500 COLOSSAL_WINDOW_HEIGHT - BORDER_REGION_HEIGHT); 501 resizable = false; 502 } 503 504 private void set_standard_window_mode() { 505 set_size_request(STANDARD_WINDOW_WIDTH, STANDARD_WINDOW_HEIGHT); 506 central_area_layouter.set_size_request(STANDARD_WINDOW_WIDTH - BORDER_REGION_WIDTH, 507 STANDARD_WINDOW_HEIGHT - BORDER_REGION_HEIGHT); 508 resizable = false; 509 } 510 511 private void set_free_sizable_window_mode() { 512 resizable = true; 513 } 514 515 private void clear_free_sizable_window_mode() { 516 resizable = false; 517 } 518 519 public Spit.Publishing.DialogPane get_active_pane() { 520 return active_pane; 521 } 522 523 public void set_close_button_mode() { 524 close_cancel_button.set_label(_("_Close")); 525 set_default(close_cancel_button); 526 } 527 528 public void set_cancel_button_mode() { 529 close_cancel_button.set_label(_("_Cancel")); 530 set_default(null); 531 } 532 533 public void lock_service() { 534 service_selector_box.set_sensitive(false); 535 } 536 537 public void unlock_service() { 538 service_selector_box.set_sensitive(true); 539 } 540 541 public void install_pane(Spit.Publishing.DialogPane pane) { 542 debug("PublishingDialog: install_pane( ): invoked."); 543 544 if (active_pane != null) { 545 debug("PublishingDialog: install_pane( ): a pane is already installed; removing it."); 546 547 active_pane.on_pane_uninstalled(); 548 central_area_layouter.remove(active_pane.get_widget()); 549 } 550 551 central_area_layouter.pack_start(pane.get_widget(), true, true, 0); 552 show_all(); 553 554 Spit.Publishing.DialogPane.GeometryOptions geometry_options = 555 pane.get_preferred_geometry(); 556 if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.EXTENDED_SIZE) != 0) 557 set_large_window_mode(); 558 else if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.COLOSSAL_SIZE) != 0) 559 set_colossal_window_mode(); 560 else 561 set_standard_window_mode(); 562 563 if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.RESIZABLE) != 0) 564 set_free_sizable_window_mode(); 565 else 566 clear_free_sizable_window_mode(); 567 568 active_pane = pane; 569 pane.on_pane_installed(); 570 } 571 572 public new int run() { 573 on_service_changed(); 574 575 int result = base.run(); 576 577 host = null; 578 579 return result; 580 } 581} 582 583} 584 585