r3748: Typo.
[rox-filer.git] / ROX-Filer / src / bulk_rename.c
blob327cfc1f26a69e1e130dcb43b0fdd7414d43b6e9
1 /*
2 * $Id$
4 * ROX-Filer, filer for the ROX desktop project
5 * Copyright (C) 2005, the ROX-Filer team.
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
17 * You should have received a copy of the GNU General Public License along with
18 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
19 * Place, Suite 330, Boston, MA 02111-1307 USA
22 /* bulk_rename.c - rename multiple files at once */
24 #include "config.h"
26 #include <stdlib.h>
27 #include <gtk/gtk.h>
28 #include <sys/types.h>
29 #include <regex.h>
30 #include <string.h>
31 #include <errno.h>
33 #include "global.h"
35 #include "main.h"
36 #include "bulk_rename.h"
37 #include "support.h"
38 #include "gui_support.h"
40 enum {RESPONSE_RENAME, RESPONSE_RESET};
42 /* Static prototypes */
43 static gboolean apply_replace(GtkWidget *box);
44 static void response(GtkWidget *box, int resp, GtkListStore *model);
45 static void reset_model(GtkListStore *model);
46 static gboolean rename_items(const char *dir, GtkListStore *list);
47 static void cell_edited(GtkCellRendererText *cell, const gchar *path_string,
48 const gchar *new_text, GtkTreeModel *model);
51 /****************************************************************
52 * EXTERNAL INTERFACE *
53 ****************************************************************/
55 /* Bulk rename these items */
56 void bulk_rename(const char *dir, GList *items)
58 GtkWidget *box, *button, *tree, *swin, *hbox;
59 GtkWidget *replace_entry, *with_entry;
60 GtkTreeViewColumn *column;
61 GtkCellRenderer *cell_renderer;
62 GtkListStore *model;
63 GtkRequisition req;
65 box = gtk_dialog_new();
66 g_object_set_data_full(G_OBJECT(box), "rename_dir",
67 g_strdup(dir), g_free);
68 gtk_window_set_title(GTK_WINDOW(box), _("Bulk rename files"));
69 gtk_dialog_set_has_separator(GTK_DIALOG(box), FALSE);
71 button = button_new_mixed(GTK_STOCK_REFRESH, _("Reset"));
72 GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
73 gtk_dialog_add_action_widget(GTK_DIALOG(box), button, RESPONSE_RESET);
74 gtk_dialog_set_default_response(GTK_DIALOG(box), RESPONSE_RESET);
75 gtk_tooltips_set_tip(tooltips, button,
76 _("Make the After column a copy of Before"), NULL);
78 gtk_dialog_add_button(GTK_DIALOG(box),
79 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL);
81 button = button_new_mixed(GTK_STOCK_EXECUTE, _("_Rename"));
82 GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
83 gtk_dialog_add_action_widget(GTK_DIALOG(box), button, RESPONSE_RENAME);
84 gtk_dialog_set_default_response(GTK_DIALOG(box), RESPONSE_RENAME);
86 /* Replace */
88 hbox = gtk_hbox_new(FALSE, 4);
89 gtk_container_set_border_width(GTK_CONTAINER(hbox), 4);
90 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box)->vbox),
91 hbox, FALSE, TRUE, 0);
93 gtk_box_pack_start(GTK_BOX(hbox),
94 gtk_label_new(_("Replace:")), FALSE, TRUE, 0);
96 replace_entry = gtk_entry_new();
97 g_object_set_data(G_OBJECT(box), "replace_entry", replace_entry);
98 gtk_box_pack_start(GTK_BOX(hbox), replace_entry, TRUE, TRUE, 0);
99 gtk_entry_set_text(GTK_ENTRY(replace_entry), "\\.htm$");
100 gtk_tooltips_set_tip(tooltips, replace_entry,
101 _("This is a regular expression to search for.\n"
102 "^ matches the start of a filename\n"
103 "$ matches the end\n"
104 "\\. matches a dot\n"
105 "\\.htm$ matches the '.htm' in 'index.html', etc"),
106 NULL);
108 gtk_box_pack_start(GTK_BOX(hbox),
109 gtk_label_new(_("With:")), FALSE, TRUE, 0);
111 with_entry = gtk_entry_new();
112 g_object_set_data(G_OBJECT(box), "with_entry", with_entry);
113 gtk_box_pack_start(GTK_BOX(hbox), with_entry, TRUE, TRUE, 0);
114 gtk_entry_set_text(GTK_ENTRY(with_entry), ".html");
115 gtk_tooltips_set_tip(tooltips, with_entry,
116 _("The first match in each filename will be replaced "
117 "by this string. "
118 "There are no special characters."), 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 After 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);
134 /* The TreeView */
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 _("Before"), 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 _("After"), 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),
157 GTK_SHADOW_IN);
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);
163 while (items) {
164 GtkTreeIter iter;
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);
170 items = items->next;
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);
180 number_of_windows++;
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)
193 reset_model(model);
194 else if (resp == RESPONSE_RENAME)
196 if (rename_items(g_object_get_data(G_OBJECT(box), "rename_dir"),
197 model))
198 gtk_widget_destroy(box);
200 else
201 gtk_widget_destroy(box);
204 /* Do a search-and-replace on the second column. */
205 static void update_model(GtkListStore *list, regex_t *replace, const char *with)
207 GtkTreeIter iter;
208 GtkTreeModel *model = (GtkTreeModel *) list;
209 int n_matched = 0;
210 int n_changed = 0;
211 int with_len;
213 with_len = strlen(with);
215 if (!gtk_tree_model_get_iter_first(model, &iter))
217 g_warning("Model empty!");
218 return;
223 regmatch_t match;
224 char *old = NULL;
226 gtk_tree_model_get(model, &iter, 1, &old, -1);
228 if (regexec(replace, old, 1, &match, 0) == 0)
230 char *new;
231 int new_len;
233 n_matched++;
234 g_return_if_fail(match.rm_so != -1);
236 new_len = match.rm_so + with_len +
237 (strlen(old) - match.rm_eo) + 1;
238 new = g_malloc(new_len);
240 strncpy(new, old, match.rm_so);
241 strcpy(new + match.rm_so, with);
242 strcpy(new + match.rm_so + with_len, old + match.rm_eo);
244 g_return_if_fail(new[new_len - 1] == '\0');
246 if (strcmp(old, new) != 0)
248 n_changed++;
249 gtk_list_store_set(list, &iter, 1, new, -1);
252 g_free(new);
254 g_free(old);
256 } while (gtk_tree_model_iter_next(model, &iter));
258 if (n_matched == 0)
259 report_error(_("No strings (in the After column) matched "
260 "the given expression"));
261 else if (n_changed == 0)
263 if (n_matched == 1)
264 report_error(_("One name matched, but the result was "
265 "the same"));
266 else
267 report_error(_("%d names matched, but the results were "
268 "all the same"), n_matched);
272 static gboolean apply_replace(GtkWidget *box)
274 GtkListStore *model;
275 GtkEntry *replace_entry, *with_entry;
276 const char *replace, *with;
277 regex_t compiled;
278 int error;
280 replace_entry = g_object_get_data(G_OBJECT(box), "replace_entry");
281 with_entry = g_object_get_data(G_OBJECT(box), "with_entry");
282 model = g_object_get_data(G_OBJECT(box), "tree_model");
284 g_return_val_if_fail(replace_entry != NULL, TRUE);
285 g_return_val_if_fail(with_entry != NULL, TRUE);
286 g_return_val_if_fail(model != NULL, TRUE);
288 replace = gtk_entry_get_text(replace_entry);
289 with = gtk_entry_get_text(with_entry);
291 if (replace[0] == '\0' && with[0] == '\0')
293 report_error(_("Specify a regular expression to match, "
294 "and a string to replace matches with."));
295 return TRUE;
298 error = regcomp(&compiled, replace, 0);
299 if (error)
301 char *message;
302 size_t size;
304 size = regerror(error, &compiled, NULL, 0);
305 g_return_val_if_fail(size > 0, TRUE);
307 message = g_malloc(size);
308 regerror(error, &compiled, message, size);
310 report_error(_("%s (for '%s')"), message, replace);
312 return TRUE;
315 update_model(model, &compiled, with);
317 regfree(&compiled);
319 return TRUE;
322 static void reset_model(GtkListStore *list)
324 GtkTreeIter iter;
325 GtkTreeModel *model = (GtkTreeModel *) list;
327 if (!gtk_tree_model_get_iter_first(model, &iter))
328 return;
330 do {
331 char *before;
332 gtk_tree_model_get(model, &iter, 0, &before, -1);
333 gtk_list_store_set(list, &iter, 1, before, -1);
334 g_free(before);
335 } while (gtk_tree_model_iter_next(model, &iter));
338 static gboolean do_rename(const char *before, const char *after)
340 /* Check again, just in case */
341 if (access(after, F_OK) == 0)
343 report_error(_("A file called '%s' already exists. "
344 "Aborting bulk rename."), after);
346 else if (rename(before, after))
348 report_error(_("Failed to rename '%s' as '%s':\n%s\n"
349 "Aborting bulk rename."), before, after,
350 g_strerror(errno));
352 else
353 return TRUE;
354 return FALSE;
357 static gboolean rename_items(const char *dir, GtkListStore *list)
359 GtkTreeModel *model = (GtkTreeModel *) list;
360 GtkTreeIter iter;
361 char *slash_example = NULL;
362 GHashTable *names = NULL;
363 gboolean success = FALSE;
364 int n_renames = 0;
366 g_return_val_if_fail(dir != NULL, FALSE);
367 g_return_val_if_fail(list != NULL, FALSE);
369 if (!gtk_tree_model_get_iter_first(model, &iter))
370 return FALSE; /* (error) */
372 names = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
373 do {
374 char *before, *after;
375 const char *dest;
377 gtk_tree_model_get(model, &iter, 0, &before, 1, &after, -1);
379 if (!slash_example && strchr(after, '/'))
381 slash_example = g_strdup(after);
384 if (g_hash_table_lookup(names, before))
386 report_error("Filename '%s' used twice!", before);
387 goto fail;
389 g_hash_table_insert(names, before, "");
391 if (after[0] == '\0' || strcmp(after, before) == 0)
393 g_free(after);
394 continue;
397 if (g_hash_table_lookup(names, after))
399 report_error("Filename '%s' used twice!", after);
400 goto fail;
402 g_hash_table_insert(names, after, "");
404 if (after[0] == '/')
405 dest = after;
406 else
407 dest = make_path(dir, after);
408 if (access(dest, F_OK) == 0)
410 report_error(_("A file called '%s' already exists"),
411 dest);
412 goto fail;
415 n_renames++;
416 } while (gtk_tree_model_iter_next(model, &iter));
418 if (slash_example)
420 char *message;
421 message = g_strdup_printf(_("Some of the After names contain "
422 "/ characters (eg '%s'). "
423 "This will cause the files to end up in "
424 "different directories. "
425 "Continue?"), slash_example);
426 if (!confirm(message, GTK_STOCK_EXECUTE, "Rename anyway"))
428 g_free(message);
429 goto fail;
431 g_free(message);
434 if (n_renames == 0)
436 report_error(_("None of the names have changed. "
437 "Nothing to do!"));
438 goto fail;
441 success = TRUE;
442 gtk_tree_model_get_iter_first(model, &iter);
443 while (success)
445 char *before, *after, *before_path;
446 const char *dest;
448 gtk_tree_model_get(model, &iter, 0, &before, 1, &after, -1);
450 if (after[0] == '\0' || strcmp(after, before) == 0)
451 dest = NULL;
452 else if (after[0] == '/')
453 dest = after;
454 else
455 dest = make_path(dir, after);
457 before_path = g_build_filename(dir, before, NULL);
459 if (dest == NULL || do_rename(before_path, dest))
461 /* Advances iter */
462 if (!gtk_list_store_remove(list, &iter))
463 break; /* Last item; finished */
465 else
466 success = FALSE;
468 g_free(before_path);
469 g_free(before);
470 g_free(after);
473 fail:
474 g_free(slash_example);
475 if (names)
476 g_hash_table_destroy(names);
477 return success;
480 static void cell_edited(GtkCellRendererText *cell,
481 const gchar *path_string, const gchar *new_text,
482 GtkTreeModel *model)
484 GtkTreePath *path;
485 GtkTreeIter iter;
487 path = gtk_tree_path_new_from_string(path_string);
488 gtk_tree_model_get_iter(model, &iter, path);
489 gtk_tree_path_free(path);
491 gtk_list_store_set(GTK_LIST_STORE(model), &iter, 1, new_text, -1);