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)
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
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 */
28 #include <sys/types.h>
36 #include "bulk_rename.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
;
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
);
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"),
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 "
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
);
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
),
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 /* Do a search-and-replace on the second column. */
205 static void update_model(GtkListStore
*list
, regex_t
*replace
, const char *with
)
208 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
213 with_len
= strlen(with
);
215 if (!gtk_tree_model_get_iter_first(model
, &iter
))
217 g_warning("Model empty!");
226 gtk_tree_model_get(model
, &iter
, 1, &old
, -1);
228 if (regexec(replace
, old
, 1, &match
, 0) == 0)
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)
249 gtk_list_store_set(list
, &iter
, 1, new, -1);
256 } while (gtk_tree_model_iter_next(model
, &iter
));
259 report_error(_("No strings (in the After column) matched "
260 "the given expression"));
261 else if (n_changed
== 0)
264 report_error(_("One name matched, but the result was "
267 report_error(_("%d names matched, but the results were "
268 "all the same"), n_matched
);
272 static gboolean
apply_replace(GtkWidget
*box
)
275 GtkEntry
*replace_entry
, *with_entry
;
276 const char *replace
, *with
;
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."));
298 error
= regcomp(&compiled
, replace
, 0);
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
);
315 update_model(model
, &compiled
, with
);
322 static void reset_model(GtkListStore
*list
)
325 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
327 if (!gtk_tree_model_get_iter_first(model
, &iter
))
332 gtk_tree_model_get(model
, &iter
, 0, &before
, -1);
333 gtk_list_store_set(list
, &iter
, 1, before
, -1);
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
,
357 static gboolean
rename_items(const char *dir
, GtkListStore
*list
)
359 GtkTreeModel
*model
= (GtkTreeModel
*) list
;
361 char *slash_example
= NULL
;
362 GHashTable
*names
= NULL
;
363 gboolean success
= FALSE
;
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
);
374 char *before
, *after
;
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
);
389 g_hash_table_insert(names
, before
, "");
391 if (after
[0] == '\0' || strcmp(after
, before
) == 0)
397 if (g_hash_table_lookup(names
, after
))
399 report_error("Filename '%s' used twice!", after
);
402 g_hash_table_insert(names
, after
, "");
407 dest
= make_path(dir
, after
);
408 if (access(dest
, F_OK
) == 0)
410 report_error(_("A file called '%s' already exists"),
416 } while (gtk_tree_model_iter_next(model
, &iter
));
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"))
436 report_error(_("None of the names have changed. "
442 gtk_tree_model_get_iter_first(model
, &iter
);
445 char *before
, *after
, *before_path
;
448 gtk_tree_model_get(model
, &iter
, 0, &before
, 1, &after
, -1);
450 if (after
[0] == '\0' || strcmp(after
, before
) == 0)
452 else if (after
[0] == '/')
455 dest
= make_path(dir
, after
);
457 before_path
= g_build_filename(dir
, before
, NULL
);
459 if (dest
== NULL
|| do_rename(before_path
, dest
))
462 if (!gtk_list_store_remove(list
, &iter
))
463 break; /* Last item; finished */
474 g_free(slash_example
);
476 g_hash_table_destroy(names
);
480 static void cell_edited(GtkCellRendererText
*cell
,
481 const gchar
*path_string
, const gchar
*new_text
,
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);