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