diff options
author | Alexandru Pandelea <alexandru.pandelea@gmail.com> | 2016-08-27 19:34:08 +0300 |
---|---|---|
committer | Alexandru Pandelea <alexandru.pandelea@gmail.com> | 2016-08-29 13:02:50 +0300 |
commit | be12a7510090b2ec38229b6e86bc601800d2056b (patch) | |
tree | cac557d7d2ca16476d8d803eb80e5c1beba1f54b /src | |
parent | d2961830106abe6c6576207f90e2d968b554a115 (diff) | |
download | nautilus-be12a7510090b2ec38229b6e86bc601800d2056b.tar.gz |
Implement batch renaming
Renaming multiple files at once has been a missing feature in Nautilus
for a long time. This patch implements that feature in the following way:
This operation is launched in the same way as the rename one, when the
selection has more than one file.
When the batch renaming is launched, a dialog is shown, offering two
modes.
In the first mode, the user can use metadata (if available), numbering and
original file name tags to create the new names. Between the tags, there
also can be written normal text, which will be added in the new names. If
numbering is used, the order of the files can be modified by using several
criteria.
In the second mode, the user can replace an existing part of the name.
https://bugzilla.gnome.org/show_bug.cgi?id=768311
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 4 | ||||
-rw-r--r-- | src/nautilus-batch-rename-dialog.c | 3027 | ||||
-rw-r--r-- | src/nautilus-batch-rename-dialog.h | 100 | ||||
-rw-r--r-- | src/nautilus-batch-rename-utilities.c | 1050 | ||||
-rw-r--r-- | src/nautilus-batch-rename-utilities.h | 63 | ||||
-rw-r--r-- | src/nautilus-file-private.h | 3 | ||||
-rw-r--r-- | src/nautilus-file-undo-operations.c | 286 | ||||
-rw-r--r-- | src/nautilus-file-undo-operations.h | 29 | ||||
-rw-r--r-- | src/nautilus-file-utilities.c | 16 | ||||
-rw-r--r-- | src/nautilus-file-utilities.h | 2 | ||||
-rw-r--r-- | src/nautilus-file.c | 433 | ||||
-rw-r--r-- | src/nautilus-file.h | 4 | ||||
-rw-r--r-- | src/nautilus-files-view.c | 26 | ||||
-rw-r--r-- | src/resources/css/Adwaita.css | 16 | ||||
-rw-r--r-- | src/resources/nautilus.gresource.xml | 1 | ||||
-rw-r--r-- | src/resources/ui/nautilus-batch-rename-dialog.ui | 502 | ||||
-rw-r--r-- | src/resources/ui/nautilus-files-view-context-menus.ui | 2 |
17 files changed, 5464 insertions, 100 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index c925f4129..99edf8fa0 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -161,6 +161,8 @@ nautilus_no_main_sources = \ gtk/nautilusgtkplacesviewrowprivate.h \ nautilus-application.c \ nautilus-application.h \ + nautilus-batch-rename-dialog.c \ + nautilus-batch-rename-dialog.h \ nautilus-bookmark-list.c \ nautilus-bookmark-list.h \ nautilus-canvas-view.c \ @@ -214,6 +216,8 @@ nautilus_no_main_sources = \ nautilus-properties-window.h \ nautilus-query-editor.c \ nautilus-query-editor.h \ + nautilus-batch-rename-utilities.c \ + nautilus-batch-rename-utilities.h \ nautilus-search-popover.c \ nautilus-search-popover.h \ nautilus-self-check-functions.c \ diff --git a/src/nautilus-batch-rename-dialog.c b/src/nautilus-batch-rename-dialog.c new file mode 100644 index 000000000..eb76a7387 --- /dev/null +++ b/src/nautilus-batch-rename-dialog.c @@ -0,0 +1,3027 @@ +/* nautilus-batch-rename-dialog.c + * + * Copyright (C) 2016 Alexandru Pandelea <alexandru.pandelea@gmail.com> + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#include <config.h> + +#include "nautilus-batch-rename-dialog.h" +#include "nautilus-file.h" +#include "nautilus-error-reporting.h" +#include "nautilus-batch-rename-utilities.h" + +#include <glib/gprintf.h> +#include <glib.h> +#include <string.h> +#include <glib/gi18n.h> + +struct _NautilusBatchRenameDialog +{ + GtkDialog parent; + + GtkWidget *grid; + NautilusWindow *window; + + GtkWidget *cancel_button; + GtkWidget *original_name_listbox; + GtkWidget *arrow_listbox; + GtkWidget *result_listbox; + GtkWidget *name_entry; + GtkWidget *rename_button; + GtkWidget *find_entry; + GtkWidget *mode_stack; + GtkWidget *replace_entry; + GtkWidget *format_mode_button; + GtkWidget *replace_mode_button; + GtkWidget *add_button; + GtkWidget *add_popover; + GtkWidget *numbering_order_label; + GtkWidget *numbering_label; + GtkWidget *scrolled_window; + GtkWidget *numbering_order_popover; + GtkWidget *numbering_order_button; + GtkWidget *conflict_box; + GtkWidget *conflict_label; + GtkWidget *conflict_down; + GtkWidget *conflict_up; + + GList *original_name_listbox_rows; + GList *arrow_listbox_rows; + GList *result_listbox_rows; + GList *listbox_labels_new; + GList *listbox_labels_old; + GList *listbox_icons; + GtkSizeGroup *size_group; + + GList *selection; + GList *new_names; + NautilusBatchRenameDialogMode mode; + NautilusDirectory *directory; + + GActionGroup *action_group; + + GMenu *numbering_order_menu; + GMenu *add_tag_menu; + + GHashTable *create_date; + GList *selection_metadata; + + /* check if all files in selection have the same parent */ + gboolean same_parent; + /* the index of the currently selected conflict */ + gint selected_conflict; + /* total conflicts number */ + gint conflicts_number; + + gint checked_parents; + GList *duplicates; + GList *distinct_parents; + GTask *conflicts_task; + GCancellable *conflict_cancellable; + gboolean checking_conflicts; + + /* this hash table has information about the status + * of all tags: availability, if it's currently used + * and position */ + GHashTable *tag_info_table; + + GtkWidget *preselected_row1; + GtkWidget *preselected_row2; + + gint row_height; + gboolean rename_clicked; + + /* the numbers of characters from the name entry */ + gint name_entry_characters; + gboolean tags_deleted; + gint cursor_position; + gboolean use_manual_cursor_position; +}; + +typedef struct +{ + gboolean available; + gboolean set; + gint position; + gint original_position; + gint new_position; + /* if the tag was just added, then we shouldn't update it's position */ + gboolean just_added; +} TagData; + +static void update_display_text (NautilusBatchRenameDialog *dialog); + +G_DEFINE_TYPE (NautilusBatchRenameDialog, nautilus_batch_rename_dialog, GTK_TYPE_DIALOG); + +static void +add_numbering_order (GSimpleAction *action, + GVariant *value, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + const gchar *target_name; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + target_name = g_variant_get_string (value, NULL); + + if (g_strcmp0 (target_name, "name-ascending") == 0) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_order_label), + _("Original name (Ascending)")); + dialog->selection = nautilus_batch_rename_dialog_sort (dialog->selection, + ORIGINAL_ASCENDING, + NULL); + } + + if (g_strcmp0 (target_name, "name-descending") == 0) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_order_label), + _("Original name (Descending)")); + dialog->selection = nautilus_batch_rename_dialog_sort (dialog->selection, + ORIGINAL_DESCENDING, + NULL); + } + + if (g_strcmp0 (target_name, "first-modified") == 0) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_order_label), + _("First Modified")); + dialog->selection = nautilus_batch_rename_dialog_sort (dialog->selection, + FIRST_MODIFIED, + NULL); + } + + if (g_strcmp0 (target_name, "last-modified") == 0) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_order_label), + _("Last Modified")); + dialog->selection = nautilus_batch_rename_dialog_sort (dialog->selection, + LAST_MODIFIED, + NULL); + } + + if (g_strcmp0 (target_name, "first-created") == 0) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_order_label), + _("First Created")); + dialog->selection = nautilus_batch_rename_dialog_sort (dialog->selection, + FIRST_CREATED, + dialog->create_date); + } + + if (g_strcmp0 (target_name, "last-created") == 0) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_order_label), + _("Last Created")); + dialog->selection = nautilus_batch_rename_dialog_sort (dialog->selection, + LAST_CREATED, + dialog->create_date); + } + + g_simple_action_set_state (action, value); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (dialog->numbering_order_button), FALSE); + + update_display_text (dialog); +} + +static void +add_original_file_name_tag (GSimpleAction *action, + GVariant *value, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + gint cursor_position; + TagData *tag_data; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + g_object_get (dialog->name_entry, "cursor-position", &cursor_position, NULL); + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ORIGINAL_FILE_NAME); + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + ORIGINAL_FILE_NAME, + g_utf8_strlen (ORIGINAL_FILE_NAME, -1), + &cursor_position); + + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (dialog->name_entry)); + + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE); +} + +static void +disable_action (NautilusBatchRenameDialog *dialog, + gchar *action_name) +{ + GAction *action; + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + action_name); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE); +} + +static void +add_metadata_tag (GSimpleAction *action, + GVariant *value, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + const gchar *action_name; + gint cursor_position; + TagData *tag_data; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + action_name = g_action_get_name (G_ACTION (action)); + g_object_get (dialog->name_entry, "cursor-position", &cursor_position, NULL); + + if (g_strrstr (action_name, "creation-date")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, CREATION_DATE); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + CREATION_DATE, + g_utf8_strlen (CREATION_DATE, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-creation-date-tag"); + } + + if (g_strrstr (action_name, "equipment")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, CAMERA_MODEL); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + CAMERA_MODEL, + g_utf8_strlen (CAMERA_MODEL, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-equipment-tag"); + } + + if (g_strrstr (action_name, "season")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, SEASON_NUMBER); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + SEASON_NUMBER, + g_utf8_strlen (SEASON_NUMBER, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-season-tag"); + } + + if (g_strrstr (action_name, "episode")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, EPISODE_NUMBER); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + EPISODE_NUMBER, + g_utf8_strlen (EPISODE_NUMBER, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-episode-tag"); + } + + if (g_strrstr (action_name, "track")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, TRACK_NUMBER); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + TRACK_NUMBER, + g_utf8_strlen (TRACK_NUMBER, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-track-number-tag"); + } + + if (g_strrstr (action_name, "artist")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, ARTIST_NAME); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + ARTIST_NAME, + g_utf8_strlen (ARTIST_NAME, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-artist-name-tag"); + } + + if (g_strrstr (action_name, "title")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, TITLE); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + TITLE, + g_utf8_strlen (TITLE, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-title-tag"); + } + + if (g_strrstr (action_name, "album")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, ALBUM_NAME); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + ALBUM_NAME, + g_utf8_strlen (ALBUM_NAME, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + disable_action (dialog, "add-album-name-tag"); + } + + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (dialog->name_entry)); +} + +static void +add_numbering_tag (GSimpleAction *action, + GVariant *value, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + const gchar *action_name; + gint cursor_position; + GAction *add_numbering_action; + TagData *tag_data; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + action_name = g_action_get_name (G_ACTION (action)); + g_object_get (dialog->name_entry, "cursor-position", &cursor_position, NULL); + + if (g_strrstr (action_name, "zero")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING); + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + NUMBERING, + g_utf8_strlen (NUMBERING, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + } + + if (g_strrstr (action_name, "one")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING0); + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + NUMBERING0, + g_utf8_strlen (NUMBERING0, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + } + + if (g_strrstr (action_name, "two")) { + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING00); + tag_data->set = TRUE; + tag_data->just_added = TRUE; + tag_data->position = cursor_position; + + gtk_editable_insert_text (GTK_EDITABLE (dialog->name_entry), + NUMBERING00, + g_utf8_strlen (NUMBERING00, -1), + &cursor_position); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), cursor_position); + } + + add_numbering_action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-zero"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (add_numbering_action), FALSE); + add_numbering_action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-one"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (add_numbering_action), FALSE); + + add_numbering_action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-two"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (add_numbering_action), FALSE); + + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (dialog->name_entry)); +} + +const GActionEntry dialog_entries[] = { + { "numbering-order-changed", NULL, "s", "'name-ascending'", add_numbering_order }, + { "add-original-file-name-tag", add_original_file_name_tag }, + { "add-numbering-tag-zero", add_numbering_tag }, + { "add-numbering-tag-one", add_numbering_tag }, + { "add-numbering-tag-two", add_numbering_tag }, + { "add-creation-date-tag", add_metadata_tag }, + { "add-equipment-tag", add_metadata_tag }, + { "add-season-tag", add_metadata_tag }, + { "add-episode-tag", add_metadata_tag }, + { "add-video-album-tag", add_metadata_tag }, + { "add-track-number-tag", add_metadata_tag }, + { "add-artist-name-tag", add_metadata_tag }, + { "add-title-tag", add_metadata_tag }, + { "add-album-name-tag", add_metadata_tag }, + +}; + +static void +row_selected (GtkListBox *box, + GtkListBoxRow *listbox_row, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + GtkListBoxRow *row; + gint index; + + if (!GTK_IS_LIST_BOX_ROW (listbox_row)) + return; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + index = gtk_list_box_row_get_index (listbox_row); + + if (GTK_WIDGET (box) == dialog->original_name_listbox) { + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->arrow_listbox), index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->arrow_listbox), + row); + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->result_listbox), index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->result_listbox), + row); + } + + if (GTK_WIDGET (box) == dialog->arrow_listbox) { + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->original_name_listbox), index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->original_name_listbox), + row); + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->result_listbox), index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->result_listbox), + row); + } + + if (GTK_WIDGET (box) == dialog->result_listbox) { + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->arrow_listbox), index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->arrow_listbox), + row); + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->original_name_listbox), index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->original_name_listbox), + row); + } +} + +static gint +compare_tag_position (gconstpointer a, + gconstpointer b) +{ + int *number1 = (int*) a; + int *number2 = (int*) b; + + return *number1 - *number2; +} + +/* This function splits the entry text into a list of regular text and tags. + * For instance, "[1, 2, 3]Paris[Creation date]" would result in: + * "[1, 2, 3]", "Paris", "[Creation date]" */ +static GList* +split_entry_text (NautilusBatchRenameDialog *dialog, + gchar *entry_text) +{ + GString *normal_text; + GString *tag; + GArray *tag_positions; + gint tags; + gint i; + gchar *substring; + gint tag_end_position; + GList *result = NULL; + TagData *tag_data; + + tags = 0; + tag_end_position = 0; + tag_positions = g_array_new (FALSE, FALSE, sizeof (gint)); + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING0); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING00); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ORIGINAL_FILE_NAME); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CREATION_DATE); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CAMERA_MODEL); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, SEASON_NUMBER); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, EPISODE_NUMBER); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TRACK_NUMBER); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ARTIST_NAME); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TITLE); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ALBUM_NAME); + if (tag_data->set) { + g_array_append_val (tag_positions, tag_data->position); + tags++; + } + + g_array_sort (tag_positions, compare_tag_position); + + for (i = 0; i < tags; i++) { + tag = g_string_new (""); + + substring = g_utf8_substring (entry_text, tag_end_position, + g_array_index (tag_positions, gint, i)); + normal_text = g_string_new (substring); + g_free (substring); + + if (g_strcmp0 (normal_text->str, "")) + result = g_list_prepend (result, normal_text); + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ORIGINAL_FILE_NAME); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (ORIGINAL_FILE_NAME, -1); + tag = g_string_append (tag, ORIGINAL_FILE_NAME); + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (NUMBERING, -1); + tag = g_string_append (tag, NUMBERING); + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING0); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (NUMBERING0, -1); + tag = g_string_append (tag, NUMBERING0); + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING00); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (NUMBERING00, -1); + tag = g_string_append (tag, NUMBERING00); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, CREATION_DATE); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (CREATION_DATE, -1); + tag = g_string_append (tag, CREATION_DATE); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, CAMERA_MODEL); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (CAMERA_MODEL, -1); + tag = g_string_append (tag, CAMERA_MODEL); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, SEASON_NUMBER); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (SEASON_NUMBER, -1); + tag = g_string_append (tag, SEASON_NUMBER); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, EPISODE_NUMBER); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (EPISODE_NUMBER, -1); + tag = g_string_append (tag, EPISODE_NUMBER); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, TRACK_NUMBER); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (TRACK_NUMBER, -1); + tag = g_string_append (tag, TRACK_NUMBER); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, ARTIST_NAME); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (ARTIST_NAME, -1); + tag = g_string_append (tag, ARTIST_NAME); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, TITLE); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (TITLE, -1); + tag = g_string_append (tag, TITLE); + } + tag_data = g_hash_table_lookup (dialog->tag_info_table, ALBUM_NAME); + if (tag_data->set && g_array_index (tag_positions, gint, i) == tag_data->position) { + tag_end_position = g_array_index (tag_positions, gint, i) + + g_utf8_strlen (ALBUM_NAME, -1); + tag = g_string_append (tag, ALBUM_NAME); + } + + result = g_list_prepend (result, tag); + } + normal_text = g_string_new (g_utf8_offset_to_pointer (entry_text, tag_end_position)); + + if (g_strcmp0 (normal_text->str, "") != 0) + result = g_list_prepend (result, normal_text); + + result = g_list_reverse (result); + + g_array_free (tag_positions, TRUE); + return result; +} + +static GList* +batch_rename_dialog_get_new_names (NautilusBatchRenameDialog *dialog) +{ + GList *result = NULL; + GList *selection; + GList *text_chunks; + g_autofree gchar *entry_text; + g_autofree gchar *replace_text; + + selection = dialog->selection; + text_chunks = NULL; + + if (dialog->mode == NAUTILUS_BATCH_RENAME_DIALOG_REPLACE) + entry_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->find_entry))); + else + entry_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->name_entry))); + + replace_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (dialog->replace_entry))); + + if (dialog->mode == NAUTILUS_BATCH_RENAME_DIALOG_REPLACE) { + result = batch_rename_dialog_get_new_names_list (dialog->mode, + selection, + NULL, + NULL, + entry_text, + replace_text); + } else { + text_chunks = split_entry_text (dialog, entry_text); + + result = batch_rename_dialog_get_new_names_list (dialog->mode, + selection, + text_chunks, + dialog->selection_metadata, + entry_text, + replace_text); + g_list_free_full (text_chunks, string_free); + } + + result = g_list_reverse (result); + + return result; +} + +static void +begin_batch_rename (NautilusBatchRenameDialog *dialog, + GList *new_names) +{ + GList *new_names_list; + GList *files; + GList *files2; + GList *new_names_list2; + gchar *file_name; + gchar *old_file_name; + GString *new_file_name; + GString *new_name; + NautilusFile *file; + + /* in the following case: + * file1 -> file2 + * file2 -> file3 + * file2 must be renamed first, so because of that, the list has to be reordered */ + for (new_names_list = new_names, files = dialog->selection; + new_names_list != NULL && files != NULL; + new_names_list = new_names_list->next, files = files->next) { + old_file_name = nautilus_file_get_name (NAUTILUS_FILE (files->data)); + new_file_name = new_names_list->data; + + for (files2 = dialog->selection, new_names_list2 = new_names; + files2 != NULL && new_names_list2 != NULL; + files2 = files2->next, new_names_list2 = new_names_list2->next) { + file_name = nautilus_file_get_name (NAUTILUS_FILE (files2->data)); + if (files2 != files && g_strcmp0 (file_name, new_file_name->str) == 0) { + file = NAUTILUS_FILE (files2->data); + new_name = new_names_list2->data; + + dialog->selection = g_list_remove_link (dialog->selection, files2); + new_names = g_list_remove_link (new_names, new_names_list2); + + dialog->selection = g_list_prepend (dialog->selection, file); + new_names = g_list_prepend (new_names, new_name); + + g_free (file_name); + + break; + } + + g_free (file_name); + } + + g_free (old_file_name); + } + + /* do the actual rename here */ + nautilus_file_batch_rename (dialog->selection, new_names, NULL, NULL); + + gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (dialog->window)), NULL); +} + +static void +listbox_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + NautilusBatchRenameDialog *dialog) +{ + gboolean show_separator; + + show_separator = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), + "show-separator")); + + if (show_separator) + { + GtkWidget *separator; + + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show (separator); + + gtk_list_box_row_set_header (row, separator); + } +} + +/* This is manually done instead of using GtkSizeGroup because of the computational + * complexity of the later.*/ +static void +update_rows_height (NautilusBatchRenameDialog *dialog) +{ + GList *l; + gint current_row_natural_height; + gint maximum_height; + + maximum_height = -1; + + /* check if maximum height has changed */ + for (l = dialog->listbox_labels_new; l != NULL; l = l->next) { + gtk_widget_get_preferred_height (GTK_WIDGET (l->data), + NULL, + ¤t_row_natural_height); + + if (current_row_natural_height > maximum_height) { + maximum_height = current_row_natural_height; + } + } + + for (l = dialog->listbox_labels_old; l != NULL; l = l->next) { + gtk_widget_get_preferred_height (GTK_WIDGET (l->data), + NULL, + ¤t_row_natural_height); + + if (current_row_natural_height > maximum_height) { + maximum_height = current_row_natural_height; + } + } + + for (l = dialog->listbox_icons; l != NULL; l = l->next) { + gtk_widget_get_preferred_height (GTK_WIDGET (l->data), + NULL, + ¤t_row_natural_height); + + if (current_row_natural_height > maximum_height) { + maximum_height = current_row_natural_height; + } + } + + if (maximum_height != dialog->row_height) { + dialog->row_height = maximum_height; + + for (l = dialog->listbox_icons; l != NULL; l = l->next) { + g_object_set (G_OBJECT (l->data), "height-request", dialog->row_height, NULL); + } + + for (l = dialog->listbox_labels_new; l != NULL; l = l->next) { + g_object_set (G_OBJECT (l->data), "height-request", dialog->row_height, NULL); + } + + for (l = dialog->listbox_labels_old; l != NULL; l = l->next) { + g_object_set (G_OBJECT (l->data), "height-request", dialog->row_height, NULL); + } + } +} + +static GtkWidget* +create_original_name_row_for_label (NautilusBatchRenameDialog *dialog, + const gchar *old_text, + gboolean show_separator) +{ + GtkWidget *row; + GtkWidget *label_old; + + row = gtk_list_box_row_new (); + + g_object_set_data (G_OBJECT (row), "show-separator", GINT_TO_POINTER (show_separator)); + + label_old = gtk_label_new (old_text); + gtk_label_set_xalign (GTK_LABEL (label_old), 0.0); + gtk_widget_set_hexpand (label_old, TRUE); + gtk_widget_set_margin_start (label_old, 6); + + gtk_label_set_ellipsize (GTK_LABEL (label_old), PANGO_ELLIPSIZE_END); + + dialog->listbox_labels_old = g_list_prepend (dialog->listbox_labels_old, label_old); + + gtk_container_add (GTK_CONTAINER (row), label_old); + gtk_widget_show_all (row); + + return row; +} + +static GtkWidget* +create_result_row_for_label (NautilusBatchRenameDialog *dialog, + const gchar *new_text, + gboolean show_separator) +{ + GtkWidget *row; + GtkWidget *label_new; + + row = gtk_list_box_row_new (); + + g_object_set_data (G_OBJECT (row), "show-separator", GINT_TO_POINTER (show_separator)); + + label_new = gtk_label_new (new_text); + gtk_label_set_xalign (GTK_LABEL (label_new), 0.0); + gtk_widget_set_hexpand (label_new, TRUE); + gtk_widget_set_margin_start (label_new, 6); + + gtk_label_set_ellipsize (GTK_LABEL (label_new), PANGO_ELLIPSIZE_END); + + dialog->listbox_labels_new = g_list_prepend (dialog->listbox_labels_new, label_new); + + gtk_container_add (GTK_CONTAINER (row), label_new); + gtk_widget_show_all (row); + + return row; +} + +static GtkWidget* +create_arrow_row_for_label (NautilusBatchRenameDialog *dialog, + gboolean show_separator) +{ + GtkWidget *row; + GtkWidget *icon; + + row = gtk_list_box_row_new (); + + g_object_set_data (G_OBJECT (row), "show-separator", GINT_TO_POINTER (show_separator)); + + icon = gtk_label_new ("→"); + gtk_label_set_xalign (GTK_LABEL (icon), 1.0); + gtk_widget_set_hexpand (icon, FALSE); + gtk_widget_set_margin_start (icon, 6); + + dialog->listbox_icons = g_list_prepend (dialog->listbox_icons, icon); + + gtk_container_add (GTK_CONTAINER (row), icon); + gtk_widget_show_all (row); + + return row; +} + +static void +prepare_batch_rename (NautilusBatchRenameDialog *dialog) +{ + GdkCursor *cursor; + GdkDisplay *display; + + /* wait for checking conflicts to finish, to be sure that + * the rename can actually take place */ + if (dialog->checking_conflicts) { + dialog->rename_clicked = TRUE; + return; + } + + if (!gtk_widget_is_sensitive (dialog->rename_button)) + return; + + display = gtk_widget_get_display (GTK_WIDGET (dialog->window)); + cursor = gdk_cursor_new_from_name (display, "progress"); + gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (dialog->window)), + cursor); + g_object_unref (cursor); + + display = gtk_widget_get_display (GTK_WIDGET (dialog)); + cursor = gdk_cursor_new_from_name (display, "progress"); + gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (dialog)), + cursor); + g_object_unref (cursor); + + gtk_widget_hide (GTK_WIDGET (dialog)); + begin_batch_rename (dialog, dialog->new_names); + + if (dialog->conflict_cancellable) + g_cancellable_cancel (dialog->conflict_cancellable); + + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +batch_rename_dialog_on_response (NautilusBatchRenameDialog *dialog, + gint response_id, + gpointer user_data) +{ + if (response_id == GTK_RESPONSE_OK) { + prepare_batch_rename (dialog); + } else { + if (dialog->conflict_cancellable) + g_cancellable_cancel (dialog->conflict_cancellable); + + gtk_widget_destroy (GTK_WIDGET (dialog)); + } +} + +static void +fill_display_listbox (NautilusBatchRenameDialog *dialog) +{ + GtkWidget *row; + GList *l1; + GList *l2; + NautilusFile *file; + GString *new_name; + gchar *name; + + dialog->original_name_listbox_rows = NULL; + dialog->arrow_listbox_rows = NULL; + dialog->result_listbox_rows = NULL; + + gtk_size_group_add_widget (dialog->size_group, dialog->result_listbox); + gtk_size_group_add_widget (dialog->size_group, dialog->original_name_listbox); + + for (l1 = dialog->new_names, l2 = dialog->selection; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + file = NAUTILUS_FILE (l2->data); + new_name = l1->data; + + name = nautilus_file_get_name (file); + row = create_original_name_row_for_label (dialog, name, TRUE); + gtk_container_add (GTK_CONTAINER (dialog->original_name_listbox), row); + dialog->original_name_listbox_rows = g_list_prepend (dialog->original_name_listbox_rows, + row); + + row = create_arrow_row_for_label (dialog, TRUE); + gtk_container_add (GTK_CONTAINER (dialog->arrow_listbox), row); + dialog->arrow_listbox_rows = g_list_prepend (dialog->arrow_listbox_rows, + row); + + row = create_result_row_for_label (dialog, new_name->str, TRUE); + gtk_container_add (GTK_CONTAINER (dialog->result_listbox), row); + dialog->result_listbox_rows = g_list_prepend (dialog->result_listbox_rows, + row); + + g_free (name); + } + + dialog->original_name_listbox_rows = g_list_reverse (dialog->original_name_listbox_rows); + dialog->arrow_listbox_rows = g_list_reverse (dialog->arrow_listbox_rows); + dialog->result_listbox_rows = g_list_reverse (dialog->result_listbox_rows); + dialog->listbox_labels_old = g_list_reverse (dialog->listbox_labels_old); + dialog->listbox_labels_new = g_list_reverse (dialog->listbox_labels_new); + dialog->listbox_icons = g_list_reverse (dialog->listbox_icons); +} + +static void +select_nth_conflict (NautilusBatchRenameDialog *dialog) +{ + GList *l; + GString *conflict_file_name; + GString *display_text; + GString *new_name; + gint nth_conflict_index; + gint nth_conflict; + gint name_occurences; + GtkAdjustment *adjustment; + GtkAllocation allocation; + ConflictData *conflict_data; + + nth_conflict = dialog->selected_conflict; + l = g_list_nth (dialog->duplicates, nth_conflict); + conflict_data = l->data; + + /* the conflict that has to be selected */ + conflict_file_name = g_string_new (conflict_data->name); + display_text = g_string_new (""); + + nth_conflict_index = conflict_data->index; + + l = g_list_nth (dialog->original_name_listbox_rows, nth_conflict_index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->original_name_listbox), + l->data); + + l = g_list_nth (dialog->arrow_listbox_rows, nth_conflict_index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->arrow_listbox), + l->data); + + l = g_list_nth (dialog->result_listbox_rows, nth_conflict_index); + gtk_list_box_select_row (GTK_LIST_BOX (dialog->result_listbox), + l->data); + + /* scroll to the selected row */ + adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (dialog->scrolled_window)); + gtk_widget_get_allocation (GTK_WIDGET (l->data), &allocation); + gtk_adjustment_set_value (adjustment, (allocation.height + 1) * nth_conflict_index); + + name_occurences = 0; + for (l = dialog->new_names; l != NULL; l = l->next) { + new_name = l->data; + if (g_string_equal (new_name, conflict_file_name)) + name_occurences++; + } + if (name_occurences > 1) + g_string_append_printf (display_text, + _("\"%s\" would not be a unique new name"), + conflict_file_name->str); + else + g_string_append_printf (display_text, + _("\"%s\" would conflict with an existing file."), + conflict_file_name->str); + + gtk_label_set_label (GTK_LABEL (dialog->conflict_label), + display_text->str); + + g_string_free (conflict_file_name, TRUE); +} + +static void +select_next_conflict_down (NautilusBatchRenameDialog *dialog) +{ + dialog->selected_conflict++; + + if (dialog->selected_conflict == 1) + gtk_widget_set_sensitive (dialog->conflict_up, TRUE); + + if (dialog->selected_conflict == dialog->conflicts_number - 1) + gtk_widget_set_sensitive (dialog->conflict_down, FALSE); + + select_nth_conflict (dialog); +} + +static void +select_next_conflict_up (NautilusBatchRenameDialog *dialog) +{ + dialog->selected_conflict--; + + if (dialog->selected_conflict == 0) + gtk_widget_set_sensitive (dialog->conflict_up, FALSE); + + if (dialog->selected_conflict == dialog->conflicts_number - 2) + gtk_widget_set_sensitive (dialog->conflict_down, TRUE); + + select_nth_conflict (dialog); +} + +static void +update_conflict_row_background (NautilusBatchRenameDialog *dialog) +{ + GList *l1; + GList *l2; + GList *l3; + GList *duplicates; + gint index; + GtkStyleContext *context; + ConflictData *conflict_data; + + index = 0; + + duplicates = dialog->duplicates; + + for (l1 = dialog->original_name_listbox_rows, + l2 = dialog->arrow_listbox_rows, + l3 = dialog->result_listbox_rows; + l1 != NULL && l2 != NULL && l3 != NULL; + l1 = l1->next, l2 = l2->next, l3 = l3->next) { + context = gtk_widget_get_style_context (GTK_WIDGET (l1->data)); + + if (gtk_style_context_has_class (context, "conflict-row")) { + gtk_style_context_remove_class (context, "conflict-row"); + + context = gtk_widget_get_style_context (GTK_WIDGET (l2->data)); + gtk_style_context_remove_class (context, "conflict-row"); + + context = gtk_widget_get_style_context (GTK_WIDGET (l3->data)); + gtk_style_context_remove_class (context, "conflict-row"); + + } + + if (duplicates != NULL) { + conflict_data = duplicates->data; + if (conflict_data->index == index) { + context = gtk_widget_get_style_context (GTK_WIDGET (l1->data)); + gtk_style_context_add_class (context, "conflict-row"); + + context = gtk_widget_get_style_context (GTK_WIDGET (l2->data)); + gtk_style_context_add_class (context, "conflict-row"); + + context = gtk_widget_get_style_context (GTK_WIDGET (l3->data)); + gtk_style_context_add_class (context, "conflict-row"); + + duplicates = duplicates->next; + } + } + index++; + } +} + +static void +update_listbox (NautilusBatchRenameDialog *dialog) +{ + GList *l1; + GList *l2; + NautilusFile *file; + gchar *old_name; + GtkLabel *label; + GString *new_name; + + for (l1 = dialog->new_names, l2 = dialog->listbox_labels_new; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + label = GTK_LABEL (l2->data); + new_name = l1->data; + + gtk_label_set_label (label, new_name->str); + } + + for (l1 = dialog->selection, l2 = dialog->listbox_labels_old; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + label = GTK_LABEL (l2->data); + file = NAUTILUS_FILE (l1->data); + + old_name = nautilus_file_get_name (file); + + if (dialog->mode == NAUTILUS_BATCH_RENAME_DIALOG_FORMAT) { + gtk_label_set_label (label, old_name); + } else { + new_name = batch_rename_replace_label_text (old_name, + gtk_entry_get_text (GTK_ENTRY (dialog->find_entry))); + gtk_label_set_markup (GTK_LABEL (label), new_name->str); + + g_string_free (new_name, TRUE); + } + + g_free (old_name); + } + + update_rows_height (dialog); + + /* check if there are name conflicts and display them if they exist */ + if (dialog->duplicates != NULL) { + update_conflict_row_background (dialog); + + gtk_widget_set_sensitive (dialog->rename_button, FALSE); + + gtk_widget_show (dialog->conflict_box); + + dialog->selected_conflict = 0; + dialog->conflicts_number = g_list_length (dialog->duplicates); + + select_nth_conflict (dialog); + + gtk_widget_set_sensitive (dialog->conflict_up, FALSE); + + if (g_list_length (dialog->duplicates) == 1) + gtk_widget_set_sensitive (dialog->conflict_down, FALSE); + else + gtk_widget_set_sensitive (dialog->conflict_down, TRUE); + } else { + gtk_widget_hide (dialog->conflict_box); + + /* re-enable the rename button if there are no more name conflicts */ + if (dialog->duplicates == NULL && !gtk_widget_is_sensitive (dialog->rename_button)) { + update_conflict_row_background (dialog); + gtk_widget_set_sensitive (dialog->rename_button, TRUE); + } + } + + /* if the rename button was clicked and there's no conflict, then start renaming */ + if (dialog->rename_clicked && dialog->duplicates == NULL) { + prepare_batch_rename (dialog); + } + + if (dialog->rename_clicked && dialog->duplicates != NULL) { + dialog->rename_clicked = FALSE; + } +} + + +void +check_conflict_for_files (NautilusBatchRenameDialog *dialog, + NautilusDirectory *directory, + GList *files) +{ + gchar *current_directory; + gchar *parent_uri; + gchar *name; + NautilusFile *file; + GString *new_name; + GString *file_name; + GList *l1, *l2; + GHashTable *directory_files_table; + GHashTable *new_names_table; + GHashTable *names_conflicts_table; + gboolean exists; + gboolean have_conflict; + gboolean tag_present; + gboolean same_parent_directory; + ConflictData *conflict_data; + + current_directory = nautilus_directory_get_uri (directory); + + directory_files_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + new_names_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + names_conflicts_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + + /* names_conflicts_table is used for knowing which names from the list are not unique, + * so that they can easily be reached when needed */ + for (l1 = dialog->new_names, l2 = dialog->selection; + l1 != NULL && l2 != NULL; + l1 = l1->next, l2 = l2->next) { + new_name = l1->data; + file = NAUTILUS_FILE (l2->data); + parent_uri = nautilus_file_get_parent_uri (file); + + tag_present = g_hash_table_lookup (new_names_table, new_name->str) != NULL; + same_parent_directory = g_strcmp0 (parent_uri, current_directory) == 0; + + if (same_parent_directory) { + if (!tag_present) { + g_hash_table_insert (new_names_table, + g_strdup (new_name->str), + nautilus_file_get_parent_uri (file)); + } else { + g_hash_table_insert (names_conflicts_table, + g_strdup (new_name->str), + nautilus_file_get_parent_uri (file)); + } + } + + g_free (parent_uri); + } + + for (l1 = files; l1 != NULL; l1 = l1->next) { + file = NAUTILUS_FILE (l1->data); + g_hash_table_insert (directory_files_table, + nautilus_file_get_name (file), + GINT_TO_POINTER (TRUE)); + } + + for (l1 = dialog->selection, l2 = dialog->new_names; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + file = NAUTILUS_FILE (l1->data); + + name = nautilus_file_get_name (file); + file_name = g_string_new (name); + g_free (name); + + parent_uri = nautilus_file_get_parent_uri (file); + + new_name = l2->data; + + have_conflict = FALSE; + + /* check for duplicate only if the parent of the current file is + * the current directory and the name of the file has changed */ + if (g_strcmp0 (parent_uri, current_directory) == 0 && + !g_string_equal (new_name, file_name)) { + exists = GPOINTER_TO_INT (g_hash_table_lookup (directory_files_table, new_name->str)); + + if (exists == TRUE && + !file_name_conflicts_with_results (dialog->selection, dialog->new_names, new_name, parent_uri)) { + conflict_data = g_new (ConflictData, 1); + conflict_data->name = g_strdup (new_name->str); + conflict_data->index = g_list_index (dialog->selection, l1->data); + dialog->duplicates = g_list_prepend (dialog->duplicates, + conflict_data); + + have_conflict = TRUE; + } + } + if (!have_conflict) { + tag_present = g_hash_table_lookup (names_conflicts_table, new_name->str) != NULL; + same_parent_directory = g_strcmp0 (nautilus_file_get_parent_uri (file), current_directory) == 0; + + if (tag_present && same_parent_directory) { + conflict_data = g_new (ConflictData, 1); + conflict_data->name = g_strdup (new_name->str); + conflict_data->index = g_list_index (dialog->selection, l1->data); + dialog->duplicates = g_list_prepend (dialog->duplicates, + conflict_data); + + have_conflict = TRUE; + } + } + + g_string_free (file_name, TRUE); + g_free (parent_uri); + } + + /* check if this is the last call of the callback. Update + * the listbox with the conflicts if it is. */ + if (dialog->checked_parents == g_list_length (dialog->distinct_parents) - 1) { + dialog->duplicates = g_list_reverse (dialog->duplicates); + + dialog->checking_conflicts = FALSE; + + update_listbox (dialog); + } + + dialog->checked_parents++; + + g_free (current_directory); + g_hash_table_destroy (directory_files_table); + g_hash_table_destroy (new_names_table); + g_hash_table_destroy (names_conflicts_table); +} + +static void +file_names_list_has_duplicates_callback (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (object); + + if (g_cancellable_is_cancelled (dialog->conflict_cancellable)) + return; + + if (dialog->same_parent) + update_listbox (dialog); +} + +static void +on_call_when_ready (NautilusDirectory *directory, + GList *files, + gpointer callback_data) +{ + check_conflict_for_files (NAUTILUS_BATCH_RENAME_DIALOG (callback_data), + directory, + files); +} + +static void +file_names_list_has_duplicates_async_thread (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + NautilusBatchRenameDialog *dialog; + GList *new_names; + GList *directory_files; + GList *l1; + GList *l2; + NautilusFile *file; + GString *file_name; + GString *new_name; + NautilusDirectory *parent; + gboolean have_conflict; + gboolean hash_table_insertion; + gchar *name; + GHashTable *directory_names_table; + GHashTable *new_names_table; + GHashTable *names_conflicts_table; + gint exists; + ConflictData *conflict_data; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (object); + + dialog->duplicates = NULL; + + if (g_cancellable_is_cancelled (cancellable)) + return; + + g_return_if_fail (g_list_length (dialog->new_names) == g_list_length (dialog->selection)); + + /* If the batch rename is launched in a search, then for each file we have to check for + * conflicts with each file in the file's parent directory */ + if (dialog->distinct_parents != NULL) { + for (l1 = dialog->distinct_parents; l1 != NULL; l1 = l1->next) { + if (g_cancellable_is_cancelled (cancellable)) + return; + + parent = nautilus_directory_get_by_uri (l1->data); + + nautilus_directory_call_when_ready (parent, + NAUTILUS_FILE_ATTRIBUTE_INFO, + TRUE, + on_call_when_ready, + dialog); + } + + g_task_return_pointer (task, object, NULL); + return; + } + + new_names = batch_rename_dialog_get_new_names (dialog); + + directory_names_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + new_names_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + names_conflicts_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + + directory_files = nautilus_directory_get_file_list (dialog->directory); + + for (l1 = new_names; l1 != NULL; l1 = l1->next) { + new_name = l1->data; + hash_table_insertion = g_hash_table_insert (new_names_table, + g_strdup (new_name->str), + GINT_TO_POINTER (TRUE)); + + if (!hash_table_insertion) { + g_hash_table_insert (names_conflicts_table, + g_strdup (new_name->str), + GINT_TO_POINTER (TRUE)); + } + } + + for (l1 = directory_files; l1 != NULL; l1 = l1->next) { + file = NAUTILUS_FILE (l1->data); + g_hash_table_insert (directory_names_table, + nautilus_file_get_name (file), + GINT_TO_POINTER (TRUE)); + } + + for (l1 = new_names, l2 = dialog->selection; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + if (g_cancellable_is_cancelled (cancellable)) { + g_list_free_full (dialog->duplicates, conflict_data_free); + break; + } + + file = NAUTILUS_FILE (l2->data); + new_name = l1->data; + + have_conflict = FALSE; + + name = nautilus_file_get_name (file); + file_name = g_string_new (name); + + g_free (name); + + /* check for duplicate only if the name has changed */ + if (!g_string_equal (new_name, file_name)) { + /* check with already existing files */ + exists = GPOINTER_TO_INT (g_hash_table_lookup (directory_names_table, new_name->str)); + + if (exists == TRUE && + !file_name_conflicts_with_results (dialog->selection, new_names, new_name, NULL)) { + conflict_data = g_new (ConflictData, 1); + conflict_data->name = g_strdup (new_name->str); + conflict_data->index = g_list_index (dialog->selection, l2->data); + dialog->duplicates = g_list_prepend (dialog->duplicates, + conflict_data); + have_conflict = TRUE; + } + + /* check with files that will result from the batch rename, unless + * this file already has a conflict */ + if (!have_conflict) { + exists = GPOINTER_TO_INT (g_hash_table_lookup (names_conflicts_table, new_name->str)); + + if (exists == TRUE) { + conflict_data = g_new (ConflictData, 1); + conflict_data->name = g_strdup (new_name->str); + conflict_data->index = g_list_index (dialog->selection, l2->data); + dialog->duplicates = g_list_prepend (dialog->duplicates, + conflict_data); + } + } + } + + g_string_free (file_name, TRUE); + } + + g_hash_table_destroy (directory_names_table); + g_hash_table_destroy (new_names_table); + g_hash_table_destroy (names_conflicts_table); + nautilus_file_list_free (directory_files); + g_list_free_full (new_names, string_free); + + dialog->duplicates = g_list_reverse (dialog->duplicates); + + dialog->checking_conflicts = FALSE; + + g_task_return_pointer (task, object, NULL); + +} + +static void +file_names_list_has_duplicates_async (NautilusBatchRenameDialog *dialog, + GAsyncReadyCallback callback, + gpointer user_data) +{ + if (dialog->checking_conflicts == TRUE) + g_cancellable_cancel (dialog->conflict_cancellable); + + dialog->conflict_cancellable = g_cancellable_new (); + + dialog->checking_conflicts = TRUE; + dialog->conflicts_task = g_task_new (dialog, dialog->conflict_cancellable, callback, user_data); + + g_task_run_in_thread (dialog->conflicts_task, file_names_list_has_duplicates_async_thread); + + g_object_unref (dialog->conflicts_task); +} + +static void +update_tags (NautilusBatchRenameDialog *dialog) +{ + TagData *tag_data; + const gchar *entry_text; + gint character_difference; + gint cursor_position; + + entry_text = gtk_entry_get_text (GTK_ENTRY (dialog->name_entry)); + + if (dialog->use_manual_cursor_position) { + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), + dialog->cursor_position); + } + + if (dialog->tags_deleted) { + dialog->tags_deleted = FALSE; + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), + g_utf8_strlen (entry_text, -1)); + } + + g_object_get (dialog->name_entry, "cursor-position", &cursor_position, NULL); + + character_difference = g_utf8_strlen (entry_text, -1) - dialog->name_entry_characters; + dialog->name_entry_characters = g_utf8_strlen (entry_text, -1); + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ORIGINAL_FILE_NAME); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CREATION_DATE); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CAMERA_MODEL); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, SEASON_NUMBER); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, EPISODE_NUMBER); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TRACK_NUMBER); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ARTIST_NAME); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TITLE); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ALBUM_NAME); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING0); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING00); + if (tag_data->just_added) { + tag_data->just_added = FALSE; + } else { + if (tag_data->set && cursor_position <= tag_data->position) { + tag_data->position += character_difference; + } + } +} + +static gboolean +have_unallowed_character (NautilusBatchRenameDialog *dialog) +{ + const gchar *entry_text; + gboolean have_unallowed_character_slash; + gboolean have_unallowed_character_dot; + gboolean have_unallowed_character_dotdot; + + have_unallowed_character_slash = FALSE; + have_unallowed_character_dot = FALSE; + have_unallowed_character_dotdot = FALSE; + + + + if (dialog->mode == NAUTILUS_BATCH_RENAME_DIALOG_FORMAT) { + entry_text = gtk_entry_get_text (GTK_ENTRY (dialog->name_entry)); + + } else { + entry_text = gtk_entry_get_text (GTK_ENTRY (dialog->replace_entry)); + } + + if (strstr (entry_text, "/") != NULL) { + have_unallowed_character_slash = TRUE; + } + + if (g_strcmp0 (entry_text, ".") == 0) { + have_unallowed_character_dot = TRUE; + } + + if (g_strcmp0 (entry_text, "..") == 0) { + have_unallowed_character_dotdot = TRUE; + } + + if (have_unallowed_character_slash) { + gtk_label_set_label (GTK_LABEL (dialog->conflict_label), + "\"/\" is an unallowed character"); + } + + if (have_unallowed_character_dot) { + gtk_label_set_label (GTK_LABEL (dialog->conflict_label), + "\".\" is an unallowed file name"); + } + + if (have_unallowed_character_dotdot) { + gtk_label_set_label (GTK_LABEL (dialog->conflict_label), + "\"..\" is an unallowed file name"); + } + + if (have_unallowed_character_slash || have_unallowed_character_dot || have_unallowed_character_dotdot) { + gtk_widget_set_sensitive (dialog->rename_button, FALSE); + gtk_widget_set_sensitive (dialog->conflict_down, FALSE); + gtk_widget_set_sensitive (dialog->conflict_up, FALSE); + + gtk_widget_show (dialog->conflict_box); + + return TRUE; + } else { + gtk_widget_hide (dialog->conflict_box); + + return FALSE; + } +} + +static void +update_display_text (NautilusBatchRenameDialog *dialog) +{ + TagData *tag_data; + TagData *tag_data0; + TagData *tag_data00; + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING); + tag_data0 = g_hash_table_lookup (dialog->tag_info_table, NUMBERING0); + tag_data00 = g_hash_table_lookup (dialog->tag_info_table, NUMBERING00); + + if (dialog->conflict_cancellable != NULL) + g_cancellable_cancel (dialog->conflict_cancellable); + + if(dialog->selection == NULL) + return; + + if (dialog->duplicates != NULL) { + g_list_free_full (dialog->duplicates, conflict_data_free); + dialog->duplicates = NULL; + } + + if (have_unallowed_character (dialog)) + return; + + update_tags (dialog); + + if (dialog->new_names != NULL) + g_list_free_full (dialog->new_names, string_free); + + if (!tag_data->set && !tag_data0->set && !tag_data00->set) { + gtk_label_set_label (GTK_LABEL (dialog->numbering_label), ""); + gtk_widget_hide (dialog->numbering_order_button); + } else { + gtk_label_set_label (GTK_LABEL (dialog->numbering_label), _("Automatic Numbering Order")); + gtk_widget_show (dialog->numbering_order_button); + } + + dialog->new_names = batch_rename_dialog_get_new_names (dialog); + dialog->checked_parents = 0; + + file_names_list_has_duplicates_async (dialog, + file_names_list_has_duplicates_callback, + NULL); +} + +static void +file_names_widget_entry_on_changed (NautilusBatchRenameDialog *dialog) +{ + update_display_text (dialog); +} + +static void +batch_rename_dialog_mode_changed (NautilusBatchRenameDialog *dialog) +{ + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->format_mode_button))) { + gtk_stack_set_visible_child_name (GTK_STACK (dialog->mode_stack), "format"); + + dialog->mode = NAUTILUS_BATCH_RENAME_DIALOG_FORMAT; + + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (dialog->name_entry)); + + } else { + gtk_stack_set_visible_child_name (GTK_STACK (dialog->mode_stack), "replace"); + + dialog->mode = NAUTILUS_BATCH_RENAME_DIALOG_REPLACE; + + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (dialog->find_entry)); + } + + update_display_text (dialog); +} + +static void +add_button_clicked (NautilusBatchRenameDialog *dialog) +{ + if (gtk_widget_is_visible (dialog->add_popover)) + gtk_widget_set_visible (dialog->add_popover, FALSE); + else + gtk_widget_set_visible (dialog->add_popover, TRUE); +} + +static void +add_popover_closed (NautilusBatchRenameDialog *dialog) +{ + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (dialog->add_button), FALSE); +} + +static void +numbering_order_button_clicked (NautilusBatchRenameDialog *dialog) +{ + if (gtk_widget_is_visible (dialog->numbering_order_popover)) + gtk_widget_set_visible (dialog->numbering_order_popover, FALSE); + else + gtk_widget_set_visible (dialog->numbering_order_popover, TRUE); +} + +static void +numbering_order_popover_closed (NautilusBatchRenameDialog *dialog) +{ + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (dialog->numbering_order_button), FALSE); +} + +void +nautilus_batch_rename_dialog_query_finished (NautilusBatchRenameDialog *dialog, + GHashTable *hash_table, + GList *selection_metadata) +{ + GMenuItem *first_created; + GMenuItem *last_created; + FileMetadata *metadata; + TagData *tag_data; + + /* for files with no metadata */ + if (hash_table != NULL && g_hash_table_size (hash_table) == 0) { + g_hash_table_destroy (hash_table); + + hash_table = NULL; + } + + if (hash_table == NULL) + dialog->create_date = NULL; + else + dialog->create_date = hash_table; + + if (dialog->create_date != NULL) { + first_created = g_menu_item_new ("First Created", + "dialog.numbering-order-changed('first-created')"); + + g_menu_append_item (dialog->numbering_order_menu, first_created); + + last_created = g_menu_item_new ("Last Created", + "dialog.numbering-order-changed('last-created')"); + + g_menu_append_item (dialog->numbering_order_menu, last_created); + + } + + dialog->selection_metadata = selection_metadata; + metadata = selection_metadata->data; + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CREATION_DATE); + if (metadata->creation_date == NULL || g_strcmp0 (metadata->creation_date->str, "") == 0) { + disable_action (dialog, "add-creation-date-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CAMERA_MODEL); + if (metadata->equipment == NULL || g_strcmp0 (metadata->equipment->str, "") == 0) { + disable_action (dialog, "add-equipment-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, SEASON_NUMBER); + if (metadata->season == NULL || g_strcmp0 (metadata->season->str, "") == 0) { + disable_action (dialog, "add-season-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, EPISODE_NUMBER); + if (metadata->episode_number == NULL || g_strcmp0 (metadata->episode_number->str, "") == 0) { + disable_action (dialog, "add-episode-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TRACK_NUMBER); + if (metadata->track_number == NULL || g_strcmp0 (metadata->track_number->str, "") == 0) { + disable_action (dialog, "add-track-number-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ARTIST_NAME); + if (metadata->artist_name == NULL || g_strcmp0 (metadata->artist_name->str, "") == 0) { + disable_action (dialog, "add-artist-name-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TITLE); + if (metadata->title == NULL || g_strcmp0 (metadata->title->str, "") == 0) { + disable_action (dialog, "add-title-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ALBUM_NAME); + if (metadata->album_name == NULL || g_strcmp0 (metadata->album_name->str, "") == 0) { + disable_action (dialog, "add-album-name-tag"); + tag_data->available = FALSE; + } else { + tag_data->set = FALSE; + } +} + +static void +update_row_shadowing (GtkWidget *row, + gboolean shown) +{ + GtkStyleContext *context; + GtkStateFlags flags; + + if (!GTK_IS_LIST_BOX_ROW (row)) + return; + + context = gtk_widget_get_style_context (row); + flags = gtk_style_context_get_state (context); + + if (shown) + flags |= GTK_STATE_PRELIGHT; + else + flags &= ~GTK_STATE_PRELIGHT; + + gtk_style_context_set_state (context, flags); + +} + +static gboolean +on_leave_event (GtkWidget *widget, + GdkEventMotion *event, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + update_row_shadowing (dialog->preselected_row1, FALSE); + update_row_shadowing (dialog->preselected_row2, FALSE); + + dialog->preselected_row1 = NULL; + dialog->preselected_row2 = NULL; + + return FALSE; +} + +static gboolean +on_motion (GtkWidget *widget, + GdkEventMotion *event, + gpointer user_data) +{ + GtkListBoxRow *row; + NautilusBatchRenameDialog *dialog; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + if (dialog->preselected_row1 && dialog->preselected_row2) { + update_row_shadowing (dialog->preselected_row1, FALSE); + update_row_shadowing (dialog->preselected_row2, FALSE); + } + + if (widget == dialog->result_listbox) { + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (dialog->original_name_listbox), event->y); + update_row_shadowing (GTK_WIDGET (row), TRUE); + dialog->preselected_row1 = GTK_WIDGET (row); + + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (dialog->arrow_listbox), event->y); + update_row_shadowing (GTK_WIDGET (row), TRUE); + dialog->preselected_row2 = GTK_WIDGET (row); + + } + + if (widget == dialog->arrow_listbox) { + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (dialog->original_name_listbox), event->y); + update_row_shadowing (GTK_WIDGET (row), TRUE); + dialog->preselected_row1 = GTK_WIDGET (row); + + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (dialog->result_listbox), event->y); + update_row_shadowing (GTK_WIDGET (row), TRUE); + dialog->preselected_row2 = GTK_WIDGET (row); + } + + if (widget == dialog->original_name_listbox) { + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (dialog->result_listbox), event->y); + update_row_shadowing (GTK_WIDGET (row), TRUE); + dialog->preselected_row1 = GTK_WIDGET (row); + + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (dialog->arrow_listbox), event->y); + update_row_shadowing (GTK_WIDGET (row), TRUE); + dialog->preselected_row2 = GTK_WIDGET (row); + } + + return FALSE; +} + +static void +nautilus_batch_rename_dialog_initialize_actions (NautilusBatchRenameDialog *dialog) +{ + GAction *action; + + dialog->action_group = G_ACTION_GROUP (g_simple_action_group_new ()); + + g_action_map_add_action_entries (G_ACTION_MAP (dialog->action_group), + dialog_entries, + G_N_ELEMENTS (dialog_entries), + dialog); + gtk_widget_insert_action_group (GTK_WIDGET (dialog), + "dialog", + G_ACTION_GROUP (dialog->action_group)); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-original-file-name-tag"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE); + + check_metadata_for_selection (dialog, dialog->selection); +} + +static void +file_names_widget_on_activate (NautilusBatchRenameDialog *dialog) +{ + prepare_batch_rename (dialog); +} + +static gboolean +remove_tag (NautilusBatchRenameDialog *dialog, + gchar *tag_name, + gchar *action_name, + gint keyval, + gboolean is_modifier) +{ + TagData *tag_data; + gint cursor_position; + GString *new_entry_text; + GString *entry_text; + gboolean delete_tag; + GAction *action; + + delete_tag = FALSE; + + g_object_get (dialog->name_entry, "cursor-position", &cursor_position, NULL); + + tag_data = g_hash_table_lookup (dialog->tag_info_table, tag_name); + + entry_text = g_string_new (gtk_entry_get_text (GTK_ENTRY (dialog->name_entry))); + + if (!tag_data->set) + return FALSE; + + if (keyval == GDK_KEY_BackSpace) { + if (cursor_position > tag_data->position && + cursor_position <= tag_data->position + g_utf8_strlen (tag_name, -1)) { + delete_tag = TRUE; + } + } + + if (keyval == GDK_KEY_Delete) { + if (cursor_position >= tag_data->position && + cursor_position < tag_data->position + g_utf8_strlen (tag_name, -1)) { + delete_tag = TRUE; + } + } + + if (!is_modifier && + keyval != GDK_KEY_Left && + keyval != GDK_KEY_Right && + keyval != GDK_KEY_Delete && + keyval != GDK_KEY_Return && + keyval != GDK_KEY_Escape && + keyval != GDK_KEY_Tab && + keyval != GDK_KEY_End && + keyval != GDK_KEY_Home) { + if (cursor_position > tag_data->position && + cursor_position < tag_data->position + g_utf8_strlen (tag_name, -1)) { + delete_tag = TRUE; + } + } + + if (delete_tag) { + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + action_name); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + new_entry_text = g_string_new (""); + new_entry_text = g_string_append_len (new_entry_text, + entry_text->str, + tag_data->position); + new_entry_text = g_string_append (new_entry_text, + g_utf8_offset_to_pointer (entry_text->str, + tag_data->position + g_utf8_strlen (tag_name, -1))); + + tag_data->set = FALSE; + dialog->cursor_position = tag_data->position; + dialog->use_manual_cursor_position = TRUE; + gtk_entry_set_text (GTK_ENTRY (dialog->name_entry), new_entry_text->str); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), tag_data->position); + dialog->use_manual_cursor_position = FALSE; + + g_string_free (new_entry_text, TRUE); + g_string_free (entry_text, TRUE); + + return TRUE; + } + + return FALSE; +} + +static GString* +remove_tag_selection (NautilusBatchRenameDialog *dialog, + GString *old_entry_text, + gchar *action_name, + gchar *tag_name, + gint start, + gint end) +{ + TagData *tag_data; + GAction *action; + GString *new_entry_text; + + new_entry_text = NULL; + + tag_data = g_hash_table_lookup (dialog->tag_info_table, tag_name); + + if (tag_data->set && tag_data->original_position < end && + tag_data->original_position + g_utf8_strlen (tag_name, -1) > start) { + new_entry_text = g_string_new (""); + new_entry_text = g_string_append_len (new_entry_text, + old_entry_text->str, + tag_data->new_position); + new_entry_text = g_string_append (new_entry_text, + g_utf8_offset_to_pointer (old_entry_text->str, + tag_data->new_position + g_utf8_strlen (tag_name, -1))); + + tag_data->set = FALSE; + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + action_name); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + } + + if (new_entry_text == NULL) + return g_string_new (old_entry_text->str); + return new_entry_text; +} + +static void +update_tag_position (NautilusBatchRenameDialog *dialog, + gchar *tag_name, + GString *new_entry_text) +{ + TagData *tag_data; + + tag_data = g_hash_table_lookup (dialog->tag_info_table, tag_name); + + if (tag_data->set) { + tag_data->original_position = tag_data->position; + tag_data->new_position = g_utf8_pointer_to_offset(new_entry_text->str, + g_strrstr (new_entry_text->str, tag_name)); + } +} + +static gboolean +on_key_press_event (GtkWidget *widget, + GdkEventKey *event, + gpointer user_data) +{ + NautilusBatchRenameDialog *dialog; + gint keyval; + GdkEvent *gdk_event; + GString *old_entry_text; + GString *new_entry_text; + gboolean entry_has_selection; + gint start; + gint end; + gboolean tag_removed; + TagData *tag_data; + gint minimum_tag_position; + GAction *action; + + gdk_event = (GdkEvent *) event; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (user_data); + + keyval = event->keyval; + + entry_has_selection = (gtk_editable_get_selection_bounds (GTK_EDITABLE (dialog->name_entry), + &start, + &end)); + + if (event->state & GDK_CONTROL_MASK) + return GDK_EVENT_PROPAGATE; + + if (entry_has_selection && + ((keyval == GDK_KEY_Delete || keyval == GDK_KEY_BackSpace) || + (!gdk_event->key.is_modifier && + keyval != GDK_KEY_Left && + keyval != GDK_KEY_Right && + keyval != GDK_KEY_Return && + keyval != GDK_KEY_Escape && + keyval != GDK_KEY_Tab && + keyval != GDK_KEY_End && + keyval != GDK_KEY_Home))) { + old_entry_text = g_string_new (gtk_entry_get_text (GTK_ENTRY (dialog->name_entry))); + + minimum_tag_position = G_MAXINT; + tag_removed = FALSE; + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ORIGINAL_FILE_NAME); + if (tag_data->set) { + update_tag_position (dialog, ORIGINAL_FILE_NAME, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-original-file-name-tag", + ORIGINAL_FILE_NAME, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CREATION_DATE); + if (tag_data->set) { + update_tag_position (dialog, CREATION_DATE, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-creation-date-tag", + CREATION_DATE, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING); + if (tag_data->set) { + update_tag_position (dialog, NUMBERING, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-numbering-tag-zero", + NUMBERING, + start, + end); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-one"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-two"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING0); + if (tag_data->set) { + update_tag_position (dialog, NUMBERING0, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-numbering-tag-one", + NUMBERING0, + start, + end); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-zero"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-two"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, NUMBERING00); + if (tag_data->set) { + update_tag_position (dialog, NUMBERING00, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-numbering-tag-two", + NUMBERING0, + start, + end); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-one"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-zero"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, CAMERA_MODEL); + if (tag_data->set) { + update_tag_position (dialog, CAMERA_MODEL, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-equipment-tag", + CAMERA_MODEL, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TRACK_NUMBER); + if (tag_data->set) { + update_tag_position (dialog, TRACK_NUMBER, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-track-number-tag", + TRACK_NUMBER, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, SEASON_NUMBER); + if (tag_data->set) { + update_tag_position (dialog, SEASON_NUMBER, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-season-tag", + SEASON_NUMBER, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, EPISODE_NUMBER); + if (tag_data->set) { + update_tag_position (dialog, EPISODE_NUMBER, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-episode-tag", + EPISODE_NUMBER, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ARTIST_NAME); + if (tag_data->set) { + update_tag_position (dialog, ARTIST_NAME, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-artist-name-tag", + ARTIST_NAME, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, TITLE); + if (tag_data->set) { + update_tag_position (dialog, TITLE, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-title-tag", + TITLE, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + tag_data = g_hash_table_lookup (dialog->tag_info_table, ALBUM_NAME); + if (tag_data->set) { + update_tag_position (dialog, ALBUM_NAME, old_entry_text); + new_entry_text = remove_tag_selection (dialog, + old_entry_text, + "add-album-name-tag", + ALBUM_NAME, + start, + end); + + if (!g_string_equal (new_entry_text, old_entry_text)) { + if (tag_data->position < minimum_tag_position) + minimum_tag_position = tag_data->position; + + tag_removed = TRUE; + } + g_string_free (old_entry_text, TRUE); + old_entry_text = new_entry_text; + } + + if (minimum_tag_position != G_MAXINT) { + dialog->use_manual_cursor_position = TRUE; + dialog->cursor_position = minimum_tag_position; + + gtk_entry_set_text (GTK_ENTRY (dialog->name_entry), new_entry_text->str); + gtk_editable_set_position (GTK_EDITABLE (dialog->name_entry), minimum_tag_position); + + dialog->use_manual_cursor_position = FALSE; + + g_string_free (new_entry_text, TRUE); + } + + if ((keyval == GDK_KEY_Delete || keyval == GDK_KEY_BackSpace) && + tag_removed) + return TRUE; + + return GDK_EVENT_PROPAGATE; + } + + tag_removed = FALSE; + + if (remove_tag (dialog, + ORIGINAL_FILE_NAME, + "add-original-file-name-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + CREATION_DATE, + "add-creation-date-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + NUMBERING, + "add-numbering-tag-zero", + keyval, + gdk_event->key.is_modifier)) { + tag_removed = TRUE; + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-one"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-two"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + } + + if (!tag_removed && remove_tag (dialog, + NUMBERING0, + "add-numbering-tag-one", + keyval, + gdk_event->key.is_modifier)) { + tag_removed = TRUE; + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-zero"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-two"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + } + + if (!tag_removed && remove_tag (dialog, + NUMBERING00, + "add-numbering-tag-two", + keyval, + gdk_event->key.is_modifier)) { + tag_removed = TRUE; + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-one"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + + action = g_action_map_lookup_action (G_ACTION_MAP (dialog->action_group), + "add-numbering-tag-zero"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); + } + + if (!tag_removed && remove_tag (dialog, + CAMERA_MODEL, + "add-equipment-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + SEASON_NUMBER, + "add-season-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + EPISODE_NUMBER, + "add-episode-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + TRACK_NUMBER, + "add-track-number-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + ARTIST_NAME, + "add-artist-name-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + TITLE, + "add-title-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (!tag_removed && remove_tag (dialog, + ALBUM_NAME, + "add-album-name-tag", + keyval, + gdk_event->key.is_modifier)) + tag_removed = TRUE; + + if (tag_removed) { + if (keyval == GDK_KEY_Delete || keyval == GDK_KEY_BackSpace) + return TRUE; + + return GDK_EVENT_PROPAGATE; + } + + return GDK_EVENT_PROPAGATE; +} + +static void +nautilus_batch_rename_dialog_finalize (GObject *object) +{ + NautilusBatchRenameDialog *dialog; + GList *l; + + dialog = NAUTILUS_BATCH_RENAME_DIALOG (object); + + if (dialog->checking_conflicts) { + g_cancellable_cancel (dialog->conflict_cancellable); + g_object_unref (dialog->conflict_cancellable); + } + + g_list_free (dialog->original_name_listbox_rows); + g_list_free (dialog->arrow_listbox_rows); + g_list_free (dialog->result_listbox_rows); + g_list_free (dialog->listbox_labels_new); + g_list_free (dialog->listbox_labels_old); + g_list_free (dialog->listbox_icons); + + for (l = dialog->selection_metadata; l != NULL; l = l->next) { + FileMetadata *metadata; + + metadata = l->data; + + if (metadata->file_name != NULL) + g_string_free (metadata->file_name, TRUE); + if (metadata->creation_date != NULL) + g_string_free (metadata->creation_date, TRUE); + if (metadata->equipment != NULL) + g_string_free (metadata->equipment, TRUE); + if (metadata->season != NULL) + g_string_free (metadata->season, TRUE); + if (metadata->episode_number != NULL) + g_string_free (metadata->episode_number, TRUE); + if (metadata->track_number != NULL) + g_string_free (metadata->track_number, TRUE); + if (metadata->artist_name != NULL) + g_string_free (metadata->artist_name, TRUE); + if (metadata->album_name != NULL) + g_string_free (metadata->album_name, TRUE); + } + + if (dialog->create_date != NULL) + g_hash_table_destroy (dialog->create_date); + + g_list_free_full (dialog->distinct_parents, g_free); + g_list_free_full (dialog->new_names, string_free); + g_list_free_full (dialog->duplicates, conflict_data_free); + + G_OBJECT_CLASS (nautilus_batch_rename_dialog_parent_class)->finalize (object); +} + +static void +nautilus_batch_rename_dialog_class_init (NautilusBatchRenameDialogClass *klass) +{ + GtkWidgetClass *widget_class; + GObjectClass *oclass; + + widget_class = GTK_WIDGET_CLASS (klass); + oclass = G_OBJECT_CLASS (klass); + + oclass->finalize = nautilus_batch_rename_dialog_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/nautilus/ui/nautilus-batch-rename-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, grid); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, cancel_button); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, original_name_listbox); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, arrow_listbox); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, result_listbox); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, name_entry); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, rename_button); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, find_entry); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, replace_entry); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, mode_stack); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, replace_mode_button); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, format_mode_button); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, add_button); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, add_popover); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, numbering_order_label); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, scrolled_window); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, numbering_order_popover); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, numbering_order_button); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, numbering_order_menu); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, conflict_box); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, conflict_label); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, conflict_up); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, conflict_down); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, add_tag_menu); + gtk_widget_class_bind_template_child (widget_class, NautilusBatchRenameDialog, numbering_label); + + gtk_widget_class_bind_template_callback (widget_class, file_names_widget_entry_on_changed); + gtk_widget_class_bind_template_callback (widget_class, file_names_widget_on_activate); + gtk_widget_class_bind_template_callback (widget_class, batch_rename_dialog_mode_changed); + gtk_widget_class_bind_template_callback (widget_class, add_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, add_popover_closed); + gtk_widget_class_bind_template_callback (widget_class, numbering_order_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, numbering_order_popover_closed); + gtk_widget_class_bind_template_callback (widget_class, select_next_conflict_up); + gtk_widget_class_bind_template_callback (widget_class, select_next_conflict_down); + gtk_widget_class_bind_template_callback (widget_class, batch_rename_dialog_on_response); + gtk_widget_class_bind_template_callback (widget_class, on_key_press_event); +} + +GtkWidget* +nautilus_batch_rename_dialog_new (GList *selection, + NautilusDirectory *directory, + NautilusWindow *window) +{ + NautilusBatchRenameDialog *dialog; + GString *dialog_title; + + dialog = g_object_new (NAUTILUS_TYPE_BATCH_RENAME_DIALOG, "use-header-bar", TRUE, NULL); + + dialog->selection = selection; + dialog->directory = directory; + dialog->window = window; + + gtk_window_set_transient_for (GTK_WINDOW (dialog), + GTK_WINDOW (window)); + + dialog_title = g_string_new (""); + g_string_append_printf (dialog_title, "Rename %d Files", g_list_length (selection)); + gtk_window_set_title (GTK_WINDOW (dialog), dialog_title->str); + + nautilus_batch_rename_dialog_initialize_actions (dialog); + + dialog->same_parent = !NAUTILUS_IS_SEARCH_DIRECTORY (directory); + + if (!dialog->same_parent) + dialog->distinct_parents = batch_rename_files_get_distinct_parents (dialog->selection); + else + dialog->distinct_parents = NULL; + + update_display_text (dialog); + + fill_display_listbox (dialog); + + gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (window)), NULL); + + g_string_free (dialog_title, TRUE); + + return GTK_WIDGET (dialog); +} + +static void +nautilus_batch_rename_dialog_init (NautilusBatchRenameDialog *self) +{ + TagData *tag_data; + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->original_name_listbox), + (GtkListBoxUpdateHeaderFunc) listbox_header_func, + self, + NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (self->arrow_listbox), + (GtkListBoxUpdateHeaderFunc) listbox_header_func, + self, + NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (self->result_listbox), + (GtkListBoxUpdateHeaderFunc) listbox_header_func, + self, + NULL); + + + self->mode = NAUTILUS_BATCH_RENAME_DIALOG_FORMAT; + + gtk_popover_bind_model (GTK_POPOVER (self->numbering_order_popover), + G_MENU_MODEL (self->numbering_order_menu), + NULL); + gtk_popover_bind_model (GTK_POPOVER (self->add_popover), + G_MENU_MODEL (self->add_tag_menu), + NULL); + + gtk_label_set_ellipsize (GTK_LABEL (self->conflict_label), PANGO_ELLIPSIZE_END); + gtk_label_set_max_width_chars (GTK_LABEL (self->conflict_label), 1); + + self->duplicates = NULL; + self->new_names = NULL; + + self->checking_conflicts = FALSE; + + self->rename_clicked = FALSE; + + + self->tag_info_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + tag_data = g_new (TagData, 1); + tag_data->available = TRUE; + tag_data->set = TRUE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (ORIGINAL_FILE_NAME), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = TRUE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (NUMBERING), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = TRUE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (NUMBERING0), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = TRUE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (NUMBERING00), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (CREATION_DATE), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (CAMERA_MODEL), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (SEASON_NUMBER), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (EPISODE_NUMBER), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (TRACK_NUMBER), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (ARTIST_NAME), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (TITLE), tag_data); + + tag_data = g_new (TagData, 1); + tag_data->available = FALSE; + tag_data->set = FALSE; + tag_data->position = 0; + g_hash_table_insert (self->tag_info_table, g_strdup (ALBUM_NAME), tag_data); + + gtk_entry_set_text (GTK_ENTRY (self->name_entry),ORIGINAL_FILE_NAME); + self->name_entry_characters = g_utf8_strlen (ORIGINAL_FILE_NAME, -1); + + self->row_height = -1; + + g_signal_connect (self->original_name_listbox, "row-selected", G_CALLBACK (row_selected), self); + g_signal_connect (self->arrow_listbox, "row-selected", G_CALLBACK (row_selected), self); + g_signal_connect (self->result_listbox, "row-selected", G_CALLBACK (row_selected), self); + + self->size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + g_signal_connect (self->original_name_listbox, + "motion-notify-event", + G_CALLBACK (on_motion), + self); + g_signal_connect (self->result_listbox, + "motion-notify-event", + G_CALLBACK (on_motion), + self); + g_signal_connect (self->arrow_listbox, + "motion-notify-event", + G_CALLBACK (on_motion), + self); + + g_signal_connect (self->original_name_listbox, + "leave-notify-event", + G_CALLBACK (on_leave_event), + self); + g_signal_connect (self->result_listbox, + "leave-notify-event", + G_CALLBACK (on_leave_event), + self); + g_signal_connect (self->arrow_listbox, + "leave-notify-event", + G_CALLBACK (on_leave_event), + self); +}
\ No newline at end of file diff --git a/src/nautilus-batch-rename-dialog.h b/src/nautilus-batch-rename-dialog.h new file mode 100644 index 000000000..3f11f4eaa --- /dev/null +++ b/src/nautilus-batch-rename-dialog.h @@ -0,0 +1,100 @@ +/* nautilus-batch-rename-utilities.c + * + * Copyright (C) 2016 Alexandru Pandelea <alexandru.pandelea@gmail.com> + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#ifndef NAUTILUS_BATCH_RENAME_DIALOG_H +#define NAUTILUS_BATCH_RENAME_DIALOG_H + +#include <glib.h> +#include <glib/gprintf.h> +#include <gtk/gtk.h> +#include "nautilus-files-view.h" + +G_BEGIN_DECLS + +#define ORIGINAL_FILE_NAME "[Original file name]" +#define NUMBERING "[1, 2, 3]" +#define NUMBERING0 "[01, 02, 03]" +#define NUMBERING00 "[001, 002, 003]" +#define CAMERA_MODEL "[Camera model]" +#define CREATION_DATE "[Creation date]" +#define SEASON_NUMBER "[Season number]" +#define EPISODE_NUMBER "[Episode number]" +#define TRACK_NUMBER "[Track number]" +#define ARTIST_NAME "[Artist name]" +#define TITLE "[Title]" +#define ALBUM_NAME "[Album name]" + +typedef enum { + NAUTILUS_BATCH_RENAME_DIALOG_APPEND = 0, + NAUTILUS_BATCH_RENAME_DIALOG_PREPEND = 1, + NAUTILUS_BATCH_RENAME_DIALOG_REPLACE = 2, + NAUTILUS_BATCH_RENAME_DIALOG_FORMAT = 3, +} NautilusBatchRenameDialogMode; + +typedef enum { + ORIGINAL_ASCENDING = 0, + ORIGINAL_DESCENDING = 1, + FIRST_MODIFIED = 2, + LAST_MODIFIED = 3, + FIRST_CREATED = 4, + LAST_CREATED = 5, +} SortingMode; + +typedef struct +{ + gchar *name; + gint index; +} ConflictData; + +typedef struct { + GString *file_name; + + /* Photo */ + GString *creation_date; + GString *equipment; + + /* Video */ + GString *season; + GString *episode_number; + + /* Music */ + GString *track_number; + GString *artist_name; + GString *title; + GString *album_name; +} FileMetadata; + +#define NAUTILUS_TYPE_BATCH_RENAME_DIALOG (nautilus_batch_rename_dialog_get_type()) + +G_DECLARE_FINAL_TYPE (NautilusBatchRenameDialog, nautilus_batch_rename_dialog, NAUTILUS, BATCH_RENAME_DIALOG, GtkDialog); + +GtkWidget* nautilus_batch_rename_dialog_new (GList *selection, + NautilusDirectory *directory, + NautilusWindow *window); + +void nautilus_batch_rename_dialog_query_finished (NautilusBatchRenameDialog *dialog, + GHashTable *hash_table, + GList *selection_metadata); + +void check_conflict_for_files (NautilusBatchRenameDialog *dialog, + NautilusDirectory *directory, + GList *files); + +G_END_DECLS + +#endif
\ No newline at end of file diff --git a/src/nautilus-batch-rename-utilities.c b/src/nautilus-batch-rename-utilities.c new file mode 100644 index 000000000..7fb9383b4 --- /dev/null +++ b/src/nautilus-batch-rename-utilities.c @@ -0,0 +1,1050 @@ +/* nautilus-batch-rename-utilities.c + * + * Copyright (C) 2016 Alexandru Pandelea <alexandru.pandelea@gmail.com> + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#include "nautilus-batch-rename-dialog.h" +#include "nautilus-batch-rename-utilities.h" +#include "nautilus-file.h" + +#include <glib.h> +#include <gtk/gtk.h> +#include <string.h> +#include <stdarg.h> +#include <eel/eel-vfs-extensions.h> + +typedef struct { + NautilusFile *file; + gint position; +} CreateDateElem; + +typedef struct { + NautilusBatchRenameDialog *dialog; + GHashTable *hash_table; + + GList *selection_metadata; + + gboolean have_creation_date; + gboolean have_equipment; + gboolean have_season; + gboolean have_episode_number; + gboolean have_track_number; + gboolean have_artist_name; + gboolean have_title; + gboolean have_album_name; +} QueryData; + +enum { + FILE_NAME_INDEX, + CREATION_DATE_INDEX, + YEAR_INDEX, + MONTH_INDEX, + DAY_INDEX, + HOURS_INDEX, + MINUTES_INDEX, + SECONDS_INDEX, + CAMERA_MODEL_INDEX, + SEASON_INDEX, + EPISODE_NUMBER_INDEX, + TRACK_NUMBER_INDEX, + ARTIST_NAME_INDEX, + TITLE_INDEX, + ALBUM_NAME_INDEX, + +} QueryMetadata; + +static void on_cursor_callback (GObject *object, + GAsyncResult *result, + gpointer user_data); + +void +string_free (gpointer mem) +{ + if (mem != NULL) + g_string_free (mem, TRUE); +} + +void +conflict_data_free (gpointer mem) +{ + ConflictData *conflict_data = mem; + + g_free (conflict_data->name); + g_free (conflict_data); +} + +static GString* +batch_rename_replace (gchar *string, + gchar *substring, + gchar *replacement) +{ + GString *new_string; + gchar **splitted_string; + gint i, n_splits; + + new_string = g_string_new (""); + + if (substring == NULL || replacement == NULL) { + g_string_append (new_string, string); + + return new_string; + } + + if (g_utf8_strlen (substring, -1) == 0) { + g_string_append (new_string, string); + + return new_string; + } + + splitted_string = g_strsplit (string, substring, -1); + if (splitted_string == NULL) { + g_string_append (new_string, string); + + return new_string; + } + + n_splits = g_strv_length (splitted_string); + + for (i = 0; i < n_splits; i++) { + g_string_append (new_string, splitted_string[i]); + + if (i != n_splits - 1) + g_string_append (new_string, replacement); + } + + g_strfreev (splitted_string); + + return new_string; +} + +/* This function changes the background color of the replaced part of the name */ +GString* +batch_rename_replace_label_text (gchar *label, + const gchar *substring) +{ + GString *new_label; + gchar **splitted_string; + gchar *token; + gint i, n_splits; + + new_label = g_string_new (""); + + if (substring == NULL || g_strcmp0 (substring, "") == 0) { + token = g_markup_escape_text (label, g_utf8_strlen (label, -1)); + new_label = g_string_append (new_label, token); + g_free (token); + + return new_label; + } + + splitted_string = g_strsplit (label, substring, -1); + if (splitted_string == NULL) { + token = g_markup_escape_text (label, g_utf8_strlen (label, -1)); + new_label = g_string_append (new_label, token); + g_free (token); + + return new_label; + } + + n_splits = g_strv_length (splitted_string); + + for (i = 0; i < n_splits; i++) { + token = g_markup_escape_text (splitted_string[i], strlen (splitted_string[i])); + new_label = g_string_append (new_label, token); + + g_free (token); + + if (i != n_splits - 1) { + token = g_markup_escape_text (substring, g_utf8_strlen (substring, -1)); + g_string_append_printf (new_label, + "<span background=\'#f57900\' color='white'>%s</span>", + token); + + g_free (token); + } + } + + g_strfreev (splitted_string); + + return new_label; +} + +static gchar* +get_metadata (GList *selection_metadata, + gchar *file_name, + gchar *metadata) +{ + GList *l; + FileMetadata *file_metadata; + + for (l = selection_metadata; l != NULL; l = l->next) { + file_metadata = l->data; + if (g_strcmp0 (file_name, file_metadata->file_name->str) == 0) { + if (g_strcmp0 (metadata, "creation_date") == 0 && + file_metadata->creation_date != NULL && + file_metadata->creation_date->len != 0) + return file_metadata->creation_date->str; + + if (g_strcmp0 (metadata, "equipment") == 0 && + file_metadata->equipment != NULL && + file_metadata->equipment->len != 0) + return file_metadata->equipment->str; + + if (g_strcmp0 (metadata, "season") == 0 && + file_metadata->season != NULL && + file_metadata->season->len != 0) + return file_metadata->season->str; + + if (g_strcmp0 (metadata, "episode_number") == 0 && + file_metadata->episode_number != NULL && + file_metadata->episode_number->len != 0) + return file_metadata->episode_number->str; + + if (g_strcmp0 (metadata, "track_number") == 0 && + file_metadata->track_number != NULL && + file_metadata->track_number->len != 0) + return file_metadata->track_number->str; + + if (g_strcmp0 (metadata, "artist_name") == 0 && + file_metadata->artist_name != NULL && + file_metadata->artist_name->len != 0) + return file_metadata->artist_name->str; + + if (g_strcmp0 (metadata, "title") == 0 && + file_metadata->title != NULL && + file_metadata->title->len != 0) + return file_metadata->title->str; + + if (g_strcmp0 (metadata, "album_name") == 0 && + file_metadata->album_name != NULL && + file_metadata->album_name->len != 0) + return file_metadata->album_name->str; + } + } + + return NULL; +} + +static GString* +batch_rename_format (NautilusFile *file, + GList *text_chunks, + GList *selection_metadata, + gint count) +{ + GList *l; + GString *tag; + GString *new_name; + gboolean added_tag; + g_autofree gchar *file_name; + g_autofree gchar *extension; + gchar *metadata; + gchar *base_name; + + file_name = nautilus_file_get_display_name (file); + extension = nautilus_file_get_extension (file); + + new_name = g_string_new (""); + + for (l = text_chunks; l != NULL; l = l->next) { + tag = l->data; + added_tag = FALSE; + + if (!added_tag && g_strcmp0 (tag->str, ORIGINAL_FILE_NAME) == 0) { + base_name = eel_filename_strip_extension (file_name); + + new_name = g_string_append (new_name, base_name); + + added_tag = TRUE; + g_free (base_name); + } + + if (!added_tag && g_strcmp0 (tag->str, NUMBERING) == 0) { + g_string_append_printf (new_name, "%d", count); + added_tag = TRUE; + } + + if (!added_tag && g_strcmp0 (tag->str, NUMBERING0) == 0) { + g_string_append_printf (new_name, "%02d", count); + + added_tag = TRUE; + } + + if (!added_tag && g_strcmp0 (tag->str, NUMBERING00) == 0) { + g_string_append_printf (new_name, "%03d", count); + + added_tag = TRUE; + } + + if (!added_tag && g_strcmp0 (tag->str, CAMERA_MODEL) == 0) { + metadata = get_metadata (selection_metadata, file_name, "equipment"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, CREATION_DATE) == 0) { + metadata = get_metadata (selection_metadata, file_name, "creation_date"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, SEASON_NUMBER) == 0) { + metadata = get_metadata (selection_metadata, file_name, "season"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, EPISODE_NUMBER) == 0) { + metadata = get_metadata (selection_metadata, file_name, "episode_number"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, TRACK_NUMBER) == 0) { + metadata = get_metadata (selection_metadata, file_name, "track_number"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, ARTIST_NAME) == 0) { + metadata = get_metadata (selection_metadata, file_name, "artist_name"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, TITLE) == 0) { + metadata = get_metadata (selection_metadata, file_name, "title"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag && g_strcmp0 (tag->str, ALBUM_NAME) == 0) { + metadata = get_metadata (selection_metadata, file_name, "album_name"); + + if (metadata != NULL) { + new_name = g_string_append (new_name, metadata); + added_tag = TRUE; + } + } + + if (!added_tag) + new_name = g_string_append (new_name, tag->str); + } + + if (g_strcmp0 (new_name->str, "") == 0) { + new_name = g_string_append (new_name, file_name); + } else { + if (extension != NULL) + new_name = g_string_append (new_name, extension); + } + + return new_name; +} + +GList* +batch_rename_dialog_get_new_names_list (NautilusBatchRenameDialogMode mode, + GList *selection, + GList *text_chunks, + GList *selection_metadata, + gchar *entry_text, + gchar *replace_text) +{ + GList *l; + GList *result; + GString *file_name; + GString *new_name; + NautilusFile *file; + gchar *name; + gint count; + + result = NULL; + count = 1; + file_name = g_string_new (""); + + for (l = selection; l != NULL; l = l->next) { + file = NAUTILUS_FILE (l->data); + + file_name = g_string_new (""); + name = nautilus_file_get_name (file); + g_string_append (file_name, name); + + /* get the new name here and add it to the list*/ + if (mode == NAUTILUS_BATCH_RENAME_DIALOG_FORMAT) { + new_name = batch_rename_format (file, + text_chunks, + selection_metadata, + count++); + result = g_list_prepend (result, new_name); + } + + if (mode == NAUTILUS_BATCH_RENAME_DIALOG_REPLACE) { + new_name = batch_rename_replace (file_name->str, + entry_text, + replace_text); + result = g_list_prepend (result, new_name); + } + + g_string_free (file_name, TRUE); + g_free (name); + } + + return result; +} + +/* There is a case that a new name for a file conflicts with an existing file name + * in the directory but it's not a problem because the file in the directory that + * conflicts is part of the batch renaming selection and it's going to change the name anyway. */ +gboolean +file_name_conflicts_with_results (GList *selection, + GList *new_names, + GString *old_name, + gchar *parent_uri) +{ + GList *l1; + GList *l2; + NautilusFile *selection_file; + gchar *name1; + GString *new_name; + gchar *selection_parent_uri; + + for (l1 = selection, l2 = new_names; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + selection_file = NAUTILUS_FILE (l1->data); + name1 = nautilus_file_get_name (selection_file); + + selection_parent_uri = nautilus_file_get_parent_uri (selection_file); + + if (g_strcmp0 (name1, old_name->str) == 0) { + new_name = l2->data; + + /* if the name didn't change, then there's a conflict */ + if (g_string_equal (old_name, new_name) && + (parent_uri == NULL || g_strcmp0 (parent_uri, selection_parent_uri) == 0)) + return FALSE; + + + /* if this file exists and it changed it's name, then there's no + * conflict */ + return TRUE; + } + + g_free (selection_parent_uri); + } + + /* the case this function searched for doesn't exist, so the file + * has a conlfict */ + return FALSE; +} + +static gint +compare_files_by_name_ascending (gconstpointer a, + gconstpointer b) +{ + NautilusFile *file1; + NautilusFile *file2; + + file1 = NAUTILUS_FILE (a); + file2 = NAUTILUS_FILE (b); + + return nautilus_file_compare_for_sort (file1,file2, + NAUTILUS_FILE_SORT_BY_DISPLAY_NAME, + FALSE, FALSE); +} + +static gint +compare_files_by_name_descending (gconstpointer a, + gconstpointer b) +{ + NautilusFile *file1; + NautilusFile *file2; + + file1 = NAUTILUS_FILE (a); + file2 = NAUTILUS_FILE (b); + + return nautilus_file_compare_for_sort (file1,file2, + NAUTILUS_FILE_SORT_BY_DISPLAY_NAME, + FALSE, TRUE); +} + +static gint +compare_files_by_first_modified (gconstpointer a, + gconstpointer b) +{ + NautilusFile *file1; + NautilusFile *file2; + + file1 = NAUTILUS_FILE (a); + file2 = NAUTILUS_FILE (b); + + return nautilus_file_compare_for_sort (file1,file2, + NAUTILUS_FILE_SORT_BY_MTIME, + FALSE, FALSE); +} + +static gint +compare_files_by_last_modified (gconstpointer a, + gconstpointer b) +{ + NautilusFile *file1; + NautilusFile *file2; + + file1 = NAUTILUS_FILE (a); + file2 = NAUTILUS_FILE (b); + + return nautilus_file_compare_for_sort (file1,file2, + NAUTILUS_FILE_SORT_BY_MTIME, + FALSE, TRUE); +} + +static gint +compare_files_by_first_created (gconstpointer a, + gconstpointer b) +{ + CreateDateElem *elem1; + CreateDateElem *elem2; + + elem1 = (CreateDateElem*) a; + elem2 = (CreateDateElem*) b; + + return elem1->position - elem2->position; +} + +static gint +compare_files_by_last_created (gconstpointer a, + gconstpointer b) +{ + CreateDateElem *elem1; + CreateDateElem *elem2; + + elem1 = (CreateDateElem*) a; + elem2 = (CreateDateElem*) b; + + return elem2->position - elem1->position; +} + +GList* +nautilus_batch_rename_dialog_sort (GList *selection, + SortingMode mode, + GHashTable *creation_date_table) +{ + GList *l,*l2; + NautilusFile *file; + GList *create_date_list; + GList *create_date_list_sorted; + gchar *name; + + if (mode == ORIGINAL_ASCENDING) + return g_list_sort (selection, compare_files_by_name_ascending); + + if (mode == ORIGINAL_DESCENDING) { + return g_list_sort (selection, compare_files_by_name_descending); + } + + if (mode == FIRST_MODIFIED) { + return g_list_sort (selection, compare_files_by_first_modified); + } + + if (mode == LAST_MODIFIED) { + return g_list_sort (selection, compare_files_by_last_modified); + } + + if (mode == FIRST_CREATED || mode == LAST_CREATED) { + create_date_list = NULL; + + for (l = selection; l != NULL; l = l->next) { + CreateDateElem *elem; + elem = g_new (CreateDateElem, 1); + + file = NAUTILUS_FILE (l->data); + + name = nautilus_file_get_name (file); + elem->file = file; + elem->position = GPOINTER_TO_INT (g_hash_table_lookup (creation_date_table, name)); + g_free (name); + + create_date_list = g_list_prepend (create_date_list, elem); + } + + if (mode == FIRST_CREATED) + create_date_list_sorted = g_list_sort (create_date_list, + compare_files_by_first_created); + else + create_date_list_sorted = g_list_sort (create_date_list, + compare_files_by_last_created); + + for (l = selection, l2 = create_date_list_sorted; l2 != NULL; l = l->next, l2 = l2->next) { + CreateDateElem *elem = l2->data; + l->data = elem->file; + } + + g_list_free_full (create_date_list, g_free); + } + + return selection; +} + +static void +cursor_next (QueryData *query_data, + TrackerSparqlCursor *cursor) +{ + tracker_sparql_cursor_next_async (cursor, + NULL, + on_cursor_callback, + query_data); +} + +static void +on_cursor_callback (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GHashTable *hash_table; + TrackerSparqlCursor *cursor; + gboolean success; + QueryData *query_data; + GError *error; + GList *l; + FileMetadata *metadata; + FileMetadata *metadata_clear; + GDateTime *datetime; + gchar *date; + const gchar *file_name; + const gchar *creation_date; + const gchar *year; + const gchar *month; + const gchar *day; + const gchar *hours; + const gchar *minutes; + const gchar *seconds; + const gchar *equipment; + const gchar *season_number; + const gchar *episode_number; + const gchar *track_number; + const gchar *artist_name; + const gchar *title; + const gchar *album_name; + + error = NULL; + metadata = NULL; + + cursor = TRACKER_SPARQL_CURSOR (object); + query_data = user_data; + hash_table = query_data->hash_table; + + success = tracker_sparql_cursor_next_finish (cursor, result, &error); + if (!success) { + g_clear_error (&error); + g_clear_object (&cursor); + + nautilus_batch_rename_dialog_query_finished (query_data->dialog, + query_data->hash_table, + query_data->selection_metadata); + + return; + } + + creation_date = tracker_sparql_cursor_get_string (cursor, CREATION_DATE_INDEX, NULL); + + year = tracker_sparql_cursor_get_string (cursor, YEAR_INDEX, NULL); + month = tracker_sparql_cursor_get_string (cursor, MONTH_INDEX, NULL); + day = tracker_sparql_cursor_get_string (cursor, DAY_INDEX, NULL); + hours = tracker_sparql_cursor_get_string (cursor, HOURS_INDEX, NULL); + minutes = tracker_sparql_cursor_get_string (cursor, MINUTES_INDEX, NULL); + seconds = tracker_sparql_cursor_get_string (cursor, SECONDS_INDEX, NULL); + equipment = tracker_sparql_cursor_get_string (cursor, CAMERA_MODEL_INDEX, NULL); + season_number = tracker_sparql_cursor_get_string (cursor, SEASON_INDEX, NULL); + episode_number = tracker_sparql_cursor_get_string (cursor, EPISODE_NUMBER_INDEX, NULL); + track_number = tracker_sparql_cursor_get_string (cursor, TRACK_NUMBER_INDEX, NULL); + artist_name = tracker_sparql_cursor_get_string (cursor, ARTIST_NAME_INDEX, NULL); + title = tracker_sparql_cursor_get_string (cursor, TITLE_INDEX, NULL); + album_name = tracker_sparql_cursor_get_string (cursor, ALBUM_NAME_INDEX, NULL); + + /* creation date used for sorting criteria */ + if (creation_date == NULL) { + if (hash_table != NULL) + g_hash_table_destroy (hash_table); + + query_data->hash_table = NULL; + query_data->have_creation_date = FALSE; + } else { + if (query_data->have_creation_date){ + g_hash_table_insert (hash_table, + g_strdup (tracker_sparql_cursor_get_string (cursor, 0, NULL)), + GINT_TO_POINTER (g_hash_table_size (hash_table))); + } + } + file_name = tracker_sparql_cursor_get_string (cursor, FILE_NAME_INDEX, NULL); + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata = l->data; + + if (g_strcmp0 (file_name, metadata->file_name->str) == 0) + break; + } + + /* Metadata to be used in file name + * creation date */ + if (query_data->have_creation_date) { + if (!creation_date) { + query_data->have_creation_date = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->creation_date != NULL) { + g_string_free (metadata_clear->creation_date, TRUE); + metadata_clear->creation_date = NULL; + } + } + } else { + datetime = g_date_time_new_local (atoi (year), + atoi (month), + atoi (day), + atoi (hours), + atoi (minutes), + atoi (seconds)); + + date = g_date_time_format (datetime, "%x"); + + if (strstr (date, "/") != NULL) { + metadata->creation_date = batch_rename_replace (date, "/", "-"); + } else { + metadata->creation_date = g_string_new (date); + } + + g_free (date); + } + } + + /* equipment */ + if (query_data->have_equipment) { + if (equipment == NULL) { + query_data->have_equipment = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->equipment != NULL) { + g_string_free (metadata_clear->equipment, TRUE); + metadata_clear->equipment = NULL; + } + } + } else { + metadata->equipment = g_string_new (equipment); + } + } + + /* season number */ + if (query_data->have_season) { + if (season_number == NULL) { + query_data->have_season = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->season != NULL) { + g_string_free (metadata_clear->season, TRUE); + metadata_clear->season = NULL; + } + } + } else { + metadata->season = g_string_new (season_number); + } + } + + /* episode number */ + if (query_data->have_episode_number) { + if (episode_number == NULL) { + query_data->have_episode_number = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->episode_number != NULL) { + g_string_free (metadata_clear->episode_number, TRUE); + metadata_clear->episode_number = NULL; + } + } + } else { + metadata->episode_number = g_string_new (episode_number); + } + } + + /* track number */ + if (query_data->have_track_number) { + if (track_number == NULL) { + query_data->have_track_number = FALSE; + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->track_number != NULL) { + g_string_free (metadata_clear->track_number, TRUE); + metadata_clear->track_number = NULL; + } + } + } else { + metadata->track_number = g_string_new (track_number); + } + } + + /* artist name */ + if (query_data->have_artist_name) { + if (artist_name == NULL) { + query_data->have_artist_name = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->artist_name != NULL) { + g_string_free (metadata_clear->artist_name, TRUE); + metadata_clear->artist_name = NULL; + } + } + } else { + metadata->artist_name = g_string_new (artist_name); + } + } + + /* title */ + if (query_data->have_title) { + if (title == NULL) { + query_data->have_title = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->title != NULL) { + g_string_free (metadata_clear->title, TRUE); + metadata_clear->title = NULL; + } + } + } else { + metadata->title = g_string_new (title); + } + } + + /* album name */ + if (query_data->have_album_name) { + if (album_name == NULL) { + query_data->have_album_name = FALSE; + + for (l = query_data->selection_metadata; l != NULL; l = l->next) { + metadata_clear = l->data; + + if (metadata_clear->album_name != NULL) { + g_string_free (metadata_clear->album_name, TRUE); + metadata_clear->album_name = NULL; + } + } + } else { + metadata->album_name = g_string_new (album_name); + } + } + + /* Get next */ + cursor_next (query_data, cursor); +} + +static void +batch_rename_dialog_query_callback (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + TrackerSparqlConnection *connection; + TrackerSparqlCursor *cursor; + QueryData *query_data; + GError *error; + + error = NULL; + + connection = TRACKER_SPARQL_CONNECTION (object); + query_data = user_data; + + cursor = tracker_sparql_connection_query_finish (connection, + result, + &error); + + if (error != NULL) { + g_warning ("Error on batch rename query for metadata: %s", error->message); + g_error_free (error); + + nautilus_batch_rename_dialog_query_finished (query_data->dialog, + query_data->hash_table, + query_data->selection_metadata); + } else { + cursor_next (query_data, cursor); + } +} + +void +check_metadata_for_selection (NautilusBatchRenameDialog *dialog, + GList *selection) +{ + TrackerSparqlConnection *connection; + GString *query; + GHashTable *hash_table; + GList *l; + NautilusFile *file; + GError *error; + QueryData *query_data; + gchar *file_name; + FileMetadata *metadata; + GList *selection_metadata; + + error = NULL; + selection_metadata = NULL; + + query = g_string_new ("SELECT " + "nfo:fileName(?file) " + "nie:contentCreated(?file) " + "year(nie:contentCreated(?file)) " + "month(nie:contentCreated(?file)) " + "day(nie:contentCreated(?file)) " + "hours(nie:contentCreated(?file)) " + "minutes(nie:contentCreated(?file)) " + "seconds(nie:contentCreated(?file)) " + "nfo:model(nfo:equipment(?file)) " + "nmm:season(?file) " + "nmm:episodeNumber(?file) " + "nmm:trackNumber(?file) " + "nmm:artistName(nmm:performer(?file)) " + "nie:title(?file) " + "nmm:albumTitle(nmm:musicAlbum(?file)) " + "WHERE { ?file a nfo:FileDataObject. "); + + g_string_append_printf (query, + "FILTER(tracker:uri-is-parent('%s', nie:url(?file))) ", + nautilus_file_get_parent_uri (NAUTILUS_FILE (selection->data))); + + for (l = selection; l != NULL; l = l->next) { + file = NAUTILUS_FILE (l->data); + file_name = nautilus_file_get_name (file); + + if (l == selection) + g_string_append_printf (query, + "FILTER (nfo:fileName(?file) = '%s' ", + file_name); + else + g_string_append_printf (query, + "|| nfo:fileName(?file) = '%s' ", + file_name); + + metadata = g_new (FileMetadata, 1); + metadata->file_name = g_string_new (file_name); + metadata->creation_date = NULL; + metadata->equipment = NULL; + metadata->season = NULL; + metadata->episode_number = NULL; + metadata->track_number = NULL; + metadata->artist_name = NULL; + metadata->title = NULL; + metadata->album_name = NULL; + + selection_metadata = g_list_append (selection_metadata, metadata); + + g_free (file_name); + } + + g_string_append (query, ")} ORDER BY ASC(nie:contentCreated(?file))"); + + connection = tracker_sparql_connection_get (NULL, &error); + if (!connection) { + g_error_free (error); + + return; + } + + hash_table = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + + query_data = g_new (QueryData, 1); + query_data->hash_table = hash_table; + query_data->dialog = dialog; + query_data->selection_metadata = selection_metadata; + + query_data->have_season = TRUE; + query_data->have_creation_date = TRUE; + query_data->have_artist_name = TRUE; + query_data->have_track_number = TRUE; + query_data->have_equipment = TRUE; + query_data->have_episode_number = TRUE; + query_data->have_title = TRUE; + query_data->have_album_name = TRUE; + + /* Make an asynchronous query to the store */ + tracker_sparql_connection_query_async (connection, + query->str, + NULL, + batch_rename_dialog_query_callback, + query_data); + + g_object_unref (connection); + g_string_free (query, TRUE); +} + +GList* +batch_rename_files_get_distinct_parents (GList *selection) +{ + GList *result; + GList *l1; + GList *l2; + NautilusFile *file; + gboolean exists; + gchar *parent_uri; + + result = NULL; + + for (l1 = selection; l1 != NULL; l1 = l1->next) { + exists = FALSE; + + file = NAUTILUS_FILE (l1->data); + parent_uri = nautilus_file_get_parent_uri (file); + + for (l2 = result; l2 != NULL; l2 = l2->next) + if (g_strcmp0 (parent_uri, l2->data) == 0) { + exists = TRUE; + break; + } + + if (!exists) { + result = g_list_prepend (result, parent_uri); + } else { + g_free (parent_uri); + } + } + + return result; +}
\ No newline at end of file diff --git a/src/nautilus-batch-rename-utilities.h b/src/nautilus-batch-rename-utilities.h new file mode 100644 index 000000000..e343b25e3 --- /dev/null +++ b/src/nautilus-batch-rename-utilities.h @@ -0,0 +1,63 @@ +/* nautilus-batch-rename-utilities.c + * + * Copyright (C) 2016 Alexandru Pandelea <alexandru.pandelea@gmail.com> + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#ifndef NAUTILUS_BATCH_RENAME_UTILITIES_H +#define NAUTILUS_BATCH_RENAME_UTILITIES_H + +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <tracker-sparql.h> + +GList* batch_rename_dialog_get_new_names_list (NautilusBatchRenameDialogMode mode, + GList *selection, + GList *tags_list, + GList *selection_metadata, + gchar *entry_text, + gchar *replace_text); + +GList* file_names_list_has_duplicates (NautilusBatchRenameDialog *dialog, + NautilusDirectory *model, + GList *names, + GList *selection, + GList *parents_list, + GCancellable *cancellable); + +GList* nautilus_batch_rename_dialog_sort (GList *selection, + SortingMode mode, + GHashTable *creation_date_table); + +void check_metadata_for_selection (NautilusBatchRenameDialog *dialog, + GList *selection); + +gboolean selection_has_single_parent (GList *selection); + +void string_free (gpointer mem); + +void conflict_data_free (gpointer mem); + +GList* batch_rename_files_get_distinct_parents (GList *selection); + +gboolean file_name_conflicts_with_results (GList *selection, + GList *new_names, + GString *old_name, + gchar *parent_uri); + +GString* batch_rename_replace_label_text (gchar *label, + const gchar *substr); + +#endif /* NAUTILUS_BATCH_RENAME_UTILITIES_H */
\ No newline at end of file diff --git a/src/nautilus-file-private.h b/src/nautilus-file-private.h index c4986511b..f0ecf7294 100644 --- a/src/nautilus-file-private.h +++ b/src/nautilus-file-private.h @@ -214,6 +214,9 @@ struct NautilusFileDetails typedef struct { NautilusFile *file; + GList *files; + gint renamed_files; + gint skipped_files; GCancellable *cancellable; NautilusFileOperationCallback callback; gpointer callback_data; diff --git a/src/nautilus-file-undo-operations.c b/src/nautilus-file-undo-operations.c index e88054722..3ec2832f8 100644 --- a/src/nautilus-file-undo-operations.c +++ b/src/nautilus-file-undo-operations.c @@ -994,6 +994,292 @@ nautilus_file_undo_info_rename_set_data_post (NautilusFileUndoInfoRename *self, self->priv->new_file = g_object_ref (new_file); } +/* batch rename */ +G_DEFINE_TYPE (NautilusFileUndoInfoBatchRename, nautilus_file_undo_info_batch_rename, NAUTILUS_TYPE_FILE_UNDO_INFO); + +struct _NautilusFileUndoInfoBatchRenameDetails { + GList *old_files; + GList *new_files; + GList *old_display_names; + GList *new_display_names; +}; + +static void +batch_rename_strings_func (NautilusFileUndoInfo *info, + gchar **undo_label, + gchar **undo_description, + gchar **redo_label, + gchar **redo_description) +{ + NautilusFileUndoInfoBatchRename *self = NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME (info); + + *undo_description = g_strdup_printf (_("Batch rename '%d' files"), + g_list_length (self->priv->new_files)); + *redo_description = g_strdup_printf (_("Batch rename '%d' files"), + g_list_length (self->priv->new_files)); + + *undo_label = g_strdup (_("_Undo Batch rename")); + *redo_label = g_strdup (_("_Redo Batch rename")); +} + +static void +batch_rename_redo_func (NautilusFileUndoInfo *info, + GtkWindow *parent_window) +{ + NautilusFileUndoInfoBatchRename *self = NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME (info); + + GList *l, *files; + NautilusFile *file; + GFile *old_file; + GFile *new_file; + GList *l1; + GList *l2; + GList *l3; + GList *l4; + GList *l5; + GList *l6; + GList *l7; + gchar *file_name; + gchar *old_file_name; + GString *new_file_name; + GString *new_name; + GString *old_name; + + files = NULL; + + for (l = self->priv->old_files; l != NULL; l = l->next) { + old_file = l->data; + + file = nautilus_file_get (old_file); + files = g_list_append (files, file); + } + + for (l1 = self->priv->new_display_names, l2 = files; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + old_file_name = nautilus_file_get_name (NAUTILUS_FILE (l2->data)); + new_file_name = l1->data; + + for (l3 = files, l4 = self->priv->new_display_names, l5 = self->priv->old_display_names, l6 = self->priv->old_files, l7 = self->priv->new_files; + l3 != NULL && l4 != NULL && l5 != NULL && l6 != NULL && l7 != NULL; + l3 = l3->next, l4 = l4->next, l5 = l5->next, l6 = l6->next, l7 = l7->next) { + file_name = nautilus_file_get_name (NAUTILUS_FILE (l3->data)); + if (l3 != l2 && g_strcmp0 (file_name, new_file_name->str) == 0) { + + file = NAUTILUS_FILE (l3->data); + new_name = l4->data; + old_name = l5->data; + old_file = l6->data; + new_file = l7->data; + + files = g_list_remove_link (files, l3); + self->priv->new_display_names = g_list_remove_link (self->priv->new_display_names, l4); + self->priv->old_display_names = g_list_remove_link (self->priv->old_display_names, l5); + self->priv->old_files = g_list_remove_link (self->priv->old_files, l6); + self->priv->new_files = g_list_remove_link (self->priv->new_files, l7); + + files = g_list_prepend (files, file); + self->priv->new_display_names = g_list_prepend (self->priv->new_display_names, new_name); + self->priv->old_display_names = g_list_prepend (self->priv->old_display_names, old_name); + self->priv->old_files = g_list_prepend (self->priv->old_files, old_file); + self->priv->new_files = g_list_prepend (self->priv->new_files, new_file); + + g_free (file_name); + + break; + } + + g_free (file_name); + } + + g_free (old_file_name); + } + + nautilus_file_batch_rename (files, self->priv->new_display_names, file_undo_info_operation_callback, self); +} + +static void +batch_rename_undo_func (NautilusFileUndoInfo *info, + GtkWindow *parent_window) +{ + NautilusFileUndoInfoBatchRename *self = NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME (info); + + GList *l, *files; + NautilusFile *file; + GFile *new_file; + GFile *old_file; + GList *l1; + GList *l2; + GList *l3; + GList *l4; + GList *l5; + GList *l6; + GList *l7; + gchar *file_name; + gchar *old_file_name; + GString *new_file_name; + GString *new_name; + GString *old_name; + + files = NULL; + + for (l = self->priv->new_files; l != NULL; l = l->next) { + new_file = l->data; + + file = nautilus_file_get (new_file); + files = g_list_append (files, file); + } + + for (l1 = self->priv->old_display_names, l2 = files; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + old_file_name = nautilus_file_get_name (NAUTILUS_FILE (l2->data)); + new_file_name = l1->data; + + for (l3 = files, l4 = self->priv->old_display_names, l5 = self->priv->new_display_names, l6 = self->priv->old_files, l7 = self->priv->new_files; + l3 != NULL && l4 != NULL && l5 != NULL && l6 != NULL && l7 != NULL; + l3 = l3->next, l4 = l4->next, l5 = l5->next, l6 = l6->next, l7 = l7->next) { + file_name = nautilus_file_get_name (NAUTILUS_FILE (l3->data)); + if (l3 != l2 && g_strcmp0 (file_name, new_file_name->str) == 0) { + file = NAUTILUS_FILE (l3->data); + new_name = l4->data; + old_name = l5->data; + old_file = l6->data; + new_file = l7->data; + + files = g_list_remove_link (files, l3); + self->priv->old_display_names = g_list_remove_link (self->priv->old_display_names, l4); + self->priv->new_display_names = g_list_remove_link (self->priv->new_display_names, l5); + self->priv->old_files = g_list_remove_link (self->priv->old_files, l6); + self->priv->new_files = g_list_remove_link (self->priv->new_files, l7); + + files = g_list_prepend (files, file); + self->priv->old_display_names = g_list_prepend (self->priv->old_display_names, new_name); + self->priv->new_display_names = g_list_prepend (self->priv->new_display_names, old_name); + self->priv->old_files = g_list_prepend (self->priv->old_files, old_file); + self->priv->new_files = g_list_prepend (self->priv->new_files, new_file); + + g_free (file_name); + + break; + } + + g_free (file_name); + } + + g_free (old_file_name); + } + + nautilus_file_batch_rename (files, self->priv->old_display_names, file_undo_info_operation_callback, self); +} + +static void +nautilus_file_undo_info_batch_rename_init (NautilusFileUndoInfoBatchRename *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, nautilus_file_undo_info_batch_rename_get_type (), + NautilusFileUndoInfoBatchRenameDetails); +} + +static void +nautilus_file_undo_info_batch_rename_finalize (GObject *obj) +{ + GList *l; + GFile *file; + GString *string; + NautilusFileUndoInfoBatchRename *self = NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME (obj); + + for (l = self->priv->new_files; l != NULL; l = l->next){ + file = l->data; + + g_clear_object (&file); + } + + for (l = self->priv->old_files; l != NULL; l = l->next){ + file = l->data; + + g_clear_object (&file); + } + + for (l = self->priv->new_display_names; l != NULL; l = l->next) { + string = l->data; + + g_string_free (string, TRUE); + } + + for (l = self->priv->old_display_names; l != NULL; l = l->next) { + string = l->data; + + g_string_free (string, TRUE); + } + + g_list_free (self->priv->new_files); + g_list_free (self->priv->old_files); + g_list_free (self->priv->new_display_names); + g_list_free (self->priv->old_display_names); + + G_OBJECT_CLASS (nautilus_file_undo_info_batch_rename_parent_class)->finalize (obj); +} + +static void +nautilus_file_undo_info_batch_rename_class_init (NautilusFileUndoInfoBatchRenameClass *klass) +{ + GObjectClass *oclass = G_OBJECT_CLASS (klass); + NautilusFileUndoInfoClass *iclass = NAUTILUS_FILE_UNDO_INFO_CLASS (klass); + + oclass->finalize = nautilus_file_undo_info_batch_rename_finalize; + + iclass->undo_func = batch_rename_undo_func; + iclass->redo_func = batch_rename_redo_func; + iclass->strings_func = batch_rename_strings_func; + + g_type_class_add_private (klass, sizeof (NautilusFileUndoInfoBatchRenameDetails)); +} + +NautilusFileUndoInfo * +nautilus_file_undo_info_batch_rename_new (gint item_count) +{ + return g_object_new (NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME, + "op-type", NAUTILUS_FILE_UNDO_OP_BATCH_RENAME, + "item-count", item_count, + NULL); +} + +void +nautilus_file_undo_info_batch_rename_set_data_pre (NautilusFileUndoInfoBatchRename *self, + GList *old_files) +{ + GList *l; + GString *old_name; + GFile *file; + + self->priv->old_files = old_files; + self->priv->old_display_names = NULL; + + for (l = old_files; l != NULL; l = l->next) { + file = l->data; + + old_name = g_string_new (g_file_get_basename (file)); + + self->priv->old_display_names = g_list_append (self->priv->old_display_names, old_name); + } +} + +void +nautilus_file_undo_info_batch_rename_set_data_post (NautilusFileUndoInfoBatchRename *self, + GList *new_files) +{ + GList *l; + GString *new_name; + GFile *file; + + self->priv->new_files = new_files; + self->priv->new_display_names = NULL; + + for (l = new_files; l != NULL; l = l->next) { + file = l->data; + + new_name = g_string_new (g_file_get_basename (file)); + + self->priv->new_display_names = g_list_append (self->priv->new_display_names, new_name); + } +} + /* trash */ G_DEFINE_TYPE (NautilusFileUndoInfoTrash, nautilus_file_undo_info_trash, NAUTILUS_TYPE_FILE_UNDO_INFO) diff --git a/src/nautilus-file-undo-operations.h b/src/nautilus-file-undo-operations.h index 940246916..630443f10 100644 --- a/src/nautilus-file-undo-operations.h +++ b/src/nautilus-file-undo-operations.h @@ -34,6 +34,7 @@ typedef enum { NAUTILUS_FILE_UNDO_OP_DUPLICATE, NAUTILUS_FILE_UNDO_OP_MOVE, NAUTILUS_FILE_UNDO_OP_RENAME, + NAUTILUS_FILE_UNDO_OP_BATCH_RENAME, NAUTILUS_FILE_UNDO_OP_CREATE_EMPTY_FILE, NAUTILUS_FILE_UNDO_OP_CREATE_FILE_FROM_TEMPLATE, NAUTILUS_FILE_UNDO_OP_CREATE_FOLDER, @@ -188,6 +189,34 @@ void nautilus_file_undo_info_rename_set_data_pre (NautilusFileUndoInfoRename *se void nautilus_file_undo_info_rename_set_data_post (NautilusFileUndoInfoRename *self, GFile *new_file); +/* batch rename */ +#define NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME (nautilus_file_undo_info_batch_rename_get_type ()) +#define NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME, NautilusFileUndoInfoBatchRename)) +#define NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME, NautilusFileUndoInfoBatchRenameClass)) +#define NAUTILUS_IS_FILE_UNDO_INFO_BATCH_RENAME(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME)) +#define NAUTILUS_IS_FILE_UNDO_INFO_BATCH_RENAME_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME)) +#define NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), NAUTILUS_TYPE_FILE_UNDO_INFO_BATCH_RENAME, NautilusFileUndoInfoBatchRenameClass)) + +typedef struct _NautilusFileUndoInfoBatchRename NautilusFileUndoInfoBatchRename; +typedef struct _NautilusFileUndoInfoBatchRenameClass NautilusFileUndoInfoBatchRenameClass; +typedef struct _NautilusFileUndoInfoBatchRenameDetails NautilusFileUndoInfoBatchRenameDetails; + +struct _NautilusFileUndoInfoBatchRename { + NautilusFileUndoInfo parent; + NautilusFileUndoInfoBatchRenameDetails *priv; +}; + +struct _NautilusFileUndoInfoBatchRenameClass { + NautilusFileUndoInfoClass parent_class; +}; + +GType nautilus_file_undo_info_batch_rename_get_type (void) G_GNUC_CONST; +NautilusFileUndoInfo *nautilus_file_undo_info_batch_rename_new (gint item_count); +void nautilus_file_undo_info_batch_rename_set_data_pre (NautilusFileUndoInfoBatchRename *self, + GList *old_files); +void nautilus_file_undo_info_batch_rename_set_data_post (NautilusFileUndoInfoBatchRename *self, + GList *new_files); + /* trash */ #define NAUTILUS_TYPE_FILE_UNDO_INFO_TRASH (nautilus_file_undo_info_trash_get_type ()) #define NAUTILUS_FILE_UNDO_INFO_TRASH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), NAUTILUS_TYPE_FILE_UNDO_INFO_TRASH, NautilusFileUndoInfoTrash)) diff --git a/src/nautilus-file-utilities.c b/src/nautilus-file-utilities.c index 262f48516..6ed4127a0 100644 --- a/src/nautilus-file-utilities.c +++ b/src/nautilus-file-utilities.c @@ -1221,3 +1221,19 @@ nautilus_ensure_extension_points (void) } #endif /* !NAUTILUS_OMIT_SELF_CHECK */ + +gboolean +nautilus_file_can_rename_files (GList *files) +{ + GList *l; + NautilusFile *file; + + for (l = files; l != NULL; l = l->next) { + file = NAUTILUS_FILE (l->data); + + if (!nautilus_file_can_rename (file)) + return FALSE; + } + + return TRUE; +} diff --git a/src/nautilus-file-utilities.h b/src/nautilus-file-utilities.h index 4b26a8caa..c4d863fec 100644 --- a/src/nautilus-file-utilities.h +++ b/src/nautilus-file-utilities.h @@ -119,4 +119,6 @@ char * nautilus_get_common_filename_prefix_from_filenames (GList *filename_list, void nautilus_ensure_extension_points (void); void nautilus_ensure_extension_builtins (void); +gboolean nautilus_file_can_rename_files (GList *files); + #endif /* NAUTILUS_FILE_UTILITIES_H */ diff --git a/src/nautilus-file.c b/src/nautilus-file.c index 60e2f9b38..c68753e98 100644 --- a/src/nautilus-file.c +++ b/src/nautilus-file.c @@ -1648,15 +1648,36 @@ nautilus_file_operation_new (NautilusFile *file, static void nautilus_file_operation_remove (NautilusFileOperation *op) { + GList *l; + NautilusFile *file; + op->file->details->operations_in_progress = g_list_remove (op->file->details->operations_in_progress, op); + + + for (l = op->files; l != NULL; l = l->next) { + file = NAUTILUS_FILE (l->data); + file->details->operations_in_progress = g_list_remove + (file->details->operations_in_progress, op); + } } void nautilus_file_operation_free (NautilusFileOperation *op) { + NautilusFile *file; + GList *l; + nautilus_file_operation_remove (op); - nautilus_file_unref (op->file); + + if (op->files == NULL) + nautilus_file_unref (op->file); + else + for (l = op->files; l != NULL; l = l->next) { + file = NAUTILUS_FILE (l->data); + nautilus_file_unref (file); + } + g_object_unref (op->cancellable); if (op->free_data) { op->free_data (op->data); @@ -1680,10 +1701,12 @@ nautilus_file_operation_complete (NautilusFileOperation *op, * as "changing back". */ nautilus_file_operation_remove (op); - nautilus_file_changed (op->file); - if (op->callback) { + + if (op->files == NULL) + nautilus_file_changed (op->file); + + if (op->callback) (* op->callback) (op->file, result_file, error, op->callback_data); - } if (error != NULL) { g_clear_object (&op->undo_info); @@ -1759,6 +1782,86 @@ rename_get_info_callback (GObject *source_object, } } +typedef struct { + NautilusFileOperation *op; + NautilusFile *file; +} BatchRenameData; + +static void +batch_rename_get_info_callback (GObject *source_object, + GAsyncResult *res, + gpointer callback_data) +{ + NautilusFileOperation *op; + NautilusDirectory *directory; + NautilusFile *existing_file; + char *old_uri; + char *new_uri; + const char *new_name; + GFileInfo *new_info; + GError *error; + BatchRenameData *data; + + data = callback_data; + + op = data->op; + op->file = data->file; + + error = NULL; + new_info = g_file_query_info_finish (G_FILE (source_object), res, &error); + if (new_info != NULL) { + old_uri = nautilus_file_get_uri (op->file); + + new_name = g_file_info_get_name (new_info); + + directory = op->file->details->directory; + + /* If there was another file by the same name in this + * directory and it is not the same file that we are + * renaming, mark it gone. + */ + existing_file = nautilus_directory_find_file_by_name (directory, new_name); + if (existing_file != NULL && existing_file != op->file) { + nautilus_file_mark_gone (existing_file); + nautilus_file_changed (existing_file); + } + + update_info_and_name (op->file, new_info); + + new_uri = nautilus_file_get_uri (op->file); + nautilus_directory_moved (old_uri, new_uri); + g_free (new_uri); + g_free (old_uri); + + /* the rename could have affected the display name if e.g. + * we're in a vfolder where the name comes from a desktop file + * and a rename affects the contents of the desktop file. + */ + if (op->file->details->got_custom_display_name) { + nautilus_file_invalidate_attributes (op->file, + NAUTILUS_FILE_ATTRIBUTE_INFO | + NAUTILUS_FILE_ATTRIBUTE_LINK_INFO); + } + + g_object_unref (new_info); + } + + op->renamed_files++; + + if (op->renamed_files + op->skipped_files == g_list_length (op->files)) { + nautilus_file_operation_complete (op, NULL, error); + } + + if (op->files == NULL) + nautilus_file_operation_complete (op, NULL, error); + + g_free (data); + + if (error) { + g_error_free (error); + } +} + static void rename_callback (GObject *source_object, GAsyncResult *res, @@ -1779,7 +1882,6 @@ rename_callback (GObject *source_object, nautilus_file_undo_info_rename_set_data_post (NAUTILUS_FILE_UNDO_INFO_RENAME (op->undo_info), new_file); } - g_file_query_info_async (new_file, NAUTILUS_FILE_DEFAULT_ATTRIBUTES, 0, @@ -1812,6 +1914,217 @@ nautilus_file_rename (NautilusFile *file, callback_data); } +static gchar* +nautilus_file_can_rename_file (NautilusFile *file, + const char *new_name, + NautilusFileOperationCallback callback, + gpointer callback_data) +{ + GError *error; + gboolean is_renameable_desktop_file; + gboolean success; + gboolean name_changed; + gchar *new_file_name; + gchar *uri; + gchar *old_name; + + is_renameable_desktop_file = + is_desktop_file (file) && can_rename_desktop_file (file); + + /* Return an error for incoming names containing path separators. + * But not for .desktop files as '/' are allowed for them */ + if (strstr (new_name, "/") != NULL && !is_renameable_desktop_file) { + error = g_error_new (G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("Slashes are not allowed in filenames")); + if (callback != NULL) + (* callback) (file, NULL, error, callback_data); + g_error_free (error); + return NULL; + } + + /* Can't rename a file that's already gone. + * We need to check this here because there may be a new + * file with the same name. + */ + if (nautilus_file_rename_handle_file_gone (file, callback, callback_data)) { + return NULL; + } + + /* Test the name-hasn't-changed case explicitly, for two reasons. + * (1) rename returns an error if new & old are same. + * (2) We don't want to send file-changed signal if nothing changed. + */ + if (!is_renameable_desktop_file && + name_is (file, new_name)) { + if (callback != NULL) + (* callback) (file, NULL, NULL, callback_data); + return NULL; + } + + /* Self-owned files can't be renamed. Test the name-not-actually-changing + * case before this case. + */ + if (nautilus_file_is_self_owned (file)) { + /* Claim that something changed even if the rename + * failed. This makes it easier for some clients who + * see the "reverting" to the old name as "changing + * back". + */ + nautilus_file_changed (file); + error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + _("Toplevel files cannot be renamed")); + + if (callback != NULL) + (* callback) (file, NULL, error, callback_data); + g_error_free (error); + + return NULL; + } + + if (is_renameable_desktop_file) { + /* Don't actually change the name if the new name is the same. + * This helps for the vfolder method where this can happen and + * we want to minimize actual changes + */ + uri = nautilus_file_get_uri (file); + old_name = nautilus_link_local_get_text (uri); + if (old_name != NULL && strcmp (new_name, old_name) == 0) { + success = TRUE; + name_changed = FALSE; + } else { + success = nautilus_link_local_set_text (uri, new_name); + name_changed = TRUE; + } + g_free (old_name); + g_free (uri); + + if (!success) { + error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + _("Probably the content of the file is an invalid desktop file format")); + if (callback != NULL) + (* callback) (file, NULL, error, callback_data); + g_error_free (error); + return NULL; + } + new_file_name = g_strdup_printf ("%s.desktop", new_name); + new_file_name = g_strdelimit (new_file_name, "/", '-'); + + if (name_is (file, new_file_name)) { + if (name_changed) { + nautilus_file_invalidate_attributes (file, + NAUTILUS_FILE_ATTRIBUTE_INFO | + NAUTILUS_FILE_ATTRIBUTE_LINK_INFO); + } + + if (callback != NULL) + (* callback) (file, NULL, NULL, callback_data); + g_free (new_file_name); + return NULL; + } + } else { + new_file_name = g_strdup (new_name); + } + + return new_file_name; +} + +static void +real_batch_rename (GList *files, + GList *new_names, + NautilusFileOperationCallback callback, + gpointer callback_data) +{ + GList *l1, *l2, *old_files, *new_files; + NautilusFileOperation *op; + GFile *location; + gchar *new_file_name; + GString *new_name; + NautilusFile *file; + GError *error; + GFile *new_file; + BatchRenameData *data; + + error = NULL; + old_files = NULL; + new_files = NULL; + + /* Set up a batch renaming operation. */ + op = nautilus_file_operation_new (files->data, callback, callback_data); + op->files = files; + op->renamed_files = 0; + op->skipped_files = 0; + + for (l1 = files->next; l1 != NULL; l1 = l1->next) { + file = NAUTILUS_FILE (l1->data); + + file->details->operations_in_progress = g_list_prepend (file->details->operations_in_progress, + op); + } + + for (l1 = files, l2 = new_names; l1 != NULL && l2 != NULL; l1 = l1->next, l2 = l2->next) { + file = NAUTILUS_FILE (l1->data); + new_name = l2->data; + + location = nautilus_file_get_location (file); + old_files = g_list_append (old_files, location); + + new_file_name = nautilus_file_can_rename_file (file, + new_name->str, + callback, + callback_data); + + if (new_file_name == NULL) { + op->skipped_files++; + + new_file = nautilus_file_get_location (file); + new_files = g_list_append (new_files, new_file); + + continue; + } + + g_assert (G_IS_FILE (location)); + + /* Do the renaming. */ + new_file = g_file_set_display_name (location, + new_file_name, + op->cancellable, + &error); + + data = g_new0 (BatchRenameData, 1); + data->op = op; + data->file = file; + + new_files = g_list_append (new_files, new_file); + + g_file_query_info_async (new_file, + NAUTILUS_FILE_DEFAULT_ATTRIBUTES, + 0, + G_PRIORITY_DEFAULT, + op->cancellable, + batch_rename_get_info_callback, + data); + + if (error != NULL) { + g_warning ("Batch rename for file \"%s\" failed", nautilus_file_get_name (file)); + g_error_free (error); + error = NULL; + } + } + + /* Tell the undo manager a batch rename is taking place */ + if (!nautilus_file_undo_manager_is_operating ()) { + op->undo_info = nautilus_file_undo_info_batch_rename_new (g_list_length (new_files)); + + nautilus_file_undo_info_batch_rename_set_data_pre (NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME (op->undo_info), + old_files); + + nautilus_file_undo_info_batch_rename_set_data_post (NAUTILUS_FILE_UNDO_INFO_BATCH_RENAME (op->undo_info), + new_files); + + nautilus_file_undo_manager_set_action (op->undo_info); + } +} + gboolean nautilus_file_rename_handle_file_gone (NautilusFile *file, NautilusFileOperationCallback callback, @@ -1820,7 +2133,7 @@ nautilus_file_rename_handle_file_gone (NautilusFile *file, GError *error; if (nautilus_file_is_gone (file)) { - /* Claim that something changed even if the rename + /* Claim that something changed even if the rename * failed. This makes it easier for some clients who * see the "reverting" to the old name as "changing * back". @@ -1836,6 +2149,18 @@ nautilus_file_rename_handle_file_gone (NautilusFile *file, return FALSE; } +void +nautilus_file_batch_rename (GList *files, + GList *new_names, + NautilusFileOperationCallback callback, + gpointer callback_data) +{ + real_batch_rename (files, + new_names, + callback, + callback_data); +} + static void real_rename (NautilusFile *file, const char *new_name, @@ -1843,107 +2168,21 @@ real_rename (NautilusFile *file, gpointer callback_data) { NautilusFileOperation *op; - char *uri; char *old_name; char *new_file_name; - gboolean success, name_changed; - gboolean is_renameable_desktop_file; GFile *location; - GError *error; g_return_if_fail (NAUTILUS_IS_FILE (file)); g_return_if_fail (new_name != NULL); g_return_if_fail (callback != NULL); - is_renameable_desktop_file = - is_desktop_file (file) && can_rename_desktop_file (file); - - /* Return an error for incoming names containing path separators. - * But not for .desktop files as '/' are allowed for them */ - if (strstr (new_name, "/") != NULL && !is_renameable_desktop_file) { - error = g_error_new (G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, - _("Slashes are not allowed in filenames")); - (* callback) (file, NULL, error, callback_data); - g_error_free (error); - return; - } - - /* Can't rename a file that's already gone. - * We need to check this here because there may be a new - * file with the same name. - */ - if (nautilus_file_rename_handle_file_gone (file, callback, callback_data)) { - return; - } - /* Test the name-hasn't-changed case explicitly, for two reasons. - * (1) rename returns an error if new & old are same. - * (2) We don't want to send file-changed signal if nothing changed. - */ - if (!is_renameable_desktop_file && - name_is (file, new_name)) { - (* callback) (file, NULL, NULL, callback_data); - return; - } + new_file_name = nautilus_file_can_rename_file (file, + new_name, + callback, + callback_data); - /* Self-owned files can't be renamed. Test the name-not-actually-changing - * case before this case. - */ - if (nautilus_file_is_self_owned (file)) { - /* Claim that something changed even if the rename - * failed. This makes it easier for some clients who - * see the "reverting" to the old name as "changing - * back". - */ - nautilus_file_changed (file); - error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - _("Toplevel files cannot be renamed")); - - (* callback) (file, NULL, error, callback_data); - g_error_free (error); + if (new_file_name == NULL) return; - } - - if (is_renameable_desktop_file) { - /* Don't actually change the name if the new name is the same. - * This helps for the vfolder method where this can happen and - * we want to minimize actual changes - */ - uri = nautilus_file_get_uri (file); - old_name = nautilus_link_local_get_text (uri); - if (old_name != NULL && strcmp (new_name, old_name) == 0) { - success = TRUE; - name_changed = FALSE; - } else { - success = nautilus_link_local_set_text (uri, new_name); - name_changed = TRUE; - } - g_free (old_name); - g_free (uri); - - if (!success) { - error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, - _("Probably the content of the file is an invalid desktop file format")); - (* callback) (file, NULL, error, callback_data); - g_error_free (error); - return; - } - new_file_name = g_strdup_printf ("%s.desktop", new_name); - new_file_name = g_strdelimit (new_file_name, "/", '-'); - - if (name_is (file, new_file_name)) { - if (name_changed) { - nautilus_file_invalidate_attributes (file, - NAUTILUS_FILE_ATTRIBUTE_INFO | - NAUTILUS_FILE_ATTRIBUTE_LINK_INFO); - } - - (* callback) (file, NULL, NULL, callback_data); - g_free (new_file_name); - return; - } - } else { - new_file_name = g_strdup (new_name); - } /* Set up a renaming operation. */ op = nautilus_file_operation_new (file, callback, callback_data); diff --git a/src/nautilus-file.h b/src/nautilus-file.h index fe226c072..7552dff07 100644 --- a/src/nautilus-file.h +++ b/src/nautilus-file.h @@ -324,6 +324,10 @@ void nautilus_file_rename (Nautilu const char *new_name, NautilusFileOperationCallback callback, gpointer callback_data); +void nautilus_file_batch_rename (GList *files, + GList *new_names, + NautilusFileOperationCallback callback, + gpointer callback_data); void nautilus_file_cancel (NautilusFile *file, NautilusFileOperationCallback callback, gpointer callback_data); diff --git a/src/nautilus-files-view.c b/src/nautilus-files-view.c index cd7aa2772..b1acfe845 100644 --- a/src/nautilus-files-view.c +++ b/src/nautilus-files-view.c @@ -28,6 +28,8 @@ #include "nautilus-files-view.h" #include "nautilus-application.h" +#include "nautilus-batch-rename-dialog.h" +#include "nautilus-batch-rename-utilities.h" #include "nautilus-error-reporting.h" #include "nautilus-file-undo-manager.h" #include "nautilus-floating-bar.h" @@ -5566,6 +5568,7 @@ real_action_rename (NautilusFilesView *view) { NautilusFile *file; GList *selection; + GtkWidget *dialog; g_assert (NAUTILUS_IS_FILES_VIEW (view)); @@ -5576,6 +5579,21 @@ real_action_rename (NautilusFilesView *view) if (selection->next != NULL) { if (have_bulk_rename_tool ()) { invoke_external_bulk_rename_utility (view, selection); + } else { + GdkCursor *cursor; + GdkDisplay *display; + + display = gtk_widget_get_display (GTK_WIDGET (nautilus_files_view_get_window (view))); + cursor = gdk_cursor_new_from_name (display, "progress"); + gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (nautilus_files_view_get_window (view))), + cursor); + g_object_unref (cursor); + + dialog = nautilus_batch_rename_dialog_new (nautilus_files_view_get_selection (NAUTILUS_VIEW (view)), + nautilus_files_view_get_model (view), + nautilus_files_view_get_window (view)); + + gtk_widget_show (GTK_WIDGET (dialog)); } } else { file = NAUTILUS_FILE (selection->data); @@ -6625,8 +6643,12 @@ real_update_actions_state (NautilusFilesView *view) action = g_action_map_lookup_action (G_ACTION_MAP (view_action_group), "rename"); if (selection_count > 1) { - g_simple_action_set_enabled (G_SIMPLE_ACTION (action), - have_bulk_rename_tool ()); + if (have_bulk_rename_tool()) + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), + have_bulk_rename_tool ()); + else + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), + nautilus_file_can_rename_files (selection)); } else { g_simple_action_set_enabled (G_SIMPLE_ACTION (action), selection_count == 1 && diff --git a/src/resources/css/Adwaita.css b/src/resources/css/Adwaita.css index bc8e596e3..48beeeecf 100644 --- a/src/resources/css/Adwaita.css +++ b/src/resources/css/Adwaita.css @@ -169,3 +169,19 @@ * always allocates at least 1 pixel */ searchbar { border-top: 1px solid @borders; } .searchbar-container { margin-top: -1px; } + +@define-color conflict_bg #fef6b6; + +.conflict-row { + background: @conflict_bg; + color: black; +} + +.conflict-row:hover { + background-color: shade(@conflict_bg, 0.9); +} + +.conflict-row:selected { + background: @theme_selected_bg_color; + color: @theme_selected_fg_color; +}
\ No newline at end of file diff --git a/src/resources/nautilus.gresource.xml b/src/resources/nautilus.gresource.xml index 05b9a4db9..53682b612 100644 --- a/src/resources/nautilus.gresource.xml +++ b/src/resources/nautilus.gresource.xml @@ -18,6 +18,7 @@ <file>ui/nautilus-no-search-results.ui</file> <file>ui/nautilus-folder-is-empty.ui</file> <file>gtk/help-overlay.ui</file> + <file>ui/nautilus-batch-rename-dialog.ui</file> <file alias="gtk/ui/nautilusgtkplacesview.ui">../gtk/nautilusgtkplacesview.ui</file> <file alias="gtk/ui/nautilusgtkplacesviewrow.ui">../gtk/nautilusgtkplacesviewrow.ui</file> <file alias="icons/thumbnail_frame.png">../../icons/thumbnail_frame.png</file> diff --git a/src/resources/ui/nautilus-batch-rename-dialog.ui b/src/resources/ui/nautilus-batch-rename-dialog.ui new file mode 100644 index 000000000..6628abf7e --- /dev/null +++ b/src/resources/ui/nautilus-batch-rename-dialog.ui @@ -0,0 +1,502 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="NautilusBatchRenameDialog" parent="GtkDialog"> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="height-request">563</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <signal name="response" handler="batch_rename_dialog_on_response"/> + <child type="action"> + <object class="GtkButton" id="cancel_button"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + </object> + </child> + <child type="action"> + <object class="GtkButton" id="rename_button"> + <property name="label" translatable="yes">_Rename</property> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="can_default">True</property> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + <action-widgets> + <action-widget response="ok" default="true">rename_button</action-widget> + <action-widget response="cancel">cancel_button</action-widget> + </action-widgets> + <child internal-child="vbox"> + <object class="GtkBox" id="vbox"> + <property name="border-width">0</property> + <child> + <object class="GtkGrid" id="grid"> + <property name="visible">True</property> + <property name="margin">0</property> + <property name="row-spacing">6</property> + <property name="column-spacing">6</property> + <property name="hexpand">True</property> + <property name="row-homogeneous">False</property> + <property name="column-homogeneous">False</property> + <child> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <property name="spacing">15</property> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="margin">20</property> + <child> + <object class="GtkRadioButton" id="format_mode_button"> + <property name="label" translatable="yes">Rename _using a template</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <signal name="toggled" handler="batch_rename_dialog_mode_changed" swapped="yes" /> + </object> + </child> + <child> + <object class="GtkRadioButton" id="replace_mode_button"> + <property name="label" translatable="yes">Find and replace _text</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="group">format_mode_button</property> + <signal name="toggled" handler="batch_rename_dialog_mode_changed" swapped="yes" /> + </object> + </child> + </object> + <packing> + <property name="left-attach">3</property> + <property name="top-attach">0</property> + <property name="width">1</property> + </packing> + </child> + <child> + <object class="GtkStack" id="mode_stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vhomogeneous">False</property> + <property name="hhomogeneous">True</property> + <property name="transition_type">crossfade</property> + <property name="transition_duration">100</property> + <child> + <object class="GtkGrid" id="format_stack_child"> + <property name="visible">True</property> + <property name="margin-left">40</property> + <property name="margin-right">40</property> + <property name="margin-top">0</property> + <property name="margin-bottom">10</property> + <property name="row-spacing">15</property> + <property name="column-spacing">6</property> + <child> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <property name="spacing">0</property> + <property name="visible">True</property> + <property name="halign">center</property> + <child> + <object class="GtkEntry" id="name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="width_request">400</property> + <property name="hexpand">False</property> + <property name="activates-default">True</property> + <signal name="changed" handler="file_names_widget_entry_on_changed" swapped="yes" /> + <signal name="activate" handler="file_names_widget_on_activate" swapped="yes" /> + <signal name="key-press-event" handler="on_key_press_event" swapped="no"/> + </object> + </child> + <child> + <object class="GtkToggleButton" id="add_button"> + <property name="visible">True</property> + <signal name="toggled" handler="add_button_clicked" swapped="yes" /> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">0</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">list-add-symbolic</property> + <property name="icon-size">1</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Add</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + </child> + </object> + </child> + <style> + <class name="linked"/> + </style> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + <property name="width">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="numbering_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Automatic Numbering Order</property> + <property name="can_focus">False</property> + <property name="height-request">35</property> + <property name="sensitive">False</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + <property name="width">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="numbering_order_button"> + <property name="visible">True</property> + <signal name="toggled" handler="numbering_order_button_clicked" swapped="yes" /> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">15</property> + <child> + <object class="GtkLabel" id="numbering_order_label"> + <property name="visible">True</property> + <property name="width-request">180</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Original name (Ascending)</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImage" id="action_icon"> + <property name="visible">True</property> + <property name="icon-name">pan-down-symbolic</property> + <property name="icon-size">1</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left-attach">4</property> + <property name="top-attach">1</property> + <property name="width">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">format</property> + <property name="title" translatable="yes">Format</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="replace_stack_child"> + <property name="visible">True</property> + <property name="margin-left">40</property> + <property name="margin-right">40</property> + <property name="margin-top">0</property> + <property name="margin-bottom">10</property> + <property name="row-spacing">16</property> + <property name="column-spacing">6</property> + <child> + <object class="GtkLabel" id="existing_text_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Existing Text</property> + <property name="can_focus">False</property> + <property name="sensitive">False</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + <property name="width">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="find_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="width_request">375</property> + <property name="hexpand">False</property> + <property name="activates-default">True</property> + <signal name="changed" handler="file_names_widget_entry_on_changed" swapped="yes" /> + <signal name="activate" handler="file_names_widget_on_activate" swapped="yes" /> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="replace_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Replace With</property> + <property name="can_focus">False</property> + <property name="sensitive">False</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + <property name="width">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="replace_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="width_request">375</property> + <signal name="changed" handler="file_names_widget_entry_on_changed" swapped="yes" /> + <signal name="activate" handler="file_names_widget_on_activate" swapped="yes" /> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">1</property> + <property name="width">3</property> + </packing> + </child> + </object> + <packing> + <property name="name">replace</property> + <property name="title" translatable="yes">Replace</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + <property name="width">8</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolled_window"> + <property name="height_request">250</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">False</property> + <property name="vexpand">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="a_box"> + <property name="visible">True</property> + <child> + <object class="GtkListBox" id="original_name_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkListBox" id="arrow_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkListBox" id="result_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">2</property> + <property name="width">8</property> + </packing> + </child> + <child> + <object class="GtkBox" id="conflict_box"> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="visible">False</property> + <property name="margin-left">6</property> + <child> + <object class="GtkLabel" id="conflict_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <child> + <object class="GtkButton" id="conflict_down"> + <property name="visible">True</property> + <property name="relief">none</property> + <signal name="clicked" handler="select_next_conflict_down" swapped="yes" /> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">go-down-symbolic</property> + <property name="icon-size">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="conflict_up"> + <property name="visible">True</property> + <property name="relief">GTK_RELIEF_NONE</property> + <signal name="clicked" handler="select_next_conflict_up" swapped="yes" /> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">go-up-symbolic</property> + <property name="icon-size">1</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">3</property> + <property name="width">8</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> + <menu id="add_tag_menu"> + <section> + <attribute name="label" translatable="yes">Automatic Numbers</attribute> + <item> + <attribute name="label" translatable="yes">1, 2, 3, 4</attribute> + <attribute name="action">dialog.add-numbering-tag-zero</attribute> + </item> + <item> + <attribute name="label" translatable="yes">01, 02, 03, 04</attribute> + <attribute name="action">dialog.add-numbering-tag-one</attribute> + </item> + <item> + <attribute name="label" translatable="yes">001, 002, 003, 004</attribute> + <attribute name="action">dialog.add-numbering-tag-two</attribute> + </item> + </section> + <section> + <attribute name="label" translatable="yes">Metadata</attribute> + <item> + <attribute name="label" translatable="yes">Creation Date</attribute> + <attribute name="action">dialog.add-creation-date-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Camera Model</attribute> + <attribute name="action">dialog.add-equipment-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Season Number</attribute> + <attribute name="action">dialog.add-season-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Episode Number</attribute> + <attribute name="action">dialog.add-episode-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Track Number</attribute> + <attribute name="action">dialog.add-track-number-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Artist Name</attribute> + <attribute name="action">dialog.add-artist-name-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Title</attribute> + <attribute name="action">dialog.add-title-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Album Name</attribute> + <attribute name="action">dialog.add-album-name-tag</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Original File Name</attribute> + <attribute name="action">dialog.add-original-file-name-tag</attribute> + </item> + </section> + </menu> + <object class="GtkPopover" id="add_popover"> + <property name="position">bottom</property> + <property name="relative-to">add_button</property> + <signal name="closed" handler="add_popover_closed" swapped="yes" /> + </object> + <object class="GtkImage" id="done_image"> + <property name="visible">True</property> + <property name="icon_name">object-select-symbolic</property> + </object> + <menu id="numbering_order_menu"> + <section> + <item> + <attribute name="label" translatable="yes">Original name (Ascending) </attribute> + <attribute name="action">dialog.numbering-order-changed</attribute> + <attribute name="target">name-ascending</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Original name (Descending)</attribute> + <attribute name="action">dialog.numbering-order-changed</attribute> + <attribute name="target">name-descending</attribute> + </item> + <item> + <attribute name="label" translatable="yes">First Modified</attribute> + <attribute name="action">dialog.numbering-order-changed</attribute> + <attribute name="target">first-modified</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Last Modified</attribute> + <attribute name="action">dialog.numbering-order-changed</attribute> + <attribute name="target">last-modified</attribute> + </item> + </section> + </menu> + <object class="GtkPopover" id="numbering_order_popover"> + <property name="position">bottom</property> + <property name="relative-to">numbering_order_button</property> + <signal name="closed" handler="numbering_order_popover_closed" swapped="yes" /> + </object> +</interface>
\ No newline at end of file diff --git a/src/resources/ui/nautilus-files-view-context-menus.ui b/src/resources/ui/nautilus-files-view-context-menus.ui index 31f26fbbb..f69e6737f 100644 --- a/src/resources/ui/nautilus-files-view-context-menus.ui +++ b/src/resources/ui/nautilus-files-view-context-menus.ui @@ -215,7 +215,7 @@ </section> <section> <item> - <attribute name="label" translatable="yes">Rena_me</attribute> + <attribute name="label" translatable="yes">Rena_me…</attribute> <attribute name="action">view.rename</attribute> </item> </section> |