1 /*
2  * Seahorse
3  *
4  * Copyright (C) 2004 Stefan Walter
5  * Copyright (C) 2011 Collabora Ltd.
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15  * See the GNU General Public License for more details.
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see
18  * <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "config.h"
22 
23 #include "seahorse-progress.h"
24 
25 #include <stdio.h>
26 #include <unistd.h>
27 #include <sys/time.h>
28 
29 typedef enum _TrackedState {
30     TASK_PART_PREPPED = 1,
31     TASK_PART_BEGUN,
32     TASK_PART_ENDED
33 } TrackedState;
34 
35 typedef struct _TrackedPart {
36     const void *progress_tag;
37     char *details;
38     TrackedState state;
39 } TrackedPart;
40 
41 typedef struct _TrackedTask {
42     GCancellable *cancellable;
43     unsigned long cancelled_sig;
44 
45     GtkWidget *dialog;
46     GtkBuilder *builder;
47     char *title;
48     char *notice;
49     gboolean showing;
50 
51     GQueue *parts;
52     int parts_prepped;
53     int parts_begun;
54     int parts_ended;
55 } TrackedTask;
56 
57 /* maps GCancellables to TrackedTasks */
58 static GHashTable *tracked_tasks = NULL;
59 
60 static void
tracked_part_free(void * data)61 tracked_part_free (void *data)
62 {
63     TrackedPart *part = data;
64     g_free (part->details);
65     g_free (part);
66 }
67 
68 static int
find_part_progress_tag(const void * part_data,const void * progress_tag)69 find_part_progress_tag (const void *part_data,
70                         const void *progress_tag)
71 {
72     TrackedPart *part = (TrackedPart *) part_data;
73     return (part->progress_tag == progress_tag) ? 0 : 1;
74 }
75 
76 static int
find_part_begun_with_details(const void * part_data,const void * unused)77 find_part_begun_with_details (const void *part_data,
78                               const void *unused)
79 {
80     TrackedPart *part = (TrackedPart *) part_data;
81     return (part->state == TASK_PART_BEGUN && part->details) ? 0 : 1;
82 }
83 
84 static TrackedPart *
tracked_part_find(TrackedTask * task,GCompareFunc func,const void * user_data)85 tracked_part_find (TrackedTask  *task,
86                    GCompareFunc  func,
87                    const void   *user_data)
88 {
89     GList *part = g_queue_find_custom (task->parts, user_data, func);
90     return part ? part->data : NULL;
91 }
92 
93 static void
on_cancellable_gone(void * user_data,GObject * where_the_object_was)94 on_cancellable_gone (void    *user_data,
95                      GObject *where_the_object_was)
96 {
97     TrackedTask *task;
98 
99     g_assert (tracked_tasks);
100     task = g_hash_table_lookup (tracked_tasks, where_the_object_was);
101     g_assert ((void *) task->cancellable == (void *) where_the_object_was);
102     task->cancellable = NULL;
103     task->cancelled_sig = 0;
104 
105     if (!g_hash_table_remove (tracked_tasks, where_the_object_was))
106         g_assert_not_reached ();
107 }
108 
109 static void
on_cancellable_cancelled(GCancellable * cancellable,void * user_data)110 on_cancellable_cancelled (GCancellable *cancellable,
111                           void         *user_data)
112 {
113     g_assert (tracked_tasks);
114     g_assert (((TrackedTask *)user_data)->cancellable == cancellable);
115     if (!g_hash_table_remove (tracked_tasks, cancellable))
116         g_assert_not_reached ();
117 }
118 
119 static void
tracked_task_free(void * data)120 tracked_task_free (void *data)
121 {
122     TrackedTask *task = data;
123     if (task->cancellable) {
124         g_object_weak_unref (G_OBJECT (task->cancellable),
125                              on_cancellable_gone, task);
126 
127         /* Use this because g_cancellable_disconnect() deadlocks */
128         if (task->cancelled_sig)
129             g_signal_handler_disconnect (task->cancellable,
130                                          task->cancelled_sig);
131     }
132 
133     g_queue_foreach (task->parts, (GFunc)tracked_part_free, NULL);
134     g_queue_free (task->parts);
135     g_free (task->title);
136     g_free (task->notice);
137     g_clear_object (&task->builder);
138     if (task->dialog)
139       gtk_widget_destroy (task->dialog);
140     g_free (task);
141 }
142 
143 static gboolean
on_pulse_timeout(void * user_data)144 on_pulse_timeout (void *user_data)
145 {
146     GtkProgressBar *progress = GTK_PROGRESS_BAR (user_data);
147 
148     if (gtk_progress_bar_get_pulse_step (progress) != 0) {
149         gtk_progress_bar_pulse (progress);
150         return G_SOURCE_CONTINUE;
151     }
152 
153     return G_SOURCE_REMOVE;
154 }
155 
156 static void
start_pulse(GtkProgressBar * progress)157 start_pulse (GtkProgressBar *progress)
158 {
159     unsigned int stag;
160 
161     gtk_progress_bar_set_pulse_step (progress, 0.05);
162     gtk_progress_bar_pulse (progress);
163 
164     stag = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (progress), "pulse-timer"));
165 
166     /* Start a pulse timer */
167     if (stag == 0) {
168         stag = g_timeout_add (100, on_pulse_timeout, progress);
169         g_object_set_data_full (G_OBJECT (progress), "pulse-timer",
170                                 GINT_TO_POINTER (stag), (GDestroyNotify)g_source_remove);
171     }
172 }
173 
174 static void
stop_pulse(GtkProgressBar * progress)175 stop_pulse (GtkProgressBar *progress)
176 {
177     gtk_progress_bar_set_pulse_step (progress, 0.0);
178 
179     /* This causes the destroy callback to be called */
180     g_object_set_data (G_OBJECT (progress), "pulse-timer", NULL);
181 }
182 
183 static void
progress_update_display(TrackedTask * task)184 progress_update_display (TrackedTask *task)
185 {
186     GtkProgressBar *progress = NULL;
187     GtkStatusbar *status = NULL;
188     GtkLabel *label = NULL;
189     GtkWidget *widget;
190     TrackedPart *part;
191     double fraction;
192     unsigned int id;
193 
194     if (task->builder == NULL)
195         return;
196 
197     /* Dig out our progress display stuff */
198     widget = GTK_WIDGET (gtk_builder_get_object (task->builder, "progress-bar"));
199     if (widget == NULL)
200         g_warning ("cannot display progress because seahorse window has no progress widget");
201     else
202         progress = GTK_PROGRESS_BAR (widget);
203     widget = GTK_WIDGET (gtk_builder_get_object (task->builder, "status"));
204     if (GTK_IS_STATUSBAR (widget)) {
205         status = GTK_STATUSBAR (widget);
206     } else {
207         widget = GTK_WIDGET (gtk_builder_get_object (task->builder, "progress-details"));
208         if (GTK_IS_LABEL (widget))
209             label = GTK_LABEL (widget);
210     }
211 
212     /* The details is the first on a begun part */
213     part = tracked_part_find (task, find_part_begun_with_details, NULL);
214     if (status) {
215         id = gtk_statusbar_get_context_id (status, "operation-progress");
216         gtk_statusbar_pop (status, id);
217         if (part != NULL)
218             gtk_statusbar_push (status, id, part->details);
219     } else if (label) {
220         gtk_label_set_text (label, part ? part->details : "");
221     }
222 
223     /* If all parts are running simultaneously then indeterminate */
224     if (task->parts_prepped == 0 && task->parts_ended == 0) {
225         fraction = -1;
226     } else {
227         fraction = (double) task->parts_ended /
228                    (double) task->parts->length;
229     }
230 
231     if (progress) {
232         if (fraction >= 0.0) {
233             stop_pulse (progress);
234             gtk_progress_bar_set_fraction (progress, fraction);
235         } else {
236             start_pulse (progress);
237         }
238     }
239 }
240 
241 static TrackedTask *
progress_lookup_or_create_task(GCancellable * cancellable)242 progress_lookup_or_create_task (GCancellable *cancellable)
243 {
244     TrackedTask *task;
245 
246     if (tracked_tasks == NULL)
247         tracked_tasks = g_hash_table_new_full (g_direct_hash, g_direct_equal,
248                                                NULL, tracked_task_free);
249 
250     task = g_hash_table_lookup (tracked_tasks, cancellable);
251     if (task == NULL) {
252         if (g_cancellable_is_cancelled (cancellable))
253             return NULL;
254 
255         task = g_new0 (TrackedTask, 1);
256         task->cancellable = cancellable;
257         g_object_weak_ref (G_OBJECT (task->cancellable),
258                            on_cancellable_gone, task);
259         task->parts = g_queue_new ();
260 
261         g_hash_table_insert (tracked_tasks, cancellable, task);
262         task->cancelled_sig = g_cancellable_connect (cancellable,
263                                                      G_CALLBACK (on_cancellable_cancelled),
264                                                      task, NULL);
265     }
266 
267     return task;
268 }
269 
270 static void
progress_prep_va(GCancellable * cancellable,const void * progress_tag,const char * details,va_list va)271 progress_prep_va (GCancellable *cancellable,
272                   const void   *progress_tag,
273                   const char   *details,
274                   va_list       va)
275 {
276     TrackedTask *task;
277     TrackedPart *part;
278 
279     task = progress_lookup_or_create_task (cancellable);
280 
281     /* Perhaps already cancelled? */
282     if (task == NULL)
283         return;
284 
285     part = tracked_part_find (task, find_part_progress_tag, progress_tag);
286     if (part == NULL) {
287         part = g_new0 (TrackedPart, 1);
288         if (details && details[0])
289             part->details = g_strdup_vprintf (details, va);
290         part->progress_tag = progress_tag;
291         part->state = TASK_PART_PREPPED;
292         g_queue_push_tail (task->parts, part);
293         task->parts_prepped++;
294     } else {
295         g_warning ("already tracking progress for this part of the operation");
296     }
297 
298     /* If the dialog is being displayed, update it */
299     g_assert (task->parts_prepped + task->parts_begun + task->parts_ended == (int) task->parts->length);
300     progress_update_display (task);
301 }
302 
303 void
seahorse_progress_prep(GCancellable * cancellable,const void * progress_tag,const char * details,...)304 seahorse_progress_prep (GCancellable *cancellable,
305                         const void   *progress_tag,
306                         const char   *details,
307                         ...)
308 {
309     va_list va;
310 
311     if (!cancellable)
312         return;
313 
314     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
315 
316     va_start (va, details);
317     progress_prep_va (cancellable, progress_tag, details, va);
318     va_end (va);
319 }
320 
321 void
seahorse_progress_begin(GCancellable * cancellable,const void * progress_tag)322 seahorse_progress_begin (GCancellable *cancellable,
323                          const void   *progress_tag)
324 {
325     TrackedTask *task = NULL;
326     TrackedPart *part;
327 
328     if (!cancellable)
329         return;
330 
331     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
332     if (g_cancellable_is_cancelled (cancellable))
333         return;
334 
335     if (tracked_tasks)
336         task = g_hash_table_lookup (tracked_tasks, cancellable);
337     if (task == NULL) {
338         g_warning ("caller is trying to begin part for task that does not exist");
339         return;
340     }
341 
342     part = tracked_part_find (task, find_part_progress_tag, progress_tag);
343     if (part == NULL) {
344         g_warning ("caller is trying to begin part of task that does not exist");
345         return;
346     }
347 
348     switch (part->state) {
349     case TASK_PART_PREPPED:
350         part->state = TASK_PART_BEGUN;
351         task->parts_begun++;
352         task->parts_prepped--;
353         break;
354     case TASK_PART_BEGUN:
355         g_warning ("caller is trying to begin part of task that has already begun");
356         return;
357     case TASK_PART_ENDED:
358         g_warning ("caller is trying to begin part of task that has already ended");
359         return;
360     default:
361         g_assert_not_reached ();
362         break;
363     }
364 
365     g_assert (task->parts_prepped + task->parts_begun + task->parts_ended == (int) task->parts->length);
366     progress_update_display (task);
367 }
368 
369 void
seahorse_progress_prep_and_begin(GCancellable * cancellable,const void * progress_tag,const char * details,...)370 seahorse_progress_prep_and_begin (GCancellable *cancellable,
371                                   const void   *progress_tag,
372                                   const char   *details,
373                                   ...)
374 {
375     va_list va;
376 
377     if (!cancellable)
378         return;
379 
380     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
381 
382     va_start (va, details);
383     progress_prep_va (cancellable, progress_tag, details, va);
384     seahorse_progress_begin (cancellable, progress_tag);
385     va_end (va);
386 }
387 
388 void
seahorse_progress_update(GCancellable * cancellable,const void * progress_tag,const char * details,...)389 seahorse_progress_update (GCancellable *cancellable,
390                           const void   *progress_tag,
391                           const char   *details,
392                           ...)
393 {
394     TrackedTask *task = NULL;
395     TrackedPart *part;
396     va_list va;
397 
398     if (!cancellable)
399         return;
400 
401     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
402     if (g_cancellable_is_cancelled (cancellable))
403         return;
404 
405     if (tracked_tasks)
406         task = g_hash_table_lookup (tracked_tasks, cancellable);
407     if (task == NULL) {
408         g_warning ("caller is trying to update part for task that does not exist");
409         return;
410     }
411 
412     part = tracked_part_find (task, find_part_progress_tag, progress_tag);
413     if (part == NULL) {
414         g_warning ("caller is trying to update part of task that does not exist");
415         return;
416     }
417 
418     switch (part->state) {
419     case TASK_PART_PREPPED:
420     case TASK_PART_BEGUN:
421         g_free (part->details);
422         if (details && details[0]) {
423             va_start (va, details);
424             part->details = g_strdup_vprintf (details, va);
425             va_end (va);
426         }
427         break;
428     case TASK_PART_ENDED:
429         g_warning ("caller is trying to update part of task that has already ended");
430         return;
431     default:
432         g_assert_not_reached ();
433         return;
434     }
435 
436     g_assert (task->parts_prepped + task->parts_begun + task->parts_ended == (int) task->parts->length);
437     progress_update_display (task);
438 }
439 
440 void
seahorse_progress_end(GCancellable * cancellable,const void * progress_tag)441 seahorse_progress_end (GCancellable *cancellable,
442                        const void   *progress_tag)
443 {
444     TrackedTask *task = NULL;
445     TrackedPart *part;
446 
447     if (!cancellable)
448         return;
449 
450     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
451     if (g_cancellable_is_cancelled (cancellable))
452         return;
453 
454     if (tracked_tasks)
455         task = g_hash_table_lookup (tracked_tasks, cancellable);
456     if (task == NULL) {
457         g_warning ("caller is trying to end part for task that does not exist");
458         return;
459     }
460 
461     part = tracked_part_find (task, find_part_progress_tag, progress_tag);
462     if (part == NULL) {
463         g_warning ("caller is trying to end part of task that does not exist");
464         return;
465     }
466 
467     switch (part->state) {
468     case TASK_PART_PREPPED:
469         g_warning ("caller is trying to end part of task that has not begun");
470         return;
471     case TASK_PART_BEGUN:
472         part->state = TASK_PART_ENDED;
473         task->parts_begun--;
474         task->parts_ended++;
475         break;
476     case TASK_PART_ENDED:
477         g_warning ("caller is trying to end part of task that has already ended");
478         return;
479     default:
480         g_assert_not_reached ();
481         break;
482     }
483 
484     g_assert (task->parts_prepped + task->parts_begun + task->parts_ended == (int) task->parts->length);
485     progress_update_display (task);
486 }
487 
488 static int
on_window_delete_event(GtkWidget * widget,GdkEvent * event,void * user_data)489 on_window_delete_event (GtkWidget *widget,
490                         GdkEvent  *event,
491                         void      *user_data)
492 {
493     TrackedTask *task = NULL;
494 
495     /* Guard against going away before we display */
496     if (tracked_tasks)
497         task = g_hash_table_lookup (tracked_tasks, user_data);
498     if (task != NULL)
499         g_cancellable_cancel (task->cancellable);
500 
501     return TRUE; /* allow window to close */
502 }
503 
504 static void
on_cancel_button_clicked(GtkButton * button,void * user_data)505 on_cancel_button_clicked (GtkButton *button,
506                           void      *user_data)
507 {
508     TrackedTask *task = NULL;
509 
510     /* Guard against going away before we display */
511     if (tracked_tasks)
512         task = g_hash_table_lookup (tracked_tasks, user_data);
513     if (task != NULL)
514         g_cancellable_cancel (task->cancellable);
515 }
516 
517 static gboolean
on_timeout_show_progress(void * user_data)518 on_timeout_show_progress (void *user_data)
519 {
520     TrackedTask *task = NULL;
521     g_autoptr(GtkBuilder) builder = NULL;
522     GtkWidget *widget;
523 
524     /* Guard against going away before we display */
525     if (tracked_tasks)
526         task = g_hash_table_lookup (tracked_tasks, user_data);
527     if (task == NULL)
528         return G_SOURCE_REMOVE;
529 
530     builder = gtk_builder_new_from_resource ("/org/gnome/Seahorse/seahorse-progress.ui");
531 
532     task->dialog = GTK_WIDGET (gtk_builder_get_object (builder, "progress"));
533     g_signal_connect (task->dialog, "delete-event",
534                       G_CALLBACK (on_window_delete_event), task->cancellable);
535 
536     /* Setup the title */
537     if (task->title) {
538         g_autofree char *text = NULL;
539 
540         gtk_window_set_title (GTK_WINDOW (task->dialog), task->title);
541 
542         /* The main message title */
543         widget = GTK_WIDGET (gtk_builder_get_object (builder, "progress-title"));
544         text = g_strdup_printf ("<b>%s</b>", task->title);
545         gtk_label_set_markup (GTK_LABEL (widget), text);
546     }
547 
548     /* Setup the notice */
549     if (task->notice) {
550         widget = GTK_WIDGET (gtk_builder_get_object (builder, "progress-notice"));
551         gtk_label_set_label (GTK_LABEL (widget), task->notice);
552         gtk_widget_show (widget);
553     }
554 
555     /* Setup the cancel button */
556     widget = GTK_WIDGET (gtk_builder_get_object (builder, "progress-cancel"));
557     g_signal_connect (widget, "clicked", G_CALLBACK (on_cancel_button_clicked),
558                       task->cancellable);
559 
560     /* Allow attach to work */
561     task->showing = FALSE;
562     seahorse_progress_attach (task->cancellable, builder);
563     gtk_widget_show (task->dialog);
564 
565     return G_SOURCE_REMOVE;
566 }
567 
568 void
seahorse_progress_show(GCancellable * cancellable,const char * title,gboolean delayed)569 seahorse_progress_show (GCancellable *cancellable,
570                         const char   *title,
571                         gboolean      delayed)
572 {
573     seahorse_progress_show_with_notice (cancellable, title, NULL, delayed);
574 }
575 
576 void
seahorse_progress_show_with_notice(GCancellable * cancellable,const char * title,const char * notice,gboolean delayed)577 seahorse_progress_show_with_notice (GCancellable *cancellable,
578                                     const char   *title,
579                                     const char   *notice,
580                                     gboolean      delayed)
581 {
582     TrackedTask *task;
583 
584     g_return_if_fail (title != NULL && title[0] != '\0');
585 
586     if (!cancellable)
587         return;
588 
589     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
590 
591     task = progress_lookup_or_create_task (cancellable);
592 
593     /* Perhaps already cancelled? */
594     if (task == NULL)
595         return;
596 
597     if (task->showing) {
598         g_warning ("caller is trying to show progress for a task which already has displayed progress");
599         return;
600     }
601 
602     g_free (task->title);
603     task->title = g_strdup (title);
604     task->notice = g_strdup (notice);
605     task->showing = TRUE;
606 
607     if (delayed)
608         g_timeout_add_seconds (2, on_timeout_show_progress, cancellable);
609     else
610         on_timeout_show_progress (cancellable);
611 }
612 
613 void
seahorse_progress_attach(GCancellable * cancellable,GtkBuilder * builder)614 seahorse_progress_attach (GCancellable *cancellable,
615                           GtkBuilder *builder)
616 {
617     TrackedTask *task;
618 
619     if (!cancellable)
620         return;
621 
622     g_return_if_fail (G_IS_CANCELLABLE (cancellable));
623 
624     task = progress_lookup_or_create_task (cancellable);
625 
626     /* Perhaps already cancelled? */
627     if (task == NULL)
628         return;
629 
630     if (task->showing) {
631         g_warning ("caller is trying to show progress for a task which already has displayed progress");
632         return;
633     }
634 
635     task->showing = TRUE;
636     task->builder = g_object_ref (builder);
637 
638     progress_update_display (task);
639 }
640