mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #634 from Ni-zav/fix/project-multiselect-drag-351-s20261010T1753Z-f4c2
This commit is contained in:
4 files changed
+85
-12
No files matched your search
@@ -78,6 +78,30 @@ pub enum DragPayload {
|
||||
Files(Vec<String>),
|
||||
}
|
||||
|
||||
/// Layers carried by a Project item drag. Dragging a selected item uses the
|
||||
/// complete selection, while dragging an unselected item uses only that item.
|
||||
/// Folders and data files are not composition layers.
|
||||
pub(crate) fn project_drop_items(app: &EffectcraftApp, dragged: u64) -> Vec<u64> {
|
||||
use effectcraft_engine::project::{FootageKind, ItemId, ItemKind};
|
||||
|
||||
let selected = &app.session.state.project_selection;
|
||||
let candidates: Vec<u64> = if selected.iter().any(|id| id.0 == dragged) {
|
||||
selected.iter().map(|id| id.0).collect()
|
||||
} else {
|
||||
vec![dragged]
|
||||
};
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|id| {
|
||||
app.session.project.item(ItemId(*id)).is_some_and(|item| match &item.kind {
|
||||
ItemKind::Comp(_) | ItemKind::Solid(_) => true,
|
||||
ItemKind::Footage(f) => f.kind != FootageKind::Data,
|
||||
ItemKind::Folder => false,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The current time formatted per project settings (timecode with `;` for drop-frame, frames
|
||||
/// or Feet + Frames).
|
||||
pub fn timecode(session: &Session, comp: &Comp, t: Tick) -> String {
|
||||
|
||||
@@ -2959,9 +2959,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
rp.galley(tag.min, g, Color32::WHITE);
|
||||
}
|
||||
if released {
|
||||
let (index, time) = (json!(index), json!(start.map(Tick::seconds)));
|
||||
let time = json!(start.map(Tick::seconds));
|
||||
match payload.as_ref() {
|
||||
crate::panels::DragPayload::Item(id) => actions.push(("layer.addItem".into(), json!({"item": id, "index": index, "time": time}))),
|
||||
crate::panels::DragPayload::Item(id) => {
|
||||
// Insert each selected item at the drop point, in selection order.
|
||||
for (offset, item) in crate::panels::project_drop_items(app, *id).into_iter().enumerate() {
|
||||
actions.push(("layer.addItem".into(), json!({"item": item, "index": index + offset, "time": time.clone()})));
|
||||
}
|
||||
}
|
||||
crate::panels::DragPayload::Files(paths) => {
|
||||
actions.push(("mediaBrowser.import".into(), json!({"paths": paths, "addToComp": true, "index": index, "time": time})))
|
||||
}
|
||||
|
||||
@@ -23,32 +23,38 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &egui::Ui, painter: &egui::Pain
|
||||
let Some(ptr) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| ui.rect_contains_pointer(map.area)) else { return };
|
||||
let stroke = Stroke::new(2.0, app.tokens.accent);
|
||||
let at = map.to_comp(ptr);
|
||||
let action: Option<(&str, Value)> = match payload.as_ref() {
|
||||
let actions: Vec<(&str, Value)> = match payload.as_ref() {
|
||||
DragPayload::Effect(effect) => layer_at(ectx, at).map(|l| {
|
||||
if let Some((_, q, _)) = layer_quad(ectx, l) {
|
||||
painter.add(egui::Shape::closed_line(q.iter().map(|c| map.to_screen(*c)).collect(), stroke));
|
||||
}
|
||||
("effect.apply", json!({"effect": effect, "layers": [l.id.0]}))
|
||||
}),
|
||||
}).into_iter().collect(),
|
||||
DragPayload::Item(item) => {
|
||||
// The layer's frame, centred on the pointer.
|
||||
if let Some([w, h]) = app.session.project.item(ItemId(*item)).and_then(|it| frame_size(&it.kind, ectx.comp.pixel_aspect)) {
|
||||
painter.rect_stroke(Rect::from_center_size(ptr, vec2(w, h) * map.zoom), 0.0, Stroke::new(1.0, app.tokens.accent), StrokeKind::Middle);
|
||||
}
|
||||
Some(("layer.addItem", json!({"item": item, "position": at})))
|
||||
// New layers go above the current selection, so insert in reverse
|
||||
// to preserve the order of the dragged Project items.
|
||||
crate::panels::project_drop_items(app, *item)
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|item| ("layer.addItem", json!({"item": item, "position": at})))
|
||||
.collect()
|
||||
}
|
||||
DragPayload::Files(paths) => Some(("mediaBrowser.import", json!({"paths": paths, "addToComp": true, "position": at}))),
|
||||
DragPayload::Property { .. } => None,
|
||||
DragPayload::Files(paths) => vec![("mediaBrowser.import", json!({"paths": paths, "addToComp": true, "position": at}))],
|
||||
DragPayload::Property { .. } => vec![],
|
||||
};
|
||||
if action.as_ref().is_some_and(|(id, _)| *id != "effect.apply") {
|
||||
if actions.first().is_some_and(|(id, _)| *id != "effect.apply") {
|
||||
painter.rect_stroke(map.area, 0.0, stroke, StrokeKind::Inside);
|
||||
}
|
||||
if ctx.input(|i| i.pointer.any_released()) {
|
||||
egui::DragAndDrop::clear_payload(&ctx);
|
||||
if let Some((id, params)) = action
|
||||
&& let Err(e) = crate::menus::invoke(app, &ctx, id, params)
|
||||
{
|
||||
app.ui.status = e;
|
||||
for (id, params) in actions {
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, id, params) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,44 @@ fn project_items_dropped_on_the_viewer_land_under_the_pointer() {
|
||||
assert!((p[0] - 80.0).abs() <= tol && (p[1] - 45.0).abs() <= tol, "{p:?}");
|
||||
}
|
||||
|
||||
/// #351: dragging one of several selected Project items inserts all the items,
|
||||
/// in selection order, instead of only the item the drag started on.
|
||||
#[test]
|
||||
fn dragging_selected_project_items_adds_every_item_to_the_timeline() {
|
||||
let (mut h, clip) = harness();
|
||||
let main = h.state().session.active_comp_id().unwrap().0;
|
||||
let other = h.state_mut().session.execute("comp.new", json!({"name": "Other", "width": 80, "height": 60, "frameRate": 30, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
||||
h.state_mut().session.execute("comp.open", json!({"comp": main})).unwrap();
|
||||
h.state_mut().session.state.project_selection =
|
||||
vec![effectcraft_engine::project::ItemId(clip), effectcraft_engine::project::ItemId(other)];
|
||||
h.run_steps(3);
|
||||
let from = rect(&h, &format!("project.item.{clip}.name")).center();
|
||||
let top = rect(&h, &format!("timeline.layer.{}.row", layer_id(&h, "Top")));
|
||||
drag(&mut h, from, pos2(top.center().x, top.max.y - 2.0), Modifiers::NONE);
|
||||
assert_eq!(stack(&h), ["Top", "Clip", "Other", "Middle", "Bottom"]);
|
||||
assert_eq!(in_point(&h, 1), in_point(&h, 2), "both layers use the drop time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_selected_project_items_adds_every_item_to_the_viewer() {
|
||||
let (mut h, clip) = harness();
|
||||
let main = h.state().session.active_comp_id().unwrap().0;
|
||||
let other = h.state_mut().session.execute("comp.new", json!({"name": "Other", "width": 80, "height": 60, "frameRate": 30, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
||||
h.state_mut().session.execute("comp.open", json!({"comp": main})).unwrap();
|
||||
h.state_mut().session.state.project_selection =
|
||||
vec![effectcraft_engine::project::ItemId(clip), effectcraft_engine::project::ItemId(other)];
|
||||
h.run_steps(3);
|
||||
let from = rect(&h, &format!("project.item.{clip}.name")).center();
|
||||
let to = comp_to_screen(&h.ctx, [80.0, 45.0]).unwrap();
|
||||
drag(&mut h, from, to, Modifiers::NONE);
|
||||
assert_eq!(stack(&h), ["Clip", "Other", "Top", "Middle", "Bottom"]);
|
||||
for layer in &h.state().session.active_comp().unwrap().layers[..2] {
|
||||
let Some(KV::Vec3(p)) = layer.props.prop("transform/position").map(|p| p.value.clone()) else { panic!("no position") };
|
||||
let tol = 1.0 / last_fit(&h.ctx) as f64 + 1e-6;
|
||||
assert!((p[0] - 80.0).abs() <= tol && (p[1] - 45.0).abs() <= tol, "{p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #88: an effect dragged from Effects & Presets onto the viewer goes on the layer under the
|
||||
/// pointer, not the selected one.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user