Merge pull request #1240 from Ni-zav/fix/1163-alt-marquee-select-unlinked-s20261012T0220Z-a3

timeline: Alt marquee selects clips without linked partners (#1163)
This commit is contained in:
Brandon Thomas committed 2026-10-13 02:34:33 -04:00
commit bdc10001fe
2 files changed
+36 -6

No files matched your search

+9 -6
View File
@@ -168,6 +168,7 @@ pub enum Drag {
start: Pos2,
before: Vec<ClipId>,
add: bool,
individual: bool,
},
Divider,
ZoomBar {
@@ -2438,7 +2439,7 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: &
// the video and audio tracks, #683)
(_, Grab::Other(hit @ (Hit::Empty { .. } | Hit::None))) if layout.content.contains(p) => {
if resp.drag_started() {
Some(Drag::Marquee { start: p, before: app.session.state.selection.clone(), add: mods.shift || mods.command })
Some(Drag::Marquee { start: p, before: app.session.state.selection.clone(), add: mods.shift || mods.command, individual: mods.alt })
} else {
app.session.state.selection.clear();
app.session.state.edit_points.clear();
@@ -2581,20 +2582,22 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: &
Some(Drag::Transition { id, edge, from, start, duration })
}
Drag::Volume(v) => Some(Drag::Volume(super::timeline_volume::drag(app, ui, seq, layout, v, p.y))),
Drag::Marquee { start, before, add } => {
Drag::Marquee { start, before, add, individual } => {
let r = Rect::from_two_pos(start, p);
ui.painter().rect_filled(r, 0.0, Color32::from_white_alpha(18));
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, Color32::from_white_alpha(140)), StrokeKind::Inside);
// the clips in the box are selected as it grows and shrinks; only the release runs
// the command, so the drag is one selection change
let mut sel = if add { before.clone() } else { Vec::new() };
for c in filmcraft_engine::commands::with_links(&app.session, &marquee_clips(seq, layout, r)) {
let clips = marquee_clips(seq, layout, r);
let clips = if individual { clips } else { filmcraft_engine::commands::with_links(&app.session, &clips) };
for c in clips {
if !sel.contains(&c) {
sel.push(c);
}
}
app.session.state.selection = sel;
Some(Drag::Marquee { start, before, add })
Some(Drag::Marquee { start, before, add, individual })
}
other => Some(other),
};
@@ -2644,13 +2647,13 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: &
let moved = seq.all_tracks().find_map(|tr| tr.transitions.iter().find(|x| x.id == id)).is_some_and(|x| (x.start, x.duration) != (start, duration));
moved.then(|| app.session.execute("sequence.setTransition", json!({"transition": id.0, "start": start.0, "duration": duration.0})))
}
Drag::Marquee { start, before, add } => {
Drag::Marquee { start, before, add, individual } => {
app.session.state.selection = before;
// egui ends a drag on Escape: that cancels the box, as in the Project panel
match resp.interact_pointer_pos() {
Some(end) if !ctx.input(|i| i.key_pressed(egui::Key::Escape)) => {
let ids: Vec<u64> = marquee_clips(seq, layout, Rect::from_two_pos(start, end)).iter().map(|c| c.0).collect();
Some(app.session.execute("timeline.select", json!({"clips": ids, "add": add})))
Some(app.session.execute("timeline.select", json!({"clips": ids, "add": add, "linked": !individual})))
}
_ => None,
}
@@ -121,6 +121,33 @@ fn the_selection_follows_the_box_while_it_is_dragged() {
assert!(d.selected(b) && !d.selected(a), "the release keeps what the box showed");
}
#[test]
fn alt_box_selects_only_video_and_delete_keeps_linked_audio() {
let mut d = Driver::demo();
let (_, video, _, rv, start) = d.setup();
let audio = {
let seq = d.harness.state().session.active_sequence().unwrap();
let link = seq.find_item(ClipId(video)).unwrap().1.link;
seq.audio_tracks.iter().flat_map(|tr| &tr.items).find(|clip| clip.link == link).unwrap().id.0
};
d.exec("timeline.select", json!({"clips": []}));
d.modifiers = egui::Modifiers::ALT;
d.send(egui::Event::PointerMoved(start));
d.button(start, true);
let over = egui::pos2(rv.center().x, start.y + 2.0);
d.move_to(start, over);
assert!(d.selected(video));
assert!(!d.selected(audio), "Alt excludes the linked audio during the drag");
d.button(over, false);
d.modifiers = egui::Modifiers::NONE;
d.frames(2);
assert!(d.selected(video) && !d.selected(audio), "release preserves the individual selection");
d.exec("edit.clear", json!({}));
let seq = d.harness.state().session.active_sequence().unwrap();
assert!(seq.find_item(ClipId(video)).is_none());
assert!(seq.find_item(ClipId(audio)).is_some(), "Delete leaves the unselected linked audio");
}
#[test]
fn shift_adds_to_the_selection_the_drag_began_with() {
let mut d = Driver::demo();