/* * fm-progress-dlg.c * * Copyright 2009 PCMan * * 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 Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifdef HAVE_CONFIG_H #include #endif #include "fm-progress-dlg.h" #include "fm-gtk-utils.h" #include #define SHOW_DLG_DELAY 1000 enum { RESPONSE_OVERWRITE = 1, RESPONSE_RENAME, RESPONSE_SKIP }; struct _FmProgressDisplay { GtkWidget* dlg; FmFileOpsJob* job; /* private */ GtkWidget* icon; GtkWidget* msg; GtkWidget* act; GtkWidget* src; GtkWidget* dest; GtkWidget* current; GtkWidget* progress; GtkWidget* remaining_time; GtkWidget* error_pane; GtkWidget* error_msg; GtkTextBuffer* error_buf; GtkTextTag* bold_tag; FmFileOpOption default_opt; char* cur_file; const char* old_cur_file; guint delay_timeout; guint update_timeout; GTimer* timer; gboolean has_error : 1; }; static void ensure_dlg(FmProgressDisplay* data); static void fm_progress_display_destroy(FmProgressDisplay* data); static void on_percent(FmFileOpsJob* job, guint percent, FmProgressDisplay* data) { if(data->dlg) { GTimeVal cur_time; char percent_text[64]; g_snprintf(percent_text, 64, "%d %%", percent); gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(data->progress), (gdouble)percent/100); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(data->progress), percent_text); gdouble elapsed = g_timer_elapsed(data->timer, NULL); if(elapsed >= 0.5) { gdouble remaining = elapsed * (100 - percent) / percent; if(data->remaining_time) { char time_str[32]; guint secs = (guint)remaining; guint mins = 0; guint hrs = 0; if(secs > 60) { mins = secs / 60; secs %= 60; if(mins > 60) { hrs = mins / 60; mins %= 60; } } g_snprintf(time_str, 32, "%02d:%02d:%02d", hrs, mins, secs); gtk_label_set_text(data->remaining_time, time_str); } } } } static void on_cur_file(FmFileOpsJob* job, const char* cur_file, FmProgressDisplay* data) { /* FIXME: Displaying currently processed file will slow down the * operation and waste CPU source due to showing the text with pango. * Consider showing current file every 0.5 second. */ g_free(data->cur_file); data->cur_file = g_strdup(cur_file); } static FmJobErrorAction on_error(FmFileOpsJob* job, GError* err, FmJobErrorSeverity severity, FmProgressDisplay* data) { GtkTextIter it; if(err->domain == G_IO_ERROR) { if(err->code == G_IO_ERROR_CANCELLED) return FM_JOB_ABORT; else if(err->code == G_IO_ERROR_FAILED_HANDLED) return FM_JOB_CONTINUE; } if(data->timer) g_timer_stop(data->timer); data->has_error = TRUE; ensure_dlg(data); /* FIXME: Need to mount volumes on demand here, too. if( err->domain == G_IO_ERROR ) { if( err->code == G_IO_ERROR_NOT_MOUNTED && severity < FM_JOB_ERROR_CRITICAL ) if(fm_mount_path(parent, dest_path)) return FM_JOB_RETRY; } */ gtk_text_buffer_get_end_iter(data->error_buf, &it); gtk_text_buffer_insert_with_tags(data->error_buf, &it, data->cur_file, -1, data->bold_tag, NULL); gtk_text_buffer_insert(data->error_buf, &it, _(": "), -1); gtk_text_buffer_insert(data->error_buf, &it, err->message, -1); gtk_text_buffer_insert(data->error_buf, &it, "\n", 1); if(!GTK_WIDGET_VISIBLE(data->error_pane)) gtk_widget_show(data->error_pane); if(data->timer) g_timer_continue(data->timer); return FM_JOB_CONTINUE; } static gint on_ask(FmFileOpsJob* job, const char* question, const char** options, FmProgressDisplay* data) { ensure_dlg(data); return fm_askv(GTK_WINDOW(data->dlg), question, options); } static void on_filename_changed(GtkEditable* entry, GtkWidget* rename) { const char* old_name = g_object_get_data(G_OBJECT(entry), "old_name"); const char* new_name = gtk_entry_get_text(GTK_ENTRY(entry)); gboolean can_rename = new_name && *new_name && g_strcmp0(old_name, new_name); gtk_widget_set_sensitive(rename, can_rename); if(can_rename) { GtkDialog* dlg = GTK_DIALOG(gtk_widget_get_toplevel(entry)); gtk_dialog_set_default_response(dlg, gtk_dialog_get_response_for_widget(dlg, rename)); } } static gint on_ask_rename(FmFileOpsJob* job, FmFileInfo* src, FmFileInfo* dest, char** new_name, FmProgressDisplay* data) { int res; GtkBuilder* builder = gtk_builder_new(); GtkWidget *dlg, *src_icon, *dest_icon, *src_fi, *dest_fi, *filename, *apply_all; char* tmp; const char* disp_size; /* return default operation if the user has set it */ if(data->default_opt) return data->default_opt; if(data->timer) g_timer_stop(data->timer); gtk_builder_set_translation_domain(builder, GETTEXT_PACKAGE); ensure_dlg(data); gtk_builder_add_from_file(builder, PACKAGE_UI_DIR "/ask-rename.ui", NULL); dlg = (GtkWidget*)gtk_builder_get_object(builder, "dlg"); src_icon = (GtkWidget*)gtk_builder_get_object(builder, "src_icon"); src_fi = (GtkWidget*)gtk_builder_get_object(builder, "src_fi"); dest_icon = (GtkWidget*)gtk_builder_get_object(builder, "dest_icon"); dest_fi = (GtkWidget*)gtk_builder_get_object(builder, "dest_fi"); filename = (GtkWidget*)gtk_builder_get_object(builder, "filename"); apply_all = (GtkWidget*)gtk_builder_get_object(builder, "apply_all"); gtk_window_set_transient_for(GTK_WINDOW(dlg), data->dlg); gtk_image_set_from_gicon(GTK_IMAGE(src_icon), src->icon->gicon, GTK_ICON_SIZE_DIALOG); disp_size = fm_file_info_get_disp_size(src); if(disp_size) { tmp = g_strdup_printf(_("Type: %s\nSize: %s\nModified: %s"), fm_file_info_get_desc(src), disp_size, fm_file_info_get_disp_mtime(src)); } else { tmp = g_strdup_printf(_("Type: %s\nModified: %s"), fm_file_info_get_desc(src), fm_file_info_get_disp_mtime(src)); } gtk_label_set_text(GTK_LABEL(src_fi), tmp); g_free(tmp); gtk_image_set_from_gicon(GTK_IMAGE(dest_icon), src->icon->gicon, GTK_ICON_SIZE_DIALOG); disp_size = fm_file_info_get_disp_size(dest); if(disp_size) { tmp = g_strdup_printf(_("Type: %s\nSize: %s\nModified: %s"), fm_file_info_get_desc(dest), fm_file_info_get_disp_size(dest), fm_file_info_get_disp_mtime(dest)); } else { tmp = g_strdup_printf(_("Type: %s\nModified: %s"), fm_file_info_get_desc(dest), fm_file_info_get_disp_mtime(dest)); } gtk_label_set_text(GTK_LABEL(dest_fi), tmp); g_free(tmp); tmp = g_filename_display_name(dest->path->name); gtk_entry_set_text(GTK_ENTRY(filename), tmp); g_free(tmp); g_object_set_data(G_OBJECT(filename), "old_name", dest->disp_name); g_signal_connect(filename, "changed", on_filename_changed, gtk_builder_get_object(builder, "rename")); g_object_unref(builder); res = gtk_dialog_run(GTK_DIALOG(dlg)); switch(res) { case RESPONSE_RENAME: *new_name = g_strdup(gtk_entry_get_text(GTK_ENTRY(filename))); res = FM_FILE_OP_RENAME; break; case RESPONSE_OVERWRITE: res = FM_FILE_OP_OVERWRITE; break; case RESPONSE_SKIP: res = FM_FILE_OP_SKIP; break; default: res = FM_FILE_OP_CANCEL; } if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(apply_all))) { if(res == RESPONSE_OVERWRITE || res == FM_FILE_OP_SKIP) data->default_opt = res; } gtk_widget_destroy(dlg); if(data->timer) g_timer_continue(data->timer); return res; } static void on_finished(FmFileOpsJob* job, FmProgressDisplay* data) { if(data->update_timeout) { g_source_remove(data->update_timeout); data->update_timeout = 0; } if(data->dlg) { /* errors happened */ if(data->has_error && data->dlg) { gtk_label_set_text(GTK_LABEL(data->current), ""); gtk_label_set_text(GTK_LABEL(data->remaining_time), "00:00:00"); gtk_dialog_set_response_sensitive(GTK_DIALOG(data->dlg), GTK_RESPONSE_CANCEL, FALSE); gtk_dialog_add_button(GTK_DIALOG(data->dlg), GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE); gtk_image_set_from_stock(data->icon, GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_DIALOG); gtk_widget_show(data->msg); if(fm_job_is_cancelled(data->job)) { gtk_label_set_text(GTK_LABEL(data->msg), _("The file operation is cancelled and there are some errors.")); gtk_window_set_title(GTK_WINDOW(data->dlg), _("Cancelled")); } else { gtk_label_set_text(GTK_LABEL(data->msg), _("The file operation is finished, but there are some errors.")); gtk_window_set_title(GTK_WINDOW(data->dlg), _("Finished")); } } else fm_progress_display_destroy(data); g_debug("file operation is finished!"); } else fm_progress_display_destroy(data); /* sepcial handling for trash * FIXME: need to refactor this to use a more elegant way later. */ if(job->type == FM_FILE_OP_TRASH) { FmPathList* unsupported = (FmPathList*)g_object_get_data(job, "trash-unsupported"); /* some files cannot be trashed because underlying filesystems don't support it. */ if(unsupported) /* delete them instead */ { if(fm_yes_no(NULL, _("Some files cannot be moved to trash can because " "the underlying file systems don't support this operation.\n" "Do you want to delete them instead?"), TRUE)) { FmJob* job = fm_file_ops_job_new(FM_FILE_OP_DELETE, unsupported); fm_file_ops_job_run_with_progress(job); } } } } static void on_cancelled(FmFileOpsJob* job, FmProgressDisplay* data) { g_debug("file operation is cancelled!"); } static void on_response(GtkDialog* dlg, gint id, FmProgressDisplay* data) { /* cancel the job */ if(id == GTK_RESPONSE_CANCEL || id == GTK_RESPONSE_DELETE_EVENT) { if(data->job) { fm_job_cancel(FM_JOB(data->job)); if(id != GTK_RESPONSE_CANCEL) fm_progress_display_destroy(data); } } else if(id == GTK_RESPONSE_CLOSE) fm_progress_display_destroy(data); } static gboolean on_update_dlg(FmProgressDisplay* data) { if(data->old_cur_file != data->cur_file) { gtk_label_set_text(GTK_LABEL(data->current), data->cur_file); data->old_cur_file = data->cur_file; } return TRUE; } static gboolean on_show_dlg(FmProgressDisplay* data) { GtkBuilder* builder = gtk_builder_new(); GtkWidget* to, *to_label; FmPath* dest; const char* title = NULL; GtkTextTagTable* tag_table = gtk_text_tag_table_new(); gtk_builder_set_translation_domain(builder, GETTEXT_PACKAGE); gtk_builder_add_from_file(builder, PACKAGE_UI_DIR "/progress.ui", NULL); data->dlg = (GtkWidget*)gtk_builder_get_object(builder, "dlg"); g_signal_connect(data->dlg, "response", on_response, data); to_label = (GtkWidget*)gtk_builder_get_object(builder, "to_label"); to = (GtkWidget*)gtk_builder_get_object(builder, "dest"); data->icon = (GtkWidget*)gtk_builder_get_object(builder, "icon"); data->msg = (GtkWidget*)gtk_builder_get_object(builder, "msg"); data->act = (GtkWidget*)gtk_builder_get_object(builder, "action"); data->src = (GtkWidget*)gtk_builder_get_object(builder, "src"); data->dest = (GtkWidget*)gtk_builder_get_object(builder, "dest"); data->current = (GtkWidget*)gtk_builder_get_object(builder, "current"); data->progress = (GtkWidget*)gtk_builder_get_object(builder, "progress"); data->error_pane = (GtkWidget*)gtk_builder_get_object(builder, "error_pane"); data->error_msg = (GtkWidget*)gtk_builder_get_object(builder, "error_msg"); data->remaining_time = (GtkWidget*)gtk_builder_get_object(builder, "remaining_time"); data->bold_tag = gtk_text_tag_new("bold"); g_object_set(data->bold_tag, "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_tag_table_add(tag_table, data->bold_tag); data->error_buf = gtk_text_buffer_new(tag_table); g_object_unref(tag_table); gtk_text_view_set_buffer(GTK_TEXT_VIEW(data->error_msg), data->error_buf); g_object_unref(builder); /* set the src label */ if(data->job->srcs) { GList* l = fm_list_peek_head_link(data->job->srcs); int i; char* disp; FmPath* path; GString* str = g_string_sized_new(512); path = FM_PATH(l->data); disp = fm_path_display_basename(path); g_string_assign(str, disp); g_free(disp); for( i =1, l=l->next; i < 10 && l; l=l->next, ++i) { path = FM_PATH(l->data); g_string_append(str, _(", ")); disp = fm_path_display_basename(path); g_string_append(str, disp); g_free(disp); } if(l) g_string_append(str, "..."); gtk_label_set_text(data->src, str->str); g_string_free(str, TRUE); } /* FIXME: use accessor functions instead */ switch(data->job->type) { case FM_FILE_OP_MOVE: title = _("Moving files"); break; case FM_FILE_OP_COPY: title = _("Copying files"); break; case FM_FILE_OP_TRASH: title = _("Trashing files"); break; case FM_FILE_OP_DELETE: title = _("Deleting files"); break; case FM_FILE_OP_LINK: title = _("Creating symlinks"); break; case FM_FILE_OP_CHANGE_ATTR: title = _("Changing file attributes"); break; } if(title) { gtk_window_set_title(GTK_WINDOW(data->dlg), title); gtk_label_set_text(GTK_LABEL(data->act), title); } if(dest = fm_file_ops_job_get_dest(data->job)) { char* dest_str = fm_path_display_name(dest, TRUE); gtk_label_set_text(GTK_LABEL(to), dest_str); g_free(dest_str); } else { gtk_widget_destroy(data->dest); gtk_widget_destroy(to_label); } gtk_window_present(GTK_WINDOW(data->dlg)); data->update_timeout = g_timeout_add(500, (GSourceFunc)on_update_dlg, data); data->delay_timeout = 0; return FALSE; } void ensure_dlg(FmProgressDisplay* data) { if(data->delay_timeout) { g_source_remove(data->delay_timeout); data->delay_timeout = 0; } if(!data->dlg) on_show_dlg(data); } static void on_prepared(FmFileOpsJob* job, FmProgressDisplay* data) { data->timer = g_timer_new(); } /* Run the file operation job with a progress dialog. * The returned data structure will be freed in idle handler automatically * when it's not needed anymore. */ FmProgressDisplay* fm_file_ops_job_run_with_progress(FmFileOpsJob* job) { FmProgressDisplay* data = g_slice_new0(FmProgressDisplay); data->job = (FmFileOpsJob*)g_object_ref(job); data->delay_timeout = g_timeout_add(SHOW_DLG_DELAY, (GSourceFunc)on_show_dlg, data); g_signal_connect(job, "ask", G_CALLBACK(on_ask), data); g_signal_connect(job, "ask-rename", G_CALLBACK(on_ask_rename), data); g_signal_connect(job, "error", G_CALLBACK(on_error), data); g_signal_connect(job, "prepared", G_CALLBACK(on_prepared), data); g_signal_connect(job, "cur-file", G_CALLBACK(on_cur_file), data); g_signal_connect(job, "percent", G_CALLBACK(on_percent), data); g_signal_connect(job, "finished", G_CALLBACK(on_finished), data); g_signal_connect(job, "cancelled", G_CALLBACK(on_cancelled), data); fm_job_run_async(job); return data; } void fm_progress_display_destroy(FmProgressDisplay* data) { if(data->job) { fm_job_cancel(FM_JOB(data->job)); g_signal_handlers_disconnect_by_func(data->job, on_ask, data); g_signal_handlers_disconnect_by_func(data->job, on_ask_rename, data); g_signal_handlers_disconnect_by_func(data->job, on_error, data); g_signal_handlers_disconnect_by_func(data->job, on_cur_file, data); g_signal_handlers_disconnect_by_func(data->job, on_percent, data); g_signal_handlers_disconnect_by_func(data->job, on_finished, data); g_object_unref(data->job); if(data->timer) g_timer_destroy(data->timer); } g_free(data->cur_file); if(data->delay_timeout) g_source_remove(data->delay_timeout); if(data->update_timeout) g_source_remove(data->update_timeout); if(data->dlg) { g_object_unref(data->error_buf); g_object_unref(data->bold_tag); gtk_widget_destroy(data->dlg); } g_slice_free(FmProgressDisplay, data); }