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
|
/// Set alongside `show_add_story_bible_item` so the name field takes
|
||||||
/// focus on the frame the dialog opens.
|
/// focus on the frame the dialog opens.
|
||||||
add_story_bible_focus: bool,
|
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.
|
/// List of auto-snapshots for rollback.
|
||||||
snapshots: Vec<crate::app::auto_snapshot::SnapshotInfo>,
|
snapshots: Vec<crate::app::auto_snapshot::SnapshotInfo>,
|
||||||
/// Word count at the last auto-snapshot.
|
/// Word count at the last auto-snapshot.
|
||||||
@@ -612,6 +625,10 @@ impl App {
|
|||||||
add_story_bible_type: String::new(),
|
add_story_bible_type: String::new(),
|
||||||
add_story_bible_name: String::new(),
|
add_story_bible_name: String::new(),
|
||||||
add_story_bible_focus: false,
|
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(),
|
snapshots: Vec::new(),
|
||||||
last_snapshot_word_count: 0,
|
last_snapshot_word_count: 0,
|
||||||
show_snapshots: false,
|
show_snapshots: false,
|
||||||
@@ -771,6 +788,10 @@ impl eframe::App for App {
|
|||||||
self.add_story_bible_item_window(ctx);
|
self.add_story_bible_item_window(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.show_rename_story_bible {
|
||||||
|
self.rename_story_bible_item_window(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if self.show_snapshots {
|
if self.show_snapshots {
|
||||||
self.snapshots_window(ctx);
|
self.snapshots_window(ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-10
@@ -161,14 +161,16 @@ impl App {
|
|||||||
|
|
||||||
/// Render the Story Bible tree recursively. `add_requested` collects the
|
/// Render the Story Bible tree recursively. `add_requested` collects the
|
||||||
/// directory path a right-click menu's "Add … entry…" was clicked for,
|
/// 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
|
/// and `rename_requested` the file path a "Rename / Move…" was clicked
|
||||||
/// than mutating `self` from inside this `&self` walk.
|
/// 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(
|
fn render_bible_tree(
|
||||||
&self,
|
&self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
nodes: &[BibleTreeNode],
|
nodes: &[BibleTreeNode],
|
||||||
selected_path: &Option<String>,
|
selected_path: &Option<String>,
|
||||||
add_requested: &mut Option<String>,
|
add_requested: &mut Option<String>,
|
||||||
|
rename_requested: &mut Option<String>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut clicked_path: Option<String> = None;
|
let mut clicked_path: Option<String> = None;
|
||||||
|
|
||||||
@@ -180,9 +182,13 @@ impl App {
|
|||||||
let header = egui::CollapsingHeader::new(name)
|
let header = egui::CollapsingHeader::new(name)
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
if let Some(child_clicked) =
|
if let Some(child_clicked) = self.render_bible_tree(
|
||||||
self.render_bible_tree(ui, children, selected_path, add_requested)
|
ui,
|
||||||
{
|
children,
|
||||||
|
selected_path,
|
||||||
|
add_requested,
|
||||||
|
rename_requested,
|
||||||
|
) {
|
||||||
clicked_path = Some(child_clicked);
|
clicked_path = Some(child_clicked);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -198,12 +204,16 @@ impl App {
|
|||||||
BibleTreeNode::File { name, path } => {
|
BibleTreeNode::File { name, path } => {
|
||||||
let is_selected = selected_path.as_ref().map_or(false, |p| p == path);
|
let is_selected = selected_path.as_ref().map_or(false, |p| p == path);
|
||||||
|
|
||||||
if ui
|
let response = ui.selectable_label(is_selected, name);
|
||||||
.selectable_label(is_selected, name)
|
if response.clicked() {
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
clicked_path = Some(path.clone());
|
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
|
// Directory a right-click menu's "Add … entry…" was clicked for, if
|
||||||
// any — applied after the tree finishes rendering, same reason.
|
// any — applied after the tree finishes rendering, same reason.
|
||||||
let mut add_requested: Option<String> = None;
|
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| {
|
ui.horizontal(|ui| {
|
||||||
// Left panel: Tree view (1/3 width). The width must be set on
|
// Left panel: Tree view (1/3 width). The width must be set on
|
||||||
// this panel's own child `Ui`, not the shared horizontal one —
|
// this panel's own child `Ui`, not the shared horizontal one —
|
||||||
@@ -293,7 +306,11 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
// Render tree
|
// Render tree
|
||||||
if let Some(clicked_path) = self.render_bible_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
|
// Load the clicked file
|
||||||
self.load_story_bible_item(&clicked_path);
|
self.load_story_bible_item(&clicked_path);
|
||||||
@@ -370,6 +387,13 @@ impl App {
|
|||||||
self.show_add_story_bible_item = true;
|
self.show_add_story_bible_item = true;
|
||||||
self.add_story_bible_focus = 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.
|
/// 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.
|
/// Open the Story Bible editor.
|
||||||
pub(super) fn open_story_bible_editor(&mut self) {
|
pub(super) fn open_story_bible_editor(&mut self) {
|
||||||
self.show_story_bible_editor = true;
|
self.show_story_bible_editor = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user