Let the Story Bible Editor rename/move files

Right-clicking a file row now offers "Rename / Move…", mirroring the
manuscript file panel's combined rename-and-move pattern: editing the
name renames, editing the folder part moves it across the tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 14:08:40 -05:00
parent 7e4932176b
commit 8b352d25ed
2 changed files with 156 additions and 10 deletions
+135 -10
View File
@@ -161,14 +161,16 @@ impl App {
/// Render the Story Bible tree recursively. `add_requested` collects the
/// directory path a right-click menu's "Add … entry…" was clicked for,
/// so the caller can open the add dialog once rendering is done rather
/// than mutating `self` from inside this `&self` walk.
/// and `rename_requested` the file path a "Rename / Move…" was clicked
/// for, so the caller can open the relevant dialog once rendering is
/// done rather than mutating `self` from inside this `&self` walk.
fn render_bible_tree(
&self,
ui: &mut egui::Ui,
nodes: &[BibleTreeNode],
selected_path: &Option<String>,
add_requested: &mut Option<String>,
rename_requested: &mut Option<String>,
) -> Option<String> {
let mut clicked_path: Option<String> = None;
@@ -180,9 +182,13 @@ impl App {
let header = egui::CollapsingHeader::new(name)
.default_open(true)
.show(ui, |ui| {
if let Some(child_clicked) =
self.render_bible_tree(ui, children, selected_path, add_requested)
{
if let Some(child_clicked) = self.render_bible_tree(
ui,
children,
selected_path,
add_requested,
rename_requested,
) {
clicked_path = Some(child_clicked);
}
});
@@ -198,12 +204,16 @@ impl App {
BibleTreeNode::File { name, path } => {
let is_selected = selected_path.as_ref().map_or(false, |p| p == path);
if ui
.selectable_label(is_selected, name)
.clicked()
{
let response = ui.selectable_label(is_selected, name);
if response.clicked() {
clicked_path = Some(path.clone());
}
response.context_menu(|ui| {
if ui.button("📝 Rename / Move…").clicked() {
*rename_requested = Some(path.clone());
ui.close_menu();
}
});
}
}
}
@@ -260,6 +270,9 @@ impl App {
// Directory a right-click menu's "Add … entry…" was clicked for, if
// any — applied after the tree finishes rendering, same reason.
let mut add_requested: Option<String> = None;
// File path a right-click menu's "Rename / Move…" was clicked for,
// if any — same deferred-handling reason as `add_requested`.
let mut rename_requested: Option<String> = None;
ui.horizontal(|ui| {
// Left panel: Tree view (1/3 width). The width must be set on
// this panel's own child `Ui`, not the shared horizontal one —
@@ -293,7 +306,11 @@ impl App {
} else {
// Render tree
if let Some(clicked_path) = self.render_bible_tree(
ui, tree, &self.story_bible_editor_path, &mut add_requested,
ui,
tree,
&self.story_bible_editor_path,
&mut add_requested,
&mut rename_requested,
) {
// Load the clicked file
self.load_story_bible_item(&clicked_path);
@@ -370,6 +387,13 @@ impl App {
self.show_add_story_bible_item = true;
self.add_story_bible_focus = true;
}
if let Some(path) = rename_requested {
self.rename_story_bible_input = strip_md(&path).to_string();
self.rename_story_bible_target = Some(path);
self.show_rename_story_bible = true;
self.rename_story_bible_focus = true;
}
}
/// Load a Story Bible item from the workspace.
@@ -525,6 +549,107 @@ impl App {
}
}
/// The rename/move dialog opened from a file row's context menu. Renaming
/// and moving are the same operation here, same as the manuscript file
/// panel's `rename_window`: editing the leaf renames, editing the folder
/// part moves.
pub(super) fn rename_story_bible_item_window(&mut self, ctx: &egui::Context) {
if self.rename_story_bible_target.is_none() {
// The tree changed underneath us; nothing left to rename.
self.show_rename_story_bible = false;
return;
}
let mut open = self.show_rename_story_bible;
let mut submit = false;
let mut cancel = false;
egui::Window::new("Rename / Move Story Bible item")
.open(&mut open)
.resizable(false)
.collapsible(false)
.default_width(320.0)
.show(ctx, |ui| {
if let Some(ref current) = self.rename_story_bible_target {
ui.label(egui::RichText::new(current.as_str()).small().weak());
}
ui.add_space(4.0);
let r = ui.add(
egui::TextEdit::singleline(&mut self.rename_story_bible_input)
.desired_width(f32::INFINITY)
.hint_text("name, or a path to move it"),
);
if self.rename_story_bible_focus {
r.request_focus();
self.rename_story_bible_focus = false;
}
submit |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.label(
egui::RichText::new(
"The path within the Story Bible folder — edit the folder part \
to move it. The .md extension is added for you.",
)
.small()
.weak(),
);
ui.separator();
ui.horizontal(|ui| {
submit |= ui.button("Rename").clicked();
cancel |= ui.button("Cancel").clicked();
});
});
if cancel || !open {
self.show_rename_story_bible = false;
return;
}
if submit {
if self.rename_selected_story_bible_item() {
self.show_rename_story_bible = false;
} else {
self.rename_story_bible_focus = true;
}
}
}
/// Move/rename the Story Bible item named by `rename_story_bible_target`
/// to `rename_story_bible_input`. Returns `false` (leaving the dialog
/// open) when the input isn't usable or the destination is taken.
fn rename_selected_story_bible_item(&mut self) -> bool {
let Some(old_path) = self.rename_story_bible_target.clone() else {
return false;
};
let Some(new_path) = sanitize_rel_path(&self.rename_story_bible_input) else {
self.status = "Enter a new name".to_string();
return false;
};
if new_path == old_path {
// Already what was asked for.
self.rename_story_bible_target = None;
return true;
}
let Some(base_dir) = self.find_story_bible_directory() else {
self.status = "Story Bible directory not found".to_string();
return false;
};
if base_dir.join(&new_path).exists() {
self.status = format!("{new_path} already exists");
return false;
}
match workspace::move_manuscript_file(&base_dir, &old_path, &new_path) {
Ok(()) => {
if self.story_bible_editor_path.as_deref() == Some(old_path.as_str()) {
self.story_bible_editor_path = Some(new_path.clone());
}
self.build_story_bible_index();
self.status = format!("Renamed to {new_path}");
self.rename_story_bible_target = None;
true
}
Err(e) => {
self.status = format!("Rename failed: {e}");
false
}
}
}
/// Open the Story Bible editor.
pub(super) fn open_story_bible_editor(&mut self) {
self.show_story_bible_editor = true;