Enable Project panel Reveal in Finder for clips with a media file.

file.reveal is enabled when a selected clip has a local path, greyed out
with no selection, and the OS opener reveals the file (xdg-open of the
parent folder on Linux).
This commit is contained in:
Vikram Bhaskaran 2026-10-11 13:02:55 +11:00
parent 4e2d0bc2d3
commit 488259414b
No known key found for this signature in database
GPG Key ID: 5CFB5F821425464B
6 changed files with 45 additions and 4 deletions

View File

@ -319,6 +319,7 @@ fn open_path(path: &str, reveal: bool) -> Result<(), String> {
}
c
} else {
// Linux file managers have no select-file flag: open the parent folder instead.
let mut c = std::process::Command::new("xdg-open");
let p = std::path::Path::new(path);
c.arg(if reveal { p.parent().unwrap_or(p) } else { p });

View File

@ -14,6 +14,7 @@
//! | `file.projectSettings.general` / `.scratchDisks` | File ▸ Project Settings ▸ General… / Scratch Disks… |
//! | `edit.find` / `edit.findNext` | Edit ▸ Find… (⌘F) / Find Next |
//! | `edit.editOriginal` | Edit ▸ Edit Original (⌘E): the frontend opens the files |
//! | `file.reveal` | Project panel ▸ Reveal in Finder / Show in Explorer: the frontend reveals the media file |
//! | `clip.editOffline` | Clip ▸ Edit Offline… |
//! | `clip.sourceSettings` | Clip ▸ Source Settings… |
//! | `clip.restoreCaptionsFromSource` | Clip ▸ Restore Captions from Source Clip (always disabled: no embedded caption reader) |
@ -163,6 +164,8 @@ pub(crate) fn commands() -> Vec<CommandSpec> {
spec("edit.find", "Find…", &["Edit"], Some("Cmd+F"), FIND_PARAMS, always, find),
spec("edit.findNext", "Find Next", &["Edit"], None, "{}", has_find, find_next),
spec("edit.editOriginal", "Edit Original", &["Edit"], Some("Cmd+E"), r#"{"items":[id]?}"#, has_original, edit_original),
// Project panel context menu (not in the File menu): reveal the clip's media file.
query("file.reveal", "Reveal in Finder", &[], None, r#"{"items":[id]?}"#, has_file_selection, edit_original),
// ---------------- Clip ----------------
spec(
"clip.editOffline",

View File

@ -361,6 +361,18 @@ fn edit_original_update_metadata_and_clip_items() {
assert!(std::fs::read_to_string(&side).unwrap().contains("someone else"));
}
#[test]
fn file_reveal_enabled_for_imported_clip() {
let dir = tmp_dir("reveal");
let path = dir.join("take.mp4");
make_movie(&path, DemoScene::Aurora, 64, 36, 8);
let (mut s, items, _) = session_with(&[&path]);
assert!(!s.is_enabled("file.reveal"), "disabled with no Project panel selection");
s.execute("project.select", json!({"items": [items[0].0]})).unwrap();
assert!(s.is_enabled("file.reveal"));
assert_eq!(s.execute("file.reveal", json!({})).unwrap()["paths"], json!([path.to_string_lossy()]));
}
#[test]
fn edit_offline_sets_the_log_fields() {
let mut s = demo();

View File

@ -3,8 +3,8 @@
//! Update Metadata, Automate to Sequence, Scene Edit Detection, Normalize Mix Track, Simplify
//! Sequence, Transcribe Sequence, Save as Template, Add Flash Cue Marker and the System
//! Compatibility Report; plus the frontend side of Edit Original / Reveal Log Files (the OS
//! opener), Generate Audio Waveform (the timeline's peak cache), Import from Media Browser (the
//! Media Browser selection) and View ▸ Dynamic Audio Waveforms.
//! opener), Reveal in Finder, Generate Audio Waveform (the timeline's peak cache), Import from
//! Media Browser (the Media Browser selection) and View ▸ Dynamic Audio Waveforms.
//!
//! The open dialog lives in `UiState::extras.dialog` (serde, like `clip_dialogs`): the engine
//! command it runs and the parameters being edited, so agents open it from the menu, change
@ -185,11 +185,12 @@ pub fn route(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, params: &Val
app.ui.extras.dialog = Some(ClipDialogDraft { command: id.into(), params: json!({}), info: report.clone(), error: String::new() });
return Some(Ok(json!({"dialog": "systemReport", "report": report})));
}
"edit.editOriginal" => {
"edit.editOriginal" | "file.reveal" => {
let reveal = id == "file.reveal";
let r = app.session.execute(id, params.clone()).map_err(|e| e.to_string());
return Some(r.and_then(|v| {
for p in v["paths"].as_array().cloned().unwrap_or_default() {
open_path(app, ctx, p.as_str().unwrap_or_default(), false)?;
open_path(app, ctx, p.as_str().unwrap_or_default(), reveal)?;
}
Ok(v)
}));

View File

@ -262,6 +262,13 @@ fn item_menu(app: &mut FilmcraftApp, ui: &mut egui::Ui, id: ItemId, kind: &ItemK
}
}
});
let can_reveal = app.session.project.resolve_media(id).is_some_and(|(_, m, _)| matches!(m.media, filmcraft_project::MediaRef::File { .. }));
let b = ui.add_enabled(can_reveal, egui::Button::new(tl!("Reveal in Finder")));
app.auto.add("project.itemMenu.file.reveal", b.rect, "Reveal in Finder");
if b.clicked() {
actions.push(("file.reveal".into(), json!({"items": [id.0]})));
ui.close();
}
if matches!(kind, ItemKind::Media(m) if matches!(m.media, filmcraft_project::MediaRef::File { .. })) {
ui.separator();
for (label, cmd) in [(tl!("Link Media…"), "media.linkMedia"), (tl!("Make Offline…"), "media.makeOffline")] {

View File

@ -305,3 +305,20 @@ fn color_matte_dialog_picks_and_changes_the_color() {
assert_eq!(d.call("ui.menu.invoke", json!({"id": "project.matteColor"}))["ok"], json!(false));
assert!(!d.dialog_open());
}
#[test]
fn reveal_in_finder_opens_an_imported_clip() {
let dir = std::env::temp_dir().join(format!("fc-ui-reveal-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("clip.png");
image::RgbaImage::from_pixel(8, 8, image::Rgba([10, 20, 30, 255])).save(&path).unwrap();
let mut d = Driver::new();
let item = d.exec("file.import", json!({"paths": [path.to_string_lossy()]}))["items"][0].as_u64().unwrap();
assert!(!d.app().session.is_enabled("file.reveal"));
d.exec("project.select", json!({"items": [item]}));
assert!(d.app().session.is_enabled("file.reveal"));
let r = d.ok("ui.menu.invoke", json!({"id": "file.reveal"}));
assert_eq!(r["paths"][0], path.to_string_lossy().as_ref());
assert_eq!(d.app().ui.extras.opened.last().map(String::as_str), Some(path.to_string_lossy().as_ref()));
let _ = std::fs::remove_dir_all(&dir);
}