/* * trash-empty.c: a routine to empty the trash * * Copyright © 2008 Ryan Lortie * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include #include #include "trash-empty.h" #include "config.h" /* only one concurrent trash empty operation can occur */ static GtkDialog *trash_empty_confirm_dialog; static GtkDialog *trash_empty_dialog; static GtkProgressBar *trash_empty_progress_bar; static GtkLabel *trash_empty_location; static GtkLabel *trash_empty_file; /* the rules: * 1) nothing here may be modified while trash_empty_update_pending. * 2) an idle may only be scheduled if trash_empty_update_pending. * 3) only the worker may set trash_empty_update_pending = TRUE. * 4) only the UI updater may set trash_empty_update_pending = FALSE. * * i -think- this is threadsafe... ((famous last words...)) */ static GFile * volatile trash_empty_current_file; static gsize volatile trash_empty_deleted_files; static gsize volatile trash_empty_total_files; static gboolean volatile trash_empty_update_pending; static gboolean trash_empty_clear_pending (gpointer user_data) { trash_empty_update_pending = FALSE; return FALSE; } static gboolean trash_empty_update_dialog (gpointer user_data) { gsize deleted, total; GFile *file; g_assert (trash_empty_update_pending); deleted = trash_empty_deleted_files; total = trash_empty_total_files; file = trash_empty_current_file; /* maybe the done() got processed first. */ if (trash_empty_dialog) { char *index_str, *total_str; char *text_tmp, *text; char *tmp; /* The i18n tools can't handle a direct embedding of the * size format using a macro. This is a work-around. */ index_str = g_strdup_printf ("%"G_GSIZE_FORMAT, deleted + 1); total_str = g_strdup_printf ("%"G_GSIZE_FORMAT, total); /* Translators: the %s in this string should be read as %d. */ text = g_strdup_printf (_("Removing item %s of %s"), index_str, total_str); gtk_progress_bar_set_text (trash_empty_progress_bar, text); g_free (total_str); g_free (index_str); g_free (text); if (deleted > total) gtk_progress_bar_set_fraction (trash_empty_progress_bar, 1.0); else gtk_progress_bar_set_fraction (trash_empty_progress_bar, (gdouble) deleted / (gdouble) total); /* no g_file_get_basename? */ { GFile *parent; parent = g_file_get_parent (file); text = g_file_get_uri (parent); g_object_unref (parent); } gtk_label_set_text (trash_empty_location, text); g_free (text); tmp = g_file_get_basename (file); /* Translators: %s is a file name */ text_tmp = g_strdup_printf (_("Removing: %s"), tmp); text = g_markup_printf_escaped ("%s", text_tmp); gtk_label_set_markup (trash_empty_file, text); g_free (text); g_free (text_tmp); g_free (tmp); /* unhide the labels */ gtk_widget_show_all (GTK_WIDGET (trash_empty_dialog)); } trash_empty_current_file = NULL; g_object_unref (file); trash_empty_update_pending = FALSE; return FALSE; } static gboolean trash_empty_done (gpointer user_data) { gtk_widget_destroy (GTK_WIDGET (trash_empty_dialog)); g_assert (trash_empty_dialog == NULL); return FALSE; } /* =============== worker thread code begins here =============== */ static void trash_empty_maybe_schedule_update (GIOSchedulerJob *job, GFile *file, gsize deleted) { if (!trash_empty_update_pending) { g_assert (trash_empty_current_file == NULL); trash_empty_current_file = g_object_ref (file); trash_empty_deleted_files = deleted; trash_empty_update_pending = TRUE; g_io_scheduler_job_send_to_mainloop_async (job, trash_empty_update_dialog, NULL, NULL); } } static void trash_empty_delete_contents (GIOSchedulerJob *job, GCancellable *cancellable, GFile *file, gboolean actually_delete, gsize *deleted) { GFileEnumerator *enumerator; GFileInfo *info; GFile *child; if (g_cancellable_is_cancelled (cancellable)) return; enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, NULL); if (enumerator) { while ((info = g_file_enumerator_next_file (enumerator, cancellable, NULL)) != NULL) { child = g_file_get_child (file, g_file_info_get_name (info)); if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) trash_empty_delete_contents (job, cancellable, child, actually_delete, deleted); if (actually_delete) { trash_empty_maybe_schedule_update (job, child, *deleted); g_file_delete (child, cancellable, NULL); } (*deleted)++; g_object_unref (child); g_object_unref (info); if (g_cancellable_is_cancelled (cancellable)) break; } g_object_unref (enumerator); } } static gboolean trash_empty_job (GIOSchedulerJob *job, GCancellable *cancellable, gpointer user_data) { gsize deleted; GFile *trash; trash = g_file_new_for_uri ("trash:///"); /* first do a dry run to count the number of files */ deleted = 0; trash_empty_delete_contents (job, cancellable, trash, FALSE, &deleted); trash_empty_total_files = deleted; /* now do the real thing */ deleted = 0; trash_empty_delete_contents (job, cancellable, trash, TRUE, &deleted); /* done */ g_object_unref (trash); g_io_scheduler_job_send_to_mainloop_async (job, trash_empty_done, NULL, NULL); return FALSE; } /* ================ worker thread code ends here ================ */ static void trash_empty_start (GtkWidget *parent) { struct { const char *name; gpointer *pointer; } widgets[] = { { "empty_trash", (gpointer *) &trash_empty_dialog }, { "progressbar", (gpointer *) &trash_empty_progress_bar }, { "location_label", (gpointer *) &trash_empty_location }, { "file_label", (gpointer *) &trash_empty_file } }; GCancellable *cancellable; GtkBuilder *builder; gint i; builder = gtk_builder_new (); gtk_builder_add_from_file (builder, GTK_BUILDERDIR "/trashapplet-empty-progress.ui", NULL); for (i = 0; i < G_N_ELEMENTS (widgets); i++) { GObject *object; object = gtk_builder_get_object (builder, widgets[i].name); if (object == NULL) { g_critical ("failed to parse trash-empty dialog markup"); if (trash_empty_dialog) gtk_widget_destroy (GTK_WIDGET (trash_empty_dialog)); g_object_unref (builder); return; } *widgets[i].pointer = object; g_object_add_weak_pointer (object, widgets[i].pointer); } g_object_unref (builder); cancellable = g_cancellable_new (); g_signal_connect_object (trash_empty_dialog, "response", G_CALLBACK (g_cancellable_cancel), cancellable, G_CONNECT_SWAPPED); g_io_scheduler_push_job (trash_empty_job, NULL, NULL, 0, cancellable); g_object_unref (cancellable); gtk_window_set_screen (GTK_WINDOW (trash_empty_dialog), gtk_widget_get_screen (parent)); gtk_widget_show (GTK_WIDGET (trash_empty_dialog)); } static gboolean trash_empty_require_confirmation (void) { gboolean confirm_trash; GSettings *settings; settings = g_settings_new ("org.mate.caja.preferences"); confirm_trash = g_settings_get_boolean (settings, "confirm-trash"); g_object_unref (settings); return confirm_trash; } static void trash_empty_confirmation_response (GtkDialog *dialog, gint response_id, gpointer user_data) { if (response_id == GTK_RESPONSE_YES) trash_empty_start (GTK_WIDGET (dialog)); gtk_widget_destroy (GTK_WIDGET (dialog)); g_assert (trash_empty_confirm_dialog == NULL); } /* * The code in trash_empty_show_confirmation_dialog() was taken from * libcaja-private/caja-file-operations.c (confirm_empty_trash) * by Michiel Sikkes and adapted for the applet. */ static void trash_empty_show_confirmation_dialog (GtkWidget *parent) { GtkWidget *dialog; GtkWidget *button; GdkScreen *screen; if (!trash_empty_require_confirmation ()) { trash_empty_start (parent); return; } screen = gtk_widget_get_screen (parent); dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_NONE, _("Empty all of the items from " "the trash?")); trash_empty_confirm_dialog = GTK_DIALOG (dialog); g_object_add_weak_pointer (G_OBJECT (dialog), (gpointer *) &trash_empty_confirm_dialog); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), _("If you choose to empty " "the trash, all items in " "it will be permanently " "lost. Please note that " "you can also delete them " "separately.")); gtk_window_set_screen (GTK_WINDOW (dialog), screen); atk_object_set_role (gtk_widget_get_accessible (dialog), ATK_ROLE_ALERT); gtk_dialog_add_button (GTK_DIALOG (dialog), "gtk-cancel", GTK_RESPONSE_CANCEL); button = gtk_button_new_with_mnemonic (_("_Empty Trash")); gtk_widget_show (button); gtk_widget_set_can_default (button, TRUE); gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button, GTK_RESPONSE_YES); gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES); gtk_widget_show (dialog); g_signal_connect (dialog, "response", G_CALLBACK (trash_empty_confirmation_response), NULL); } void trash_empty (GtkWidget *parent) { if (trash_empty_confirm_dialog) gtk_window_present (GTK_WINDOW (trash_empty_confirm_dialog)); else if (trash_empty_dialog) gtk_window_present (GTK_WINDOW (trash_empty_dialog)); /* theoretically possible that an update is pending, but very unlikely. */ else if (!trash_empty_update_pending) trash_empty_show_confirmation_dialog (parent); }