2 * ROX-Filer, filer for the ROX desktop project
3 * Copyright (C) 2006, Thomas Leonard and others (see changelog for details).
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the Free
7 * Software Foundation; either version 2 of the License, or (at your option)
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * You should have received a copy of the GNU General Public License along with
16 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
17 * Place, Suite 330, Boston, MA 02111-1307 USA
20 /* bulk_rename.c - rename multiple files at once */
26 #include <sys/types.h>
34 #include "bulk_rename.h"
36 #include "gui_support.h"
38 enum {RESPONSE_RENAME
, RESPONSE_RESET
};
40 /* Static prototypes */
41 static gboolean
apply_replace(GtkWidget
*box
);
42 static void response(GtkWidget
*box
, int resp
, GtkListStore
*model
);
43 static void reset_model(GtkListStore
*model
);
44 static gboolean
rename_items(const char *dir
, GtkListStore
*list
);
45 static void cell_edited(GtkCellRendererText
*cell
, const gchar
*path_string
,
46 const gchar
*new_text
, GtkTreeModel
*model
);
49 /****************************************************************
50 * EXTERNAL INTERFACE *
51 ****************************************************************/
53 /* Bulk rename these items */
54 void bulk_rename(const char *dir
, GList
*items
)
56 GtkWidget
*box
, *button
, *tree
, *swin
, *hbox
;
57 GtkWidget
*replace_entry
, *with_entry
;
58 GtkTreeViewColumn
*column
;
59 GtkCellRenderer
*cell_renderer
;
63 box
= gtk_dialog_new();
64 g_object_set_data_full(G_OBJECT(box
), "rename_dir",
65 g_strdup(dir
), g_free
);
66 gtk_window_set_title(GTK_WINDOW(box
), _("Bulk rename files"));
67 gtk_dialog_set_has_separator(GTK_DIALOG(box
), FALSE
);
69 button
= button_new_mixed(GTK_STOCK_REFRESH
, _("Reset"));
70 GTK_WIDGET_SET_FLAGS(button
, GTK_CAN_DEFAULT
);
71 gtk_dialog_add_action_widget(GTK_DIALOG(box
), button
, RESPONSE_RESET
);
72 gtk_dialog_set_default_response(GTK_DIALOG(box
), RESPONSE_RESET
);
73 gtk_tooltips_set_tip(tooltips
, button
,
74 _("Make the New column a copy of Old"), NULL
);
76 gtk_dialog_add_button(GTK_DIALOG(box
),
77 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
);
79 button
= button_new_mixed(GTK_STOCK_EXECUTE
, _("_Rename"));
80 GTK_WIDGET_SET_FLAGS(button
, GTK_CAN_DEFAULT
);
81 gtk_dialog_add_action_widget(GTK_DIALOG(box
), button
, RESPONSE_RENAME
);
82 gtk_dialog_set_default_response(GTK_DIALOG(box
), RESPONSE_RENAME
);
86 hbox
= gtk_hbox_new(FALSE
, 4);
87 gtk_container_set_border_width(GTK_CONTAINER(hbox
), 4);
88 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box
)->vbox
),
89 hbox
, FALSE
, TRUE
, 0);
91 gtk_box_pack_start(GTK_BOX(hbox
),
92 gtk_label_new(_("Replace:")), FALSE
, TRUE
, 0);
94 replace_entry
= gtk_entry_new();
95 g_object_set_data(G_OBJECT(box
), "replace_entry", replace_entry
);
96 gtk_box_pack_start(GTK_BOX(hbox
), replace_entry
, TRUE
, TRUE
, 0);
97 gtk_entry_set_text(GTK_ENTRY(replace_entry
), "\\.htm$");
98 gtk_tooltips_set_tip(tooltips
, replace_entry
,
99 _("This is a regular expression to search for.\n"
100 "^ matches the start of a filename\n"
101 "$ matches the end\n"
102 "\\. matches a dot\n"
103 "\\.htm$ matches the '.htm' in 'index.htm', etc"),
106 gtk_box_pack_start(GTK_BOX(hbox
),
107 gtk_label_new(_("With:")), FALSE
, TRUE
, 0);
109 with_entry
= gtk_entry_new();
110 g_object_set_data(G_OBJECT(box
), "with_entry", with_entry
);
111 gtk_box_pack_start(GTK_BOX(hbox
), with_entry
, TRUE
, TRUE
, 0);
112 gtk_entry_set_text(GTK_ENTRY(with_entry
), ".html");
113 gtk_tooltips_set_tip(tooltips
, with_entry
,
114 _("The first match in each filename will be replaced "
116 "There are no special characters."), NULL
);
118 button
= gtk_button_new_with_label(_("Apply"));
119 gtk_box_pack_start(GTK_BOX(hbox
), button
, FALSE
, TRUE
, 0);
120 gtk_tooltips_set_tip(tooltips
, button
,
121 _("Do a search-and-replace in the New column. "
122 "The files are not actually renamed until you click "
123 "on the Rename button below."), NULL
);
125 g_signal_connect_swapped(replace_entry
, "activate",
126 G_CALLBACK(gtk_widget_grab_focus
), with_entry
);
127 g_signal_connect_swapped(with_entry
, "activate",
128 G_CALLBACK(apply_replace
), box
);
129 g_signal_connect_swapped(button
, "clicked",
130 G_CALLBACK(apply_replace
), box
);
134 model
= gtk_list_store_new(2, G_TYPE_STRING
, G_TYPE_STRING
);
135 g_object_set_data(G_OBJECT(box
), "tree_model", model
);
136 tree
= gtk_tree_view_new_with_model(GTK_TREE_MODEL(model
));
138 cell_renderer
= gtk_cell_renderer_text_new();
139 column
= gtk_tree_view_column_new_with_attributes(
140 _("Old name"), cell_renderer
, "text", 0, NULL
);
141 gtk_tree_view_column_set_resizable(column
, TRUE
);
142 gtk_tree_view_append_column(GTK_TREE_VIEW(tree
), column
);
144 cell_renderer
= gtk_cell_renderer_text_new();
145 g_object_set(G_OBJECT(cell_renderer
), "editable", TRUE
, NULL
);
146 g_signal_connect(G_OBJECT(cell_renderer
), "edited",
147 G_CALLBACK(cell_edited
), model
);
148 column
= gtk_tree_view_column_new_with_attributes(
149 _("New name"), cell_renderer
, "text", 1, NULL
);
150 gtk_tree_view_append_column(GTK_TREE_VIEW(tree
), column
);
152 swin
= gtk_scrolled_window_new(NULL
, NULL
);
153 gtk_container_set_border_width(GTK_CONTAINER(swin
), 4);
154 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin
),
156 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin
),
157 GTK_POLICY_AUTOMATIC
, GTK_POLICY_ALWAYS
);
158 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box
)->vbox
), swin
, TRUE
, TRUE
, 0);
159 gtk_container_add(GTK_CONTAINER(swin
), tree
);
163 const char *name
= items
->data
;
165 gtk_list_store_append(model
, &iter
);
166 gtk_list_store_set(model
, &iter
, 0, name
, 1, name
, -1);
171 gtk_widget_show_all(tree
);
172 gtk_widget_size_request(tree
, &req
);
173 req
.width
= MIN(req
.width
+ 50, screen_width
- 50);
174 req
.height
= MIN(req
.height
+ 150, screen_height
- 50);
176 gtk_window_set_default_size(GTK_WINDOW(box
), req
.width
, req
.height
);
179 g_signal_connect(box
, "destroy", G_CALLBACK(one_less_window
), NULL
);
180 g_signal_connect(box
, "response", G_CALLBACK(response
), model
);
181 gtk_widget_show_all(box
);
184 /****************************************************************
185 * INTERNAL FUNCTIONS *
186 ****************************************************************/
188 static void response(GtkWidget
*box
, int resp
, GtkListStore
*model
)
190 if (resp
== RESPONSE_RESET
)
192 else if (resp
== RESPONSE_RENAME
)
194 if (rename_items(g_object_get_data(G_OBJECT(box
), "rename_dir"),
196 gtk_widget_destroy(box
);
199 gtk_widget_destroy(box
);
202 /* Do a search-and-replace on the second column. */
203 static void update_model(GtkListStore
*list
, regex_t
*replace
, const char *with
)
206 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
211 with_len
= strlen(with
);
213 if (!gtk_tree_model_get_iter_first(model
, &iter
))
215 g_warning("Model empty!");
224 gtk_tree_model_get(model
, &iter
, 1, &old
, -1);
226 if (regexec(replace
, old
, 1, &match
, 0) == 0)
232 g_return_if_fail(match
.rm_so
!= -1);
234 new_len
= match
.rm_so
+ with_len
+
235 (strlen(old
) - match
.rm_eo
) + 1;
236 new = g_malloc(new_len
);
238 strncpy(new, old
, match
.rm_so
);
239 strcpy(new + match
.rm_so
, with
);
240 strcpy(new + match
.rm_so
+ with_len
, old
+ match
.rm_eo
);
242 g_return_if_fail(new[new_len
- 1] == '\0');
244 if (strcmp(old
, new) != 0)
247 gtk_list_store_set(list
, &iter
, 1, new, -1);
254 } while (gtk_tree_model_iter_next(model
, &iter
));
257 report_error(_("No strings (in the New column) matched "
258 "the given expression"));
259 else if (n_changed
== 0)
262 report_error(_("One name matched, but the result was "
265 report_error(_("%d names matched, but the results were "
266 "all the same"), n_matched
);
270 static gboolean
apply_replace(GtkWidget
*box
)
273 GtkEntry
*replace_entry
, *with_entry
;
274 const char *replace
, *with
;
278 replace_entry
= g_object_get_data(G_OBJECT(box
), "replace_entry");
279 with_entry
= g_object_get_data(G_OBJECT(box
), "with_entry");
280 model
= g_object_get_data(G_OBJECT(box
), "tree_model");
282 g_return_val_if_fail(replace_entry
!= NULL
, TRUE
);
283 g_return_val_if_fail(with_entry
!= NULL
, TRUE
);
284 g_return_val_if_fail(model
!= NULL
, TRUE
);
286 replace
= gtk_entry_get_text(replace_entry
);
287 with
= gtk_entry_get_text(with_entry
);
289 if (replace
[0] == '\0' && with
[0] == '\0')
291 report_error(_("Specify a regular expression to match, "
292 "and a string to replace matches with."));
296 error
= regcomp(&compiled
, replace
, 0);
302 size
= regerror(error
, &compiled
, NULL
, 0);
303 g_return_val_if_fail(size
> 0, TRUE
);
305 message
= g_malloc(size
);
306 regerror(error
, &compiled
, message
, size
);
308 report_error(_("%s (for '%s')"), message
, replace
);
313 update_model(model
, &compiled
, with
);
320 static void reset_model(GtkListStore
*list
)
323 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
325 if (!gtk_tree_model_get_iter_first(model
, &iter
))
330 gtk_tree_model_get(model
, &iter
, 0, &before
, -1);
331 gtk_list_store_set(list
, &iter
, 1, before
, -1);
333 } while (gtk_tree_model_iter_next(model
, &iter
));
336 static gboolean
do_rename(const char *before
, const char *after
)
338 /* Check again, just in case */
339 if (access(after
, F_OK
) == 0)
341 report_error(_("A file called '%s' already exists. "
342 "Aborting bulk rename."), after
);
344 else if (rename(before
, after
))
346 report_error(_("Failed to rename '%s' as '%s':\n%s\n"
347 "Aborting bulk rename."), before
, after
,
355 static gboolean
rename_items(const char *dir
, GtkListStore
*list
)
357 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
359 char *slash_example
= NULL
;
360 GHashTable
*names
= NULL
;
361 gboolean success
= FALSE
;
364 g_return_val_if_fail(dir
!= NULL
, FALSE
);
365 g_return_val_if_fail(list
!= NULL
, FALSE
);
367 if (!gtk_tree_model_get_iter_first(model
, &iter
))
368 return FALSE
; /* (error) */
370 names
= g_hash_table_new_full(g_str_hash
, g_str_equal
, g_free
, NULL
);
372 char *before
, *after
;
375 gtk_tree_model_get(model
, &iter
, 0, &before
, 1, &after
, -1);
377 if (!slash_example
&& strchr(after
, '/'))
379 slash_example
= g_strdup(after
);
382 if (g_hash_table_lookup(names
, before
))
384 report_error("Filename '%s' used twice!", before
);
387 g_hash_table_insert(names
, before
, "");
389 if (after
[0] == '\0' || strcmp(after
, before
) == 0)
395 if (g_hash_table_lookup(names
, after
))
397 report_error("Filename '%s' used twice!", after
);
400 g_hash_table_insert(names
, after
, "");
405 dest
= make_path(dir
, after
);
406 if (access(dest
, F_OK
) == 0)
408 report_error(_("A file called '%s' already exists"),
414 } while (gtk_tree_model_iter_next(model
, &iter
));
419 message
= g_strdup_printf(_("Some of the New names contain "
420 "/ characters (eg '%s'). "
421 "This will cause the files to end up in "
422 "different directories. "
423 "Continue?"), slash_example
);
424 if (!confirm(message
, GTK_STOCK_EXECUTE
, "Rename anyway"))
434 report_error(_("None of the names have changed. "
440 gtk_tree_model_get_iter_first(model
, &iter
);
443 char *before
, *after
, *before_path
;
446 gtk_tree_model_get(model
, &iter
, 0, &before
, 1, &after
, -1);
448 if (after
[0] == '\0' || strcmp(after
, before
) == 0)
450 else if (after
[0] == '/')
453 dest
= make_path(dir
, after
);
455 before_path
= g_build_filename(dir
, before
, NULL
);
457 if (dest
== NULL
|| do_rename(before_path
, dest
))
460 if (!gtk_list_store_remove(list
, &iter
))
461 break; /* Last item; finished */
472 g_free(slash_example
);
474 g_hash_table_destroy(names
);
478 static void cell_edited(GtkCellRendererText
*cell
,
479 const gchar
*path_string
, const gchar
*new_text
,
485 path
= gtk_tree_path_new_from_string(path_string
);
486 gtk_tree_model_get_iter(model
, &iter
, path
);
487 gtk_tree_path_free(path
);
489 gtk_list_store_set(GTK_LIST_STORE(model
), &iter
, 1, new_text
, -1);