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