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 "The only special characters are back-references "
117 "from \\0 to \\9. To use them literally, "
118 "they have to be escaped with a backslash."), NULL
);
120 button
= gtk_button_new_with_label(_("Apply"));
121 gtk_box_pack_start(GTK_BOX(hbox
), button
, FALSE
, TRUE
, 0);
122 gtk_tooltips_set_tip(tooltips
, button
,
123 _("Do a search-and-replace in the New column. "
124 "The files are not actually renamed until you click "
125 "on the Rename button below."), NULL
);
127 g_signal_connect_swapped(replace_entry
, "activate",
128 G_CALLBACK(gtk_widget_grab_focus
), with_entry
);
129 g_signal_connect_swapped(with_entry
, "activate",
130 G_CALLBACK(apply_replace
), box
);
131 g_signal_connect_swapped(button
, "clicked",
132 G_CALLBACK(apply_replace
), box
);
136 model
= gtk_list_store_new(2, G_TYPE_STRING
, G_TYPE_STRING
);
137 g_object_set_data(G_OBJECT(box
), "tree_model", model
);
138 tree
= gtk_tree_view_new_with_model(GTK_TREE_MODEL(model
));
140 cell_renderer
= gtk_cell_renderer_text_new();
141 column
= gtk_tree_view_column_new_with_attributes(
142 _("Old name"), cell_renderer
, "text", 0, NULL
);
143 gtk_tree_view_column_set_resizable(column
, TRUE
);
144 gtk_tree_view_append_column(GTK_TREE_VIEW(tree
), column
);
146 cell_renderer
= gtk_cell_renderer_text_new();
147 g_object_set(G_OBJECT(cell_renderer
), "editable", TRUE
, NULL
);
148 g_signal_connect(G_OBJECT(cell_renderer
), "edited",
149 G_CALLBACK(cell_edited
), model
);
150 column
= gtk_tree_view_column_new_with_attributes(
151 _("New name"), cell_renderer
, "text", 1, NULL
);
152 gtk_tree_view_append_column(GTK_TREE_VIEW(tree
), column
);
154 swin
= gtk_scrolled_window_new(NULL
, NULL
);
155 gtk_container_set_border_width(GTK_CONTAINER(swin
), 4);
156 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin
),
158 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin
),
159 GTK_POLICY_AUTOMATIC
, GTK_POLICY_ALWAYS
);
160 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box
)->vbox
), swin
, TRUE
, TRUE
, 0);
161 gtk_container_add(GTK_CONTAINER(swin
), tree
);
165 const char *name
= items
->data
;
167 gtk_list_store_append(model
, &iter
);
168 gtk_list_store_set(model
, &iter
, 0, name
, 1, name
, -1);
173 gtk_widget_show_all(tree
);
174 gtk_widget_size_request(tree
, &req
);
175 req
.width
= MIN(req
.width
+ 50, screen_width
- 50);
176 req
.height
= MIN(req
.height
+ 150, screen_height
- 50);
178 gtk_window_set_default_size(GTK_WINDOW(box
), req
.width
, req
.height
);
181 g_signal_connect(box
, "destroy", G_CALLBACK(one_less_window
), NULL
);
182 g_signal_connect(box
, "response", G_CALLBACK(response
), model
);
183 gtk_widget_show_all(box
);
186 /****************************************************************
187 * INTERNAL FUNCTIONS *
188 ****************************************************************/
190 static void response(GtkWidget
*box
, int resp
, GtkListStore
*model
)
192 if (resp
== RESPONSE_RESET
)
194 else if (resp
== RESPONSE_RENAME
)
196 if (rename_items(g_object_get_data(G_OBJECT(box
), "rename_dir"),
198 gtk_widget_destroy(box
);
201 gtk_widget_destroy(box
);
204 /** Substitute: s/old/with/
205 * Returns the result as a new string, or NULL if there is no match.
206 * "replace" is a compiled version of "with".
207 * Caller must free the result.
209 static GString
*subst(const char *old
, regex_t
*replace
, const char *with
)
213 regmatch_t match
[max_subs
];
215 if (regexec(replace
, old
, max_subs
, match
, 0) != 0)
216 return NULL
; /* No match */
218 g_return_val_if_fail(match
[0].rm_so
!= -1, NULL
);
220 new = g_string_new(NULL
);
221 g_string_append_len(new, old
, match
[0].rm_so
);
224 for (i
= 0; with
[i
]; i
++)
226 if (with
[i
] == '\\' && with
[i
+1])
229 if (with
[i
] >= '0' && with
[i
]-'0' < max_subs
)
233 subpat
= with
[i
] - '0';
235 if (match
[subpat
].rm_so
!= -1)
236 g_string_append_len(new, old
+ match
[subpat
].rm_so
,
237 match
[subpat
].rm_eo
- match
[subpat
].rm_so
);
242 // Escape next character
243 g_string_append_c(new, with
[i
]);
247 g_string_append_c(new, with
[i
]);
250 g_string_append(new, old
+ match
[0].rm_eo
);
255 /* Do a search-and-replace on the second column. */
256 static void update_model(GtkListStore
*list
, regex_t
*replace
, const char *with
)
259 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
263 if (!gtk_tree_model_get_iter_first(model
, &iter
))
265 g_warning("Model empty!");
274 gtk_tree_model_get(model
, &iter
, 1, &old
, -1);
276 new = subst(old
, replace
, with
);
280 if (strcmp(old
, new->str
) != 0)
283 gtk_list_store_set(list
, &iter
, 1, new->str
, -1);
286 g_string_free(new, TRUE
);
290 } while (gtk_tree_model_iter_next(model
, &iter
));
293 report_error(_("No strings (in the New column) matched "
294 "the given expression"));
295 else if (n_changed
== 0)
298 report_error(_("One name matched, but the result was "
301 report_error(_("%d names matched, but the results were "
302 "all the same"), n_matched
);
306 static gboolean
apply_replace(GtkWidget
*box
)
309 GtkEntry
*replace_entry
, *with_entry
;
310 const char *replace
, *with
;
314 replace_entry
= g_object_get_data(G_OBJECT(box
), "replace_entry");
315 with_entry
= g_object_get_data(G_OBJECT(box
), "with_entry");
316 model
= g_object_get_data(G_OBJECT(box
), "tree_model");
318 g_return_val_if_fail(replace_entry
!= NULL
, TRUE
);
319 g_return_val_if_fail(with_entry
!= NULL
, TRUE
);
320 g_return_val_if_fail(model
!= NULL
, TRUE
);
322 replace
= gtk_entry_get_text(replace_entry
);
323 with
= gtk_entry_get_text(with_entry
);
325 if (replace
[0] == '\0' && with
[0] == '\0')
327 report_error(_("Specify a regular expression to match, "
328 "and a string to replace matches with."));
332 error
= regcomp(&compiled
, replace
, REG_EXTENDED
);
338 size
= regerror(error
, &compiled
, NULL
, 0);
339 g_return_val_if_fail(size
> 0, TRUE
);
341 message
= g_malloc(size
);
342 regerror(error
, &compiled
, message
, size
);
344 report_error(_("%s (for '%s')"), message
, replace
);
349 update_model(model
, &compiled
, with
);
356 static void reset_model(GtkListStore
*list
)
359 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
361 if (!gtk_tree_model_get_iter_first(model
, &iter
))
366 gtk_tree_model_get(model
, &iter
, 0, &before
, -1);
367 gtk_list_store_set(list
, &iter
, 1, before
, -1);
369 } while (gtk_tree_model_iter_next(model
, &iter
));
372 static gboolean
do_rename(const char *before
, const char *after
)
374 /* Check again, just in case */
375 if (access(after
, F_OK
) == 0)
377 report_error(_("A file called '%s' already exists. "
378 "Aborting bulk rename."), after
);
380 else if (rename(before
, after
))
382 report_error(_("Failed to rename '%s' as '%s':\n%s\n"
383 "Aborting bulk rename."), before
, after
,
391 static gboolean
rename_items(const char *dir
, GtkListStore
*list
)
393 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
395 char *slash_example
= NULL
;
396 GHashTable
*names
= NULL
;
397 gboolean success
= FALSE
;
400 g_return_val_if_fail(dir
!= NULL
, FALSE
);
401 g_return_val_if_fail(list
!= NULL
, FALSE
);
403 if (!gtk_tree_model_get_iter_first(model
, &iter
))
404 return FALSE
; /* (error) */
406 names
= g_hash_table_new_full(g_str_hash
, g_str_equal
, g_free
, NULL
);
408 char *before
, *after
;
411 gtk_tree_model_get(model
, &iter
, 0, &before
, 1, &after
, -1);
413 if (!slash_example
&& strchr(after
, '/'))
415 slash_example
= g_strdup(after
);
418 if (g_hash_table_lookup(names
, before
))
420 report_error("Filename '%s' used twice!", before
);
423 g_hash_table_insert(names
, before
, "");
425 if (after
[0] == '\0' || strcmp(after
, before
) == 0)
431 if (g_hash_table_lookup(names
, after
))
433 report_error("Filename '%s' used twice!", after
);
436 g_hash_table_insert(names
, after
, "");
441 dest
= make_path(dir
, after
);
442 if (access(dest
, F_OK
) == 0)
444 report_error(_("A file called '%s' already exists"),
450 } while (gtk_tree_model_iter_next(model
, &iter
));
455 message
= g_strdup_printf(_("Some of the New names contain "
456 "/ characters (eg '%s'). "
457 "This will cause the files to end up in "
458 "different directories. "
459 "Continue?"), slash_example
);
460 if (!confirm(message
, GTK_STOCK_EXECUTE
, "Rename anyway"))
470 report_error(_("None of the names have changed. "
476 gtk_tree_model_get_iter_first(model
, &iter
);
479 char *before
, *after
, *before_path
;
482 gtk_tree_model_get(model
, &iter
, 0, &before
, 1, &after
, -1);
484 if (after
[0] == '\0' || strcmp(after
, before
) == 0)
486 else if (after
[0] == '/')
489 dest
= make_path(dir
, after
);
491 before_path
= g_build_filename(dir
, before
, NULL
);
493 if (dest
== NULL
|| do_rename(before_path
, dest
))
496 if (!gtk_list_store_remove(list
, &iter
))
497 break; /* Last item; finished */
508 g_free(slash_example
);
510 g_hash_table_destroy(names
);
514 static void cell_edited(GtkCellRendererText
*cell
,
515 const gchar
*path_string
, const gchar
*new_text
,
521 path
= gtk_tree_path_new_from_string(path_string
);
522 gtk_tree_model_get_iter(model
, &iter
, path
);
523 gtk_tree_path_free(path
);
525 gtk_list_store_set(GTK_LIST_STORE(model
), &iter
, 1, new_text
, -1);
529 static void test_subst(const char *string
, const char *pattern
, const char *with
, const char *expected
)
534 g_print("Testing s/%s/%s\n", pattern
, with
);
536 if (regcomp(&compiled
, pattern
, REG_EXTENDED
))
537 g_error("Failed to compiled '%s'", pattern
);
539 new = subst(string
, &compiled
, with
);
543 g_return_if_fail(expected
== NULL
);
547 //g_print("Got: %s\n", new->str);
548 g_return_if_fail(expected
!= NULL
);
549 g_return_if_fail(strcmp(new->str
, expected
) == 0);
550 g_string_free(new, TRUE
);
556 void bulk_rename_tests()
558 test_subst("hello", "l", "L", "heLlo");
559 test_subst("hello", "h(.*)l", "\\1-\\1", "el-elo");
560 test_subst("hello", "(h)", "\\1", "hello");
561 test_subst("hello", "(h)", "\\\\1", "\\1ello");
562 test_subst("hello", "(h)", "\\\\\\1", "\\hello");
563 test_subst("hello", "(h)", "\\", "\\ello");
564 test_subst("hello", "(h)$", "\\", NULL
);
565 test_subst("hello", "(.)(.)(.).*", "\\0-\\1-\\2-\\3-\\4-\\9-\\:", "hello-h-e-l---:");