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:
@@ -470,6 +470,19 @@ pub struct App {
|
||||
/// Set alongside `show_add_story_bible_item` so the name field takes
|
||||
/// focus on the frame the dialog opens.
|
||||
add_story_bible_focus: bool,
|
||||
/// Whether the "rename/move Story Bible item" dialog is open.
|
||||
show_rename_story_bible: bool,
|
||||
/// The dialog's path field, pre-filled with the current path (no `.md`)
|
||||
/// when opened, as both a rename and a move, same as the manuscript
|
||||
/// panel's `rename_input`.
|
||||
rename_story_bible_input: String,
|
||||
/// The Story Bible item being renamed/moved: its full path (with `.md`),
|
||||
/// set by the tree's right-click menu. There's no `selected` index to
|
||||
/// recover this from the way the manuscript panel does.
|
||||
rename_story_bible_target: Option<String>,
|
||||
/// Set alongside `show_rename_story_bible` so the path field takes focus
|
||||
/// on the frame the dialog opens.
|
||||
rename_story_bible_focus: bool,
|
||||
/// List of auto-snapshots for rollback.
|
||||
snapshots: Vec<crate::app::auto_snapshot::SnapshotInfo>,
|
||||
/// Word count at the last auto-snapshot.
|
||||
@@ -612,6 +625,10 @@ impl App {
|
||||
add_story_bible_type: String::new(),
|
||||
add_story_bible_name: String::new(),
|
||||
add_story_bible_focus: false,
|
||||
show_rename_story_bible: false,
|
||||
rename_story_bible_input: String::new(),
|
||||
rename_story_bible_target: None,
|
||||
rename_story_bible_focus: false,
|
||||
snapshots: Vec::new(),
|
||||
last_snapshot_word_count: 0,
|
||||
show_snapshots: false,
|
||||
@@ -771,6 +788,10 @@ impl eframe::App for App {
|
||||
self.add_story_bible_item_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_rename_story_bible {
|
||||
self.rename_story_bible_item_window(ctx);
|
||||
}
|
||||
|
||||
if self.show_snapshots {
|
||||
self.snapshots_window(ctx);
|
||||
}
|
||||
|
||||
+135
-10
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user