mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-30 13:07:19 -06:00
NEST6: Reveal in Project and Reveal Sequence in Project
clip.revealInProject (clip context menu) selects the clip's source in the Project panel and shows it there: the panel comes forward with the search cleared, the list opens the bins on the way, and the icon and freeform views go into the bin that holds the item. sequence.revealInProject (Timeline tab menu) does the same for the sequence being edited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
598268721a
commit
4164fa2e7c
@@ -1240,6 +1240,26 @@ fn build() -> Vec<CommandSpec> {
|
||||
crate::clip_ops::add_frame_hold(s, p)
|
||||
}),
|
||||
cmd!("clip.nest", "Nest…", ["Clip"], None, r#"{"name":str}"#, has_selection, |s, p| nest(s, p)),
|
||||
// Reveal in Project (clip context menu) and Reveal Sequence in Project (Timeline tab menu):
|
||||
// select the item in the Project panel and show it there
|
||||
cmd!("clip.revealInProject", "Reveal in Project", [], None, r#"{"clip":id?}"#, has_seq, |s, p| {
|
||||
let seq = s.active_sequence().ok_or(EngineError::NoSequence)?;
|
||||
let t = s.playhead();
|
||||
let tg = s.targeting().targeted;
|
||||
// the named or first selected clip, else the topmost clip under the playhead on a targeted track
|
||||
let item = clips_p(s, p)
|
||||
.iter()
|
||||
.find_map(|c| seq.find_item(*c).map(|(_, i)| i.item))
|
||||
.or_else(|| {
|
||||
seq.video_tracks.iter().rev().chain(seq.audio_tracks.iter()).filter(|tr| tg.contains(&tr.id)).find_map(|tr| tr.item_at(t).map(|i| i.item))
|
||||
})
|
||||
.ok_or_else(|| EngineError::Other("select a clip to reveal".into()))?;
|
||||
reveal_in_project(s, item)
|
||||
}),
|
||||
cmd!("sequence.revealInProject", "Reveal Sequence in Project", [], None, r#"{"item":id?}"#, has_seq, |s, p| {
|
||||
let item = item_p(p, "item").or(s.state.active_sequence).ok_or(EngineError::NoSequence)?;
|
||||
reveal_in_project(s, item)
|
||||
}),
|
||||
// ================= Sequence =================
|
||||
cmd!("sequence.open", "Open in Timeline", [], None, r#"{"item":id}"#, always, |s, p| {
|
||||
let id = item_p(p, "item").ok_or_else(|| bad("sequence.open", "need `item`"))?;
|
||||
@@ -2576,6 +2596,14 @@ fn paste(s: &mut Session, insert: bool) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Select `item` in the Project panel and ask the UI to show it there.
|
||||
fn reveal_in_project(s: &mut Session, item: ItemId) -> Result<Value> {
|
||||
s.project.item(item).ok_or_else(|| EngineError::Other("that clip's source is not in the project".into()))?;
|
||||
s.state.project_selection = vec![item];
|
||||
s.events.push(crate::Event::RevealInProject(item));
|
||||
Ok(json!({"item": item.0}))
|
||||
}
|
||||
|
||||
/// The name Nest… offers: the first unused "Nested Sequence NN".
|
||||
pub fn next_nested_name(s: &Session) -> String {
|
||||
(1..=s.project.items.len().saturating_add(1))
|
||||
|
||||
@@ -299,10 +299,17 @@ pub struct EditorState {
|
||||
/// Events for frontends (drained each frame).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub enum Event {
|
||||
ProjectChanged { revision: u64 },
|
||||
Toast { message: String, error: bool },
|
||||
ProjectChanged {
|
||||
revision: u64,
|
||||
},
|
||||
Toast {
|
||||
message: String,
|
||||
error: bool,
|
||||
},
|
||||
OpenSequence(ItemId),
|
||||
OpenSource(ItemId),
|
||||
/// Show this item in the Project panel (Reveal in Project).
|
||||
RevealInProject(ItemId),
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Editing with nested sequences: the nest toggle ("Insert and overwrite sequences as nests or
|
||||
//! individual clips"), a sequence as a source, and Match Frame on a nest.
|
||||
//! individual clips"), a sequence as a source, Match Frame on a nest, and Reveal in Project.
|
||||
//! Premiere's behaviour was observed in Premiere Pro 26.5.2.
|
||||
|
||||
use super::*;
|
||||
@@ -187,6 +187,31 @@ fn match_frame_on_a_nest_loads_the_nested_sequence_at_the_frame_it_shows() {
|
||||
assert_eq!((s.state.source_item, s.state.source_playhead), (Some(nested), rate.tick_of(5 + 8)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_in_project_selects_the_clips_item_and_asks_the_ui_to_show_it() {
|
||||
let mut s = demo();
|
||||
let v1 = clips(&s, TrackKind::Video, 0);
|
||||
s.drain_events();
|
||||
// the selected clip
|
||||
s.execute("timeline.select", json!({"clips": [v1[2].id.0]})).unwrap();
|
||||
assert_eq!(s.execute("clip.revealInProject", json!({})).unwrap()["item"], v1[2].item.0);
|
||||
assert_eq!(s.state.project_selection, vec![v1[2].item]);
|
||||
assert!(s.drain_events().iter().any(|e| matches!(e, Event::RevealInProject(i) if *i == v1[2].item)));
|
||||
// a named clip
|
||||
s.execute("clip.revealInProject", json!({"clip": v1[0].id.0})).unwrap();
|
||||
assert_eq!(s.state.project_selection, vec![v1[0].item]);
|
||||
// nothing selected: the clip under the playhead on a targeted track
|
||||
s.execute("timeline.select", json!({"clips": []})).unwrap();
|
||||
s.set_playhead(v1[3].start + Tick(v1[3].duration.0 / 2));
|
||||
s.execute("clip.revealInProject", json!({})).unwrap();
|
||||
assert_eq!(s.state.project_selection, vec![v1[3].item]);
|
||||
// the sequence in the Timeline
|
||||
let main = s.state.active_sequence.unwrap();
|
||||
s.execute("sequence.revealInProject", json!({})).unwrap();
|
||||
assert_eq!(s.state.project_selection, vec![main]);
|
||||
assert!(s.drain_events().iter().any(|e| matches!(e, Event::RevealInProject(i) if *i == main)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_in_and_out_in_the_source_monitor_mark_a_sequence_like_a_clip() {
|
||||
// marks on a sequence loaded in the Source Monitor used to be dropped silently
|
||||
|
||||
@@ -472,6 +472,48 @@ impl FilmcraftApp {
|
||||
self.ui.focused = p;
|
||||
}
|
||||
|
||||
/// Reveal in Project: bring the Project panel forward, showing the bin that holds `item` with
|
||||
/// the search cleared, so the (already selected) item is on show.
|
||||
fn reveal_in_project(&mut self, item: filmcraft_project::ItemId) {
|
||||
// the bins on the way down to the item (bounded: a project file can nest bins arbitrarily deep)
|
||||
fn path(b: &filmcraft_project::Bin, item: filmcraft_project::ItemId, depth: usize, out: &mut Vec<u64>) -> bool {
|
||||
if depth > 256 {
|
||||
return false;
|
||||
}
|
||||
for c in &b.children {
|
||||
match c {
|
||||
filmcraft_project::BinEntry::Item(i) if *i == item => return true,
|
||||
filmcraft_project::BinEntry::Bin(inner) => {
|
||||
out.push(inner.id.0);
|
||||
if path(inner, item, depth + 1, out) {
|
||||
return true;
|
||||
}
|
||||
out.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
let mut bins = Vec::new();
|
||||
let found = path(&self.session.project.root, item, 0, &mut bins);
|
||||
// the list shows the whole tree: open the bins on the way. Icons and freeform show one bin
|
||||
// at a time: go into the one that holds the item.
|
||||
let list = self.session.prefs.project_panel.view.mode == filmcraft_engine::project_panel::ViewMode::List;
|
||||
self.ui.project_panel.bin = if list || !found { None } else { bins.last().copied() };
|
||||
if list {
|
||||
for b in bins {
|
||||
if !self.ui.expanded_bins.contains(&b) {
|
||||
self.ui.expanded_bins.push(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ui.project_panel.active_tab = None;
|
||||
self.ui.project_panel.selected_bin = None;
|
||||
self.ui.project_search.clear();
|
||||
self.show_panel(PanelKind::Project);
|
||||
}
|
||||
|
||||
pub fn status(&mut self, s: impl Into<String>) {
|
||||
self.ui.status = s.into();
|
||||
}
|
||||
@@ -1032,6 +1074,7 @@ impl FilmcraftApp {
|
||||
filmcraft_engine::Event::OpenSource(_) => {
|
||||
self.ui.dock.activate(PanelKind::Source);
|
||||
}
|
||||
filmcraft_engine::Event::RevealInProject(item) => self.reveal_in_project(item),
|
||||
filmcraft_engine::Event::Toast { message, .. } => self.toast = Some((message, ctx.input(|i| i.time))),
|
||||
filmcraft_engine::Event::ProjectChanged { .. } => {}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,10 @@ pub fn panel_menu_popup(app: &mut FilmcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
if p == PanelKind::Timeline {
|
||||
ui.separator();
|
||||
if ui.add_enabled(app.session.state.active_sequence.is_some(), egui::Button::new("Reveal Sequence in Project")).clicked() {
|
||||
let _ = app.session.execute("sequence.revealInProject", serde_json::json!({}));
|
||||
close = true;
|
||||
}
|
||||
ui.checkbox(&mut app.ui.timeline.show_thumbnails, "Video Thumbnails");
|
||||
ui.checkbox(&mut app.ui.timeline.show_waveforms, "Audio Waveforms");
|
||||
}
|
||||
|
||||
@@ -1439,7 +1439,7 @@ const CLIP_MENU: &[&[(&str, &str)]] = &[
|
||||
("Field Options…", "clip.fieldOptions"),
|
||||
("Scale to Frame Size", "clip.scaleToFrameSize"),
|
||||
],
|
||||
&[("Join Through Edits", "sequence.joinThroughEdits")],
|
||||
&[("Reveal in Project", "clip.revealInProject"), ("Join Through Edits", "sequence.joinThroughEdits")],
|
||||
];
|
||||
|
||||
fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: &Layout, rect: Rect) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Headless UI tests of editing with nested sequences: the nest toggle button.
|
||||
//! Headless UI tests of editing with nested sequences: the nest toggle button and Reveal in
|
||||
//! Project.
|
||||
|
||||
use std::sync::mpsc::{Sender, channel};
|
||||
|
||||
@@ -6,6 +7,7 @@ use egui_kittest::Harness;
|
||||
use filmcraft_engine::Session;
|
||||
use filmcraft_ui_egui::FilmcraftApp;
|
||||
use filmcraft_ui_egui::control::ControlRequest;
|
||||
use filmcraft_ui_egui::dock::PanelKind;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
struct Driver {
|
||||
@@ -54,11 +56,20 @@ impl Driver {
|
||||
v["result"].clone()
|
||||
}
|
||||
|
||||
fn exec(&mut self, command: &str, params: Value) -> Value {
|
||||
self.ok("engine.execute", json!({"command": command, "params": params}))
|
||||
}
|
||||
|
||||
fn click(&mut self, id: &str) {
|
||||
self.ok("ui.click", json!({"id": id}));
|
||||
self.frames(3);
|
||||
}
|
||||
|
||||
fn has(&mut self, id: &str) -> bool {
|
||||
let v = self.ok("ui.elements", json!({"prefix": id}));
|
||||
v.as_array().unwrap().iter().any(|e| e["id"] == json!(id))
|
||||
}
|
||||
|
||||
fn app(&mut self) -> &mut FilmcraftApp {
|
||||
self.harness.state_mut()
|
||||
}
|
||||
@@ -73,3 +84,37 @@ fn the_nest_button_toggles_how_sequences_are_edited_in() {
|
||||
d.click("timeline.toggle.nest");
|
||||
assert!(!d.app().session.state.sequences_as_clips);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_in_project_shows_the_clips_item_in_the_project_panel() {
|
||||
use filmcraft_engine::project_panel::ViewMode;
|
||||
for mode in [ViewMode::List, ViewMode::Icon] {
|
||||
let mut d = Driver::new();
|
||||
d.app().session.prefs.project_panel.view.mode = mode;
|
||||
let (clip, item) = {
|
||||
let it = &d.app().session.active_sequence().unwrap().video_tracks[0].items[2];
|
||||
(it.id.0, it.item)
|
||||
};
|
||||
let bin = d.app().session.project.root.parent_of(item).expect("the demo keeps its footage in a bin").0;
|
||||
// the Project panel is closed, its bins are collapsed and a search hides everything
|
||||
d.app().ui.dock.close(PanelKind::Project);
|
||||
d.app().ui.expanded_bins.clear();
|
||||
d.app().ui.project_search = "no such thing".into();
|
||||
d.frames(2);
|
||||
assert!(!d.has(&format!("project.item.{}", item.0)));
|
||||
d.exec("clip.revealInProject", json!({"clip": clip}));
|
||||
d.frames(3);
|
||||
assert!(d.app().ui.dock.is_visible(PanelKind::Project), "{mode:?}");
|
||||
assert!(d.app().ui.project_search.is_empty());
|
||||
assert_eq!(d.app().session.state.project_selection, vec![item]);
|
||||
if mode == ViewMode::List {
|
||||
// the list shows the tree with the item's bin opened
|
||||
assert!(d.app().ui.expanded_bins.contains(&bin));
|
||||
assert_eq!(d.app().ui.project_panel.bin, None);
|
||||
} else {
|
||||
// icons show one bin at a time: the panel is inside the item's bin
|
||||
assert_eq!(d.app().ui.project_panel.bin, Some(bin));
|
||||
}
|
||||
assert!(d.has(&format!("project.item.{}", item.0)), "{mode:?}: the item is on show");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user