Skip to main content

max / audiofiles

The file-list flip stranded three features; keep them, file them `cefe4159` said to check whether `ui/file_list_menus.rs` still had callers and, if not, to delete it in the same pass. It has none for four of its five public items, and deleting them would have been wrong: three are features that stopped working, not dead code. Measured, by caller count outside the file: `reveal` and `reveal_label` are live and used by the described list. `draw_context_menu` has none and is genuinely superseded -- `quasi::files` describes the row menu through `Cells::menu` -- so it goes, 189 lines. The other three have none and nothing replaced them: draw_multi_context_menu the selection menu, 11 entries draw_background_context_menu the empty-space menu, 5 entries start_os_drag dragging a sample out to a DAW `crate::drag_out` is reachable only through the last of those, so the whole module is stranded with it. For a sample manager, dragging into a DAW is close to the point of the app. `49b7429` is where they went: it replaced the shipped file list with `quasi::files`, which describes the row menu and says plainly that it does not describe the other two menus or the drag. What went unnoticed is that the shipped call sites went at the same time, so the undescribed half did not stay working beside the described one. Kept rather than deleted, because deleting is losing the features quietly -- which `da48cb6d` allowed for the settings sections only because each had a task. This is that task: `edd4d1d8`, with what each is waiting on. The two menus want `quasi:vocabulary:anchored-menu`, since a menu over a selection or a surface has no container; the drag is host input and `quasi::files` never meant to carry it. Second door-shaped hole from the same flip, after `8d5519cb`.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-25 22:32 UTC
Signed with PGP, not checked
Commit: 7977ca182a726bf9b3e599bb150af9ee061e0b59
Parent: 7cb0ece
1 file changed, +18 insertions, -192 deletions
@@ -1,11 +1,26 @@
1 - //! Context menus and drag-out handlers extracted from file_list.rs.
1 + //! What is left of the file list's context menus, plus the drag-out handler.
2 + //!
3 + //! **Three of the four things in here have no caller**, and that is a regression
4 + //! rather than dead code: the file-list flip (`49b7429`) replaced the shipped
5 + //! list with `quasi::files`, which describes the *row* menu through
6 + //! [`Cells::menu`](quasi_router::Cells) and does not describe the other two
7 + //! menus or the drag at all. So the selection menu, the empty-space menu and
8 + //! dragging a sample out to a DAW are all unreachable in the running app.
9 + //!
10 + //! They are kept rather than deleted, because deleting them is losing the
11 + //! features quietly and each is waiting on something specific:
12 + //! `quasi:vocabulary:anchored-menu` for the two menus (a menu over a selection
13 + //! or over a surface has no container in the vocabulary), and a host-input
14 + //! answer for the drag, which `quasi::files` records as deliberately undescribed.
15 + //! Filed as audiofiles `edd4d1d8`.
16 + //!
17 + //! `draw_context_menu` was the fourth and is deleted (2026-08-25): its
18 + //! replacement ships, so it was the one thing in here that really was dead.
2 19
3 20 use egui;
4 21
5 22 use crate::state::BrowserState;
6 - use audiofiles_core::vfs::NodeType;
7 23
8 - use super::theme;
9 24 use super::widgets;
10 25
11 26 #[cfg(any(target_os = "macos", target_os = "windows"))]
@@ -58,195 +73,6 @@
58 73 }
59 74 }
60 75
61 - /// Draw the right-click context menu for a single item.
62 - /// Branches on node type: samples get Preview/Copy Path/Delete,
63 - /// directories get Open/Delete.
64 - pub fn draw_context_menu(
65 - ui: &mut egui::Ui,
66 - state: &mut BrowserState,
67 - row_idx: usize,
68 - node: &audiofiles_core::vfs::VfsNodeWithAnalysis,
69 - sync_manager: Option<&audiofiles_sync::SyncManager>,
70 - ) {
71 - match node.node.node_type {
72 - NodeType::Sample => {
73 - if node.cloud_only {
74 - ui.label(
75 - egui::RichText::new("Cloud-only sample")
76 - .color(theme::content_muted())
77 - .italics(),
78 - );
79 - // Targeted download for the row under the cursor. Falls back
80 - // gracefully when sync isn't configured (CLAP plugin, dev
81 - // builds without an embedded API key) by hiding the item.
82 - if let Some(sync) = sync_manager
83 - && let Some(hash) = &node.node.sample_hash
84 - && ui
85 - .button("Download")
86 - .on_hover_text("Fetch this sample from the cloud to local storage")
87 - .clicked()
88 - {
89 - let hash = hash.to_string();
90 - if sync.download_sample(&hash) {
91 - state.status = format!("Downloading {}...", node.node.name);
92 - } else {
93 - state.status = "Sync not ready, open the Sync panel first".to_string();
94 - }
95 - ui.close();
96 - }
97 - ui.separator();
98 - }
99 - if !node.cloud_only && ui.button("Preview").clicked() {
100 - if let Some(hash) = &node.node.sample_hash {
101 - let hash = hash.clone();
102 - state.trigger_preview(&hash);
103 - }
104 - ui.close();
105 - }
106 - if ui.button("Copy Path").clicked() {
107 - if let Some(path) = state.selected_sample_path() {
108 - state.status = format!("Copied: {path}");
109 - ui.ctx().copy_text(path);
110 - }
111 - ui.close();
112 - }
113 - // M-6: one-click jump to the file in the system file manager. The
114 - // label and the spawning both live at the top of this file now, so
115 - // the described file list offers the same act under the same name.
116 - if !node.cloud_only && ui.button(reveal_label()).clicked() {
117 - if let Some(path) = state.selected_sample_path() {
118 - reveal(&path);
119 - }
120 - ui.close();
121 - }
122 - if ui.button("Find Similar (Shift+F)").clicked() {
123 - if let Some(hash) = &node.node.sample_hash {
124 - let hash = hash.clone();
125 - state.find_similar(&hash);
126 - }
127 - ui.close();
128 - }
129 - if ui.button("Find Duplicates (Shift+D)").clicked() {
130 - if let Some(hash) = &node.node.sample_hash {
131 - let hash = hash.clone();
132 - state.find_near_duplicates(&hash);
133 - }
134 - ui.close();
135 - }
136 - // Add to Collection submenu
137 - if let Some(hash) = &node.node.sample_hash {
138 - let hash_clone = hash.clone();
139 - let collections = state.collections_ui.collections.clone();
140 - let is_in_collection = state.collections_ui.active_collection.is_some();
141 - if !collections.is_empty() {
142 - ui.menu_button("Add to Collection", |ui| {
143 - for coll in collections.iter() {
144 - if ui.button(&coll.name).clicked() {
145 - let _ = state.backend.add_to_collection(coll.id, &hash_clone);
146 - state.refresh_collections();
147 - state.status = format!("Added to {}", coll.name);
148 - ui.close();
149 - }
150 - }
151 - });
152 - }
153 - if is_in_collection
154 - && let Some(active_id) = state.collections_ui.active_collection
155 - && widgets::danger_button(ui, "Remove from Collection").clicked()
156 - {
157 - let _ = state.backend.remove_from_collection(active_id, &hash_clone);
158 - state.refresh_collections();
159 - state.activate_collection(active_id);
160 - ui.close();
161 - }
162 - }
163 - if !node.cloud_only {
164 - if let Some(hash) = &node.node.sample_hash {
165 - let hash_clone = hash.clone();
166 - if ui.button("Edit... (E)").clicked() {
167 - state.open_edit_window(&hash_clone);
168 - ui.close();
169 - }
170 - }
171 - if ui.button("Play as Instrument").clicked() {
172 - if let Some(hash) = &node.node.sample_hash {
173 - let hash = hash.clone();
174 - let name = node.node.name.clone();
175 - state.load_chromatic_sample(&hash);
176 - state.preview.instrument_visible = true;
177 - state.preview.show_midi_window = true;
178 - state.status = format!("Instrument: {name}");
179 - }
180 - ui.close();
181 - }
182 - if ui.button("Export...").clicked() {
183 - state.nav.selection.set_single(row_idx);
184 - state.start_export_flow(Some(vec![node.node.id]));
185 - ui.close();
186 - }
187 - // M-7: single-row Re-analyze parity with the multi-row menu.
188 - // Reuses ReanalyzeOverwrite with a one-element vec so the
189 - // backend path matches the bulk case exactly.
190 - if ui
191 - .button("Re-analyze...")
192 - .on_hover_text("Run analysis again on this sample")
193 - .clicked()
194 - {
195 - if let Some(hash) = &node.node.sample_hash
196 - && let Ok(ext) = state.backend.sample_extension(hash)
197 - {
198 - let hashes = vec![(hash.to_string(), ext)];
199 - let has_existing = node.bpm.is_some() || node.musical_key.is_some();
200 - if has_existing {
201 - state.overlay.pending_confirm =
202 - Some(crate::state::ConfirmAction::ReanalyzeOverwrite {
203 - sample_hashes: hashes,
204 - overwrite_count: 1,
205 - });
206 - } else {
207 - state.start_analysis_flow(hashes);
208 - }
209 - }
210 - ui.close();
211 - }
212 - }
213 - ui.separator();
214 - if widgets::danger_button(ui, "Delete").clicked() {
215 - state.nav.selection.set_single(row_idx);
216 - state.confirm_delete_selected();
217 - ui.close();
218 - }
219 - }
220 - NodeType::Directory => {
221 - if ui.button("Open").clicked() {
222 - state.nav.selection.set_single(row_idx);
223 - state.enter_directory();
224 - ui.close();
225 - }
226 - if ui.button("New Folder").clicked() {
227 - state.vfs_modal.show_dir_create = true;
228 - state.vfs_modal.dir_create_input.clear();
229 - ui.close();
230 - }
231 - if ui.button("Rename").clicked() {
232 - state.vfs_modal.dir_rename_target = Some((node.node.id, node.node.name.clone()));
233 - ui.close();
234 - }
235 - if ui.button("Export...").clicked() {
236 - state.nav.selection.set_single(row_idx);
237 - state.start_export_flow(Some(vec![node.node.id]));
238 - ui.close();
239 - }
240 - ui.separator();
241 - if widgets::danger_button(ui, "Delete").clicked() {
242 - state.nav.selection.set_single(row_idx);
243 - state.confirm_delete_selected();
244 - ui.close();
245 - }
246 - }
247 - }
248 - }
249 -
250 76 /// Context menu when multiple items are selected.
251 77 pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
252 78 let count = state.nav.selection.count();