Add a right-click context menu to file rows
Renaming, archiving and deleting a file all lived in buttons under the list, and all of them required selecting the file first. The same operations are now on the row itself: right-click a file for Rename, Copy path, Archive and Delete. The menu records what was picked rather than acting on it, because the list is drawn inside a closure still borrowing `self` — the same deferred pattern the click and drag-drop handling already use. Right- clicking selects the row first, so the menu always acts on the file you clicked; selecting is a no-op when it is already current, so this cannot reload the buffer out from under an unsaved edit. Rename opens a focused dialog with the path pre-filled, and stays open if the name is rejected so a clash does not cost what was typed; rename_selected reports success for that. Delete asks first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
This commit is contained in:
@@ -172,6 +172,9 @@ impl App {
|
||||
let mut clicked: Option<usize> = None;
|
||||
let mut toggled: Option<String> = None;
|
||||
let mut dropped: Option<FileDrop> = None;
|
||||
// What the right-click menu picked, applied further down: the
|
||||
// row closure below is still borrowing `self` to draw with.
|
||||
let mut action: Option<FileAction> = None;
|
||||
let pointer = ui.input(|i| i.pointer.interact_pos());
|
||||
// Filtering happens on the flat list, so folders left with no
|
||||
// files simply stop appearing.
|
||||
@@ -349,6 +352,14 @@ impl App {
|
||||
if label.clicked() {
|
||||
clicked = Some(idx);
|
||||
}
|
||||
label.context_menu(|ui| {
|
||||
file_context_menu(
|
||||
ui,
|
||||
&name,
|
||||
idx,
|
||||
&mut action,
|
||||
);
|
||||
});
|
||||
if let Some(goal) = meta.goal {
|
||||
row_goal_bar(
|
||||
ui,
|
||||
@@ -440,6 +451,10 @@ impl App {
|
||||
if let Some(drop) = dropped {
|
||||
self.apply_drop(drop);
|
||||
}
|
||||
if let Some(action) = action {
|
||||
let ctx = ui.ctx().clone();
|
||||
self.apply_file_action(action, &ctx);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
@@ -517,6 +532,74 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a file row's right-click menu asked for. The menu records the choice
|
||||
/// rather than acting on it, because the list is drawn inside a closure that is
|
||||
/// still borrowing `self`; [`App::apply_file_action`] runs it once that ends.
|
||||
///
|
||||
/// Each carries the row's own index, so the menu acts on the file you clicked
|
||||
/// even when a different one is selected.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(super) enum FileAction {
|
||||
/// Open the rename dialog on this file.
|
||||
Rename(usize),
|
||||
/// Put its workspace-relative path on the clipboard.
|
||||
CopyPath(usize),
|
||||
/// Move it to the archive folder.
|
||||
Archive(usize),
|
||||
/// Open the delete confirmation for it.
|
||||
Delete(usize),
|
||||
}
|
||||
|
||||
impl FileAction {
|
||||
/// The row this action came from.
|
||||
pub(super) fn index(self) -> usize {
|
||||
match self {
|
||||
FileAction::Rename(i)
|
||||
| FileAction::CopyPath(i)
|
||||
| FileAction::Archive(i)
|
||||
| FileAction::Delete(i) => i,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The right-click menu on a file row, offering the same operations as the
|
||||
/// buttons under the list without having to select the file first.
|
||||
fn file_context_menu(ui: &mut egui::Ui, name: &str, idx: usize, action: &mut Option<FileAction>) {
|
||||
ui.label(egui::RichText::new(base_name(name)).strong());
|
||||
ui.separator();
|
||||
let mut pick = |ui: &mut egui::Ui, label: &str, hover: &str, chosen: FileAction| {
|
||||
if ui.button(label).on_hover_text(hover).clicked() {
|
||||
*action = Some(chosen);
|
||||
ui.close_menu();
|
||||
}
|
||||
};
|
||||
pick(
|
||||
ui,
|
||||
"\u{1f4dd} Rename\u{2026}",
|
||||
"Rename the file, or edit the folder part of its path to move it",
|
||||
FileAction::Rename(idx),
|
||||
);
|
||||
pick(
|
||||
ui,
|
||||
"\u{29c9} Copy path",
|
||||
"Copy the file's path within the workspace to the clipboard",
|
||||
FileAction::CopyPath(idx),
|
||||
);
|
||||
ui.separator();
|
||||
pick(
|
||||
ui,
|
||||
"\u{1f5c4} Archive",
|
||||
"Move it into the archive folder and out of the manuscript, keeping it on disk",
|
||||
FileAction::Archive(idx),
|
||||
);
|
||||
pick(
|
||||
ui,
|
||||
"\u{1f5d1} Delete\u{2026}",
|
||||
"Remove it from disk \u{2014} asks first",
|
||||
FileAction::Delete(idx),
|
||||
);
|
||||
}
|
||||
|
||||
/// Outline a whole-row drop target while a file is dragged over it, and report
|
||||
/// whether it is being hovered (so the caller can look for the release).
|
||||
fn drop_highlight(ui: &egui::Ui, response: &egui::Response) -> bool {
|
||||
@@ -639,6 +722,21 @@ pub(super) fn row_goal_bar(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_file_action_carries_the_row_it_came_from() {
|
||||
// The four arms of `index` are near-identical, so a copy-paste slip
|
||||
// would silently point an action at the wrong file — and for Delete
|
||||
// that is destructive. Pin every variant to the row it was built with.
|
||||
for action in [
|
||||
FileAction::Rename(3),
|
||||
FileAction::CopyPath(3),
|
||||
FileAction::Archive(3),
|
||||
FileAction::Delete(3),
|
||||
] {
|
||||
assert_eq!(action.index(), 3, "{action:?} lost its row index");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_title_falls_back_to_position() {
|
||||
// No override, no header title -> 1-based index with a period (width 1).
|
||||
|
||||
Reference in New Issue
Block a user