Repository navigation
timeline: Track Select Forward/Backward selects and moves the captions with the clips (#3388) #3407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -349,7 +349,9 @@ pub(crate) fn item_p(p: &Value, k: &str) -> Option<ItemId> { | |
| /// before and after (ticks) and the clips what is left of it now lives on as (none = removed; a | ||
| /// clip a moved clip lands inside is split in two). With `copy: true` (Option-drag) the clips stay | ||
| /// where they are and copies land at the destinations instead; the copies are selected and listed | ||
| /// as `copied`. | ||
| /// as `copied`. `captions` (caption ids) move with them by the same offset in the same undo step; | ||
| /// if they cannot go that far (they would overlap captions that stay, or sit on a locked track) | ||
| /// nothing moves. Not with `copy`. | ||
| fn move_clips(s: &mut Session, p: &Value) -> Result<Value> { | ||
| const CMD: &str = "timeline.move"; | ||
| let mut listed: Vec<(ClipId, TrackId, Tick, bool)> = Vec::new(); | ||
|
|
@@ -395,6 +397,22 @@ fn move_clips(s: &mut Session, p: &Value) -> Result<Value> { | |
| } | ||
| } | ||
| let late = plan.iter().map(|m| -m.2).max().unwrap_or(0).max(0); | ||
| // `captions` ride along by the first listed clip's offset (shift above included), so subtitles | ||
| // stay on the dialogue they belong to | ||
| let mut caption_ids: Vec<ClipId> = | ||
| p.get("captions").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_u64().map(ClipId)).collect()).unwrap_or_default(); | ||
| let mut seen = std::collections::HashSet::new(); | ||
| caption_ids.retain(|c| seen.insert(*c)); | ||
| if !caption_ids.is_empty() && bool_p(p, "copy").unwrap_or(false) { | ||
| return Err(bad(CMD, "`captions` cannot be combined with `copy`")); | ||
| } | ||
| let caption_delta = match plan.first() { | ||
| Some(&(clip, _, start, ..)) if !caption_ids.is_empty() => { | ||
| let from = q.find_item(clip).map(|(_, it)| i128::from(it.start.0)).ok_or(edit::EditError::NoItem(clip))?; | ||
| Tick(i64::try_from(start + late - from).map_err(|_| bad(CMD, "`time` is out of range"))?) | ||
| } | ||
| _ => Tick::ZERO, | ||
| }; | ||
| let mut moves: Vec<(ClipId, TrackId, Tick, bool)> = Vec::with_capacity(plan.len()); | ||
| for (clip, track, start, was, new_track) in plan { | ||
| // a partner that ends up where it is does not move (its listed clip only changed track): | ||
|
|
@@ -454,7 +472,12 @@ fn move_clips(s: &mut Session, p: &Value) -> Result<Value> { | |
| } else { | ||
| s.edit_sequence(if ins { "Move (Insert)" } else { "Move" }, |q, ctx, _| { | ||
| let destinations = prepare(q, ctx)?; | ||
| Ok(edit::move_items(q, &destinations, ins, ctx)?) | ||
| edit::move_items(q, &destinations, ins, ctx)?; | ||
| // all or nothing: captions that cannot go as far as the clips would drift off their dialogue | ||
| if caption_delta != Tick::ZERO && edit::captions::move_captions(q, &caption_ids, caption_delta)? != caption_delta { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate caption IDs for zero-offset moves. If a clip changes track without changing time,
|
||
| return Err(bad(CMD, "the captions cannot move as far as the clips (they would overlap other captions)")); | ||
| } | ||
| Ok(()) | ||
| })?; | ||
| Vec::new() | ||
| }; | ||
|
|
@@ -478,7 +501,11 @@ fn move_clips(s: &mut Session, p: &Value) -> Result<Value> { | |
| if copy { | ||
| return Ok(json!({"copied": copied.iter().map(|c| c.0).collect::<Vec<_>>(), "overwritten": overwritten})); | ||
| } | ||
| Ok(json!({"moved": moves.iter().map(|m| m.0.0).collect::<Vec<_>>(), "overwritten": overwritten})) | ||
| let mut result = json!({"moved": moves.iter().map(|m| m.0.0).collect::<Vec<_>>(), "overwritten": overwritten}); | ||
| if !caption_ids.is_empty() { | ||
| result["captions"] = json!(caption_ids.iter().map(|c| c.0).collect::<Vec<_>>()); | ||
| } | ||
| Ok(result) | ||
| } | ||
|
|
||
| /// Slip clips together: the media delta of each clip is scaled down by the same factor so that the | ||
|
|
@@ -2784,7 +2811,7 @@ fn build() -> Vec<CommandSpec> { | |
| "Move Clips", | ||
| [], | ||
| None, | ||
| r#"{"moves":[{"clip":id,"track":id|"V2"|"new" (after the last of its kind),"time":ticks}],"insert":bool,"linked":bool?,"copy":bool?}"#, | ||
| r#"{"moves":[{"clip":id,"track":id|"V2"|"new" (after the last of its kind),"time":ticks}],"insert":bool,"linked":bool?,"copy":bool?,"captions":[id]?}"#, | ||
| has_seq, | ||
| move_clips | ||
| ), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -395,6 +395,19 @@ pub(super) fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence | |
| .map(|i| i.id.0) | ||
| .collect(); | ||
| let _ = app.session.execute("timeline.select", json!({"clips": ids})); | ||
| // the captions on the same side go with the clips so the subtitles stay in sync; | ||
| // Shift keeps to the clicked track, and a locked caption track stays put | ||
| if !mods.shift { | ||
| let captions: Vec<u64> = seq | ||
| .caption_tracks | ||
| .iter() | ||
| .filter(|tr| !tr.locked) | ||
| .flat_map(|tr| tr.captions.iter()) | ||
| .filter(|c| if fwd { c.end() > t } else { c.start < t }) | ||
| .map(|c| c.id.0) | ||
| .collect(); | ||
| let _ = app.session.execute("captions.select", json!({"captions": captions, "add": true})); | ||
| } | ||
| // as in Premiere Pro, a drag with the tool moves what it just selected | ||
| if resp.drag_started() { | ||
| let clips = app.session.state.selection.clone(); | ||
|
|
@@ -707,7 +720,14 @@ pub(super) fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence | |
| .filter_map(|c| seq.find_item(*c).map(|(tid, it)| json!({"clip": c.0, "track": to(tid), "time": (it.start + offset).max(Tick::ZERO).0}))) | ||
| .collect(); | ||
| // Option held while dropping copies (Premiere: Option-drag duplicates) | ||
| Some(app.session.execute("timeline.move", json!({"moves": moves, "insert": mods.command, "linked": false, "copy": copy || mods.alt}))) | ||
| let copying = copy || mods.alt; | ||
| // the captions Track Select picked up move with the clips, in the same undo step | ||
| let captions: Vec<u64> = if matches!(tool, Tool::TrackSelectForward | Tool::TrackSelectBackward) && !copying { | ||
| app.session.state.caption_selection.iter().map(|c| c.0).collect() | ||
| } else { | ||
| Vec::new() | ||
| }; | ||
| Some(app.session.execute("timeline.move", json!({"moves": moves, "insert": mods.command, "linked": false, "copy": copying, "captions": captions}))) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent a second caption shift during Track Select insert moves. If the user Cmd-drags with Track Select on a sync-locked caption track,
|
||
| } | ||
| Drag::Trim { clip, edge, mode, delta, linked, .. } if delta != Tick::ZERO => Some(app.session.execute( | ||
| "timeline.trim", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔎 Supported by static analysis
Script executed:
Repository: storytold/filmcraft
Length of output: 42426
Script executed:
Repository: storytold/filmcraft
Length of output: 42345
Script executed:
Repository: storytold/filmcraft
Length of output: 41530
Script executed:
Repository: storytold/filmcraft
Length of output: 8484
Script executed:
Repository: storytold/filmcraft
Length of output: 3417
Script executed:
Repository: storytold/filmcraft
Length of output: 3431
Script executed:
Repository: storytold/filmcraft
Length of output: 2194
Reject malformed
captionsentries before moving clips.filter_mapsilently removes invalid entries. A mixed array can move the clips while omitting some requested captions. Reject a present non-arraycaptionsvalue or any non-numeric array entry. Keep an absent field as an empty list.Suggested fix