Skip to content

Commit d5cf079

Browse files
committed
Batch rename files in asset browser
Right-clicking multiple selected files now offers Batch Rename, which renames them to {base}_{NN} preserving each file's extension. Existing targets are skipped with an error toast, and AssetPathChanged is emitted per file so scene references patch up.
1 parent 70c1b23 commit d5cf079

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

crates/renzora_asset_browser/src/lib.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,120 @@ impl EditorPanel for AssetBrowserPanel {
481481
}
482482
}
483483

484+
// --- Batch rename dialog ---
485+
if state.batch_rename_active {
486+
let count = state.batch_rename_assets.len();
487+
let preview_ext = state
488+
.batch_rename_assets
489+
.first()
490+
.and_then(|p| p.extension())
491+
.and_then(|e| e.to_str())
492+
.map(|s| s.to_string());
493+
let mut open = true;
494+
egui::Window::new("Batch Rename")
495+
.collapsible(false)
496+
.resizable(false)
497+
.open(&mut open)
498+
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
499+
.show(ui.ctx(), |ui| {
500+
ui.horizontal(|ui| {
501+
ui.label("Base name:");
502+
ui.text_edit_singleline(&mut state.batch_rename_base);
503+
});
504+
ui.horizontal(|ui| {
505+
ui.label("Start at:");
506+
ui.add(
507+
egui::DragValue::new(&mut state.batch_rename_start).range(0..=9999),
508+
);
509+
});
510+
ui.add_space(4.0);
511+
let preview_text = match &preview_ext {
512+
Some(ext) => format!(
513+
"Preview: {}_{:02}.{}, {}_{:02}.{}, … ({} files)",
514+
state.batch_rename_base, state.batch_rename_start, ext,
515+
state.batch_rename_base, state.batch_rename_start + 1, ext,
516+
count,
517+
),
518+
None => format!(
519+
"Preview: {}_{:02}, {}_{:02}, … ({} items)",
520+
state.batch_rename_base,
521+
state.batch_rename_start,
522+
state.batch_rename_base,
523+
state.batch_rename_start + 1,
524+
count,
525+
),
526+
};
527+
ui.label(
528+
egui::RichText::new(preview_text)
529+
.size(11.0)
530+
.color(theme.text.muted.to_color32()),
531+
);
532+
ui.add_space(4.0);
533+
ui.horizontal(|ui| {
534+
let base_ok = !state.batch_rename_base.trim().is_empty();
535+
let rename_btn = egui::Button::new("Rename");
536+
if ui.add_enabled(base_ok, rename_btn).clicked() {
537+
state.pending_batch_rename = Some((
538+
state.batch_rename_base.clone(),
539+
state.batch_rename_start,
540+
state.batch_rename_assets.clone(),
541+
));
542+
state.batch_rename_active = false;
543+
}
544+
if ui.button("Cancel").clicked() {
545+
state.batch_rename_active = false;
546+
}
547+
});
548+
});
549+
if !open {
550+
state.batch_rename_active = false;
551+
}
552+
}
553+
554+
// --- Process pending batch rename ---
555+
if let Some((base, start, assets)) = state.pending_batch_rename.take() {
556+
let mut new_selection: std::collections::HashSet<std::path::PathBuf> =
557+
std::collections::HashSet::new();
558+
for (i, old_path) in assets.iter().enumerate() {
559+
let Some(parent) = old_path.parent() else {
560+
new_selection.insert(old_path.clone());
561+
continue;
562+
};
563+
let ext = old_path.extension().and_then(|e| e.to_str());
564+
let new_name = match ext {
565+
Some(e) => format!("{}_{:02}.{}", base, start as usize + i, e),
566+
None => format!("{}_{:02}", base, start as usize + i),
567+
};
568+
let new_path = parent.join(&new_name);
569+
if new_path == *old_path {
570+
new_selection.insert(old_path.clone());
571+
continue;
572+
}
573+
if new_path.exists() {
574+
state.last_error = Some(format!("Skipped: {} already exists", new_name));
575+
state.error_timeout = 3.0;
576+
new_selection.insert(old_path.clone());
577+
continue;
578+
}
579+
let is_dir = old_path.is_dir();
580+
match std::fs::rename(old_path, &new_path) {
581+
Ok(_) => {
582+
new_selection.insert(new_path.clone());
583+
if state.selected_path.as_ref() == Some(old_path) {
584+
state.selected_path = Some(new_path.clone());
585+
}
586+
emit_asset_path_change(world, old_path, &new_path, is_dir);
587+
}
588+
Err(e) => {
589+
state.last_error = Some(format!("Rename failed: {}", e));
590+
state.error_timeout = 3.0;
591+
new_selection.insert(old_path.clone());
592+
}
593+
}
594+
}
595+
state.selected_assets = new_selection;
596+
}
597+
484598
// --- Process pending move (drag-to-folder) ---
485599
if let Some((sources, target)) = state.pending_move.take() {
486600
let mut moved = 0usize;
@@ -837,6 +951,22 @@ fn render_context_menu(
837951
}
838952
state.context_menu_pos = None;
839953
}
954+
} else if state.selected_assets.len() > 1 {
955+
if menu_item(ui, regular::TEXT_AA, "Batch Rename…", "", text_primary) {
956+
let mut assets: Vec<std::path::PathBuf> =
957+
state.selected_assets.iter().cloned().collect();
958+
assets.sort();
959+
state.batch_rename_base = assets
960+
.first()
961+
.and_then(|p| p.file_stem())
962+
.and_then(|s| s.to_str())
963+
.unwrap_or("file")
964+
.to_string();
965+
state.batch_rename_start = 1;
966+
state.batch_rename_assets = assets;
967+
state.batch_rename_active = true;
968+
state.context_menu_pos = None;
969+
}
840970
}
841971

842972
if menu_item(ui, regular::COPY, "Duplicate", "Ctrl+D", text_primary) {

crates/renzora_asset_browser/src/state.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,21 @@ pub struct AssetBrowserState {
116116
// === Pending operations ===
117117
/// Pending rename operation (old_path, new_name).
118118
pub pending_rename: Option<(PathBuf, String)>,
119+
/// Pending batch rename operation (base, start_index, paths).
120+
pub pending_batch_rename: Option<(String, u32, Vec<PathBuf>)>,
119121
/// Pending delete operation.
120122
pub pending_delete: Vec<PathBuf>,
121123
/// Last error message.
122124
pub last_error: Option<String>,
123125
/// Error auto-clear timer.
124126
pub error_timeout: f32,
125127

128+
// === Batch rename dialog ===
129+
pub batch_rename_active: bool,
130+
pub batch_rename_base: String,
131+
pub batch_rename_start: u32,
132+
pub batch_rename_assets: Vec<PathBuf>,
133+
126134
// === Inline create (file created immediately, then enters rename mode) ===
127135
/// When set, a new asset was just created and should enter rename mode.
128136
pub pending_inline_create: Option<PathBuf>,
@@ -185,9 +193,14 @@ impl Default for AssetBrowserState {
185193
drop_target_folder: None,
186194
tree_folder_rects: Vec::new(),
187195
pending_rename: None,
196+
pending_batch_rename: None,
188197
pending_delete: Vec::new(),
189198
last_error: None,
190199
error_timeout: 0.0,
200+
batch_rename_active: false,
201+
batch_rename_base: String::new(),
202+
batch_rename_start: 1,
203+
batch_rename_assets: Vec::new(),
191204
pending_inline_create: None,
192205
drag_moving: Vec::new(),
193206
move_drop_target: None,

0 commit comments

Comments
 (0)