Merge pull request #81 from storytold/fix/44-project-import

Fix #44: double-click the Project panel's empty area to import
This commit is contained in:
BFlat authored and GitHub committed 2026-10-06 00:37:36 +07:00
commit dd56ae5c00
2 files changed
+51

No files matched your search

+7
View File
@@ -303,6 +303,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let footer_h = 28.0;
let list = Rect::from_min_max(pos2(rect.min.x, hdr.max.y), pos2(rect.max.x, rect.max.y - footer_h));
let lp = p.with_clip_rect(list);
// The list's empty area (under the rows and the scroll bar, which take their own clicks):
// a double-click imports, as in After Effects (File ▸ Import ▸ File...).
let empty = ui.interact(list, egui::Id::new("proj-empty"), Sense::click());
app.auto.add("project.empty", list, "Double-click to import files");
if overflow > 0.0 && ui.rect_contains_pointer(list) {
let (dx, dy, shift) = ui.input(|i| (i.smooth_scroll_delta.x, i.smooth_scroll_delta.y, i.modifiers.shift));
let d = if dx.abs() > 0.0 {
@@ -686,6 +690,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
actions.push(("project.delete".into(), json!({})));
}
app.auto.add("project.delete", tr, "Delete");
if empty.double_clicked() {
actions.push(("file.import".into(), json!({})));
}
for (id, params) in actions {
if let Err(e) = crate::menus::invoke(app, &ctx, &id, params) {
app.ui.status = e;
+44
View File
@@ -219,3 +219,47 @@ fn project_details_stay_inside_the_panel() {
let r = rect(&h, "project.details.hint");
assert!(r.min.x >= panel.min.x + 9.0 && r.max.x <= panel.max.x - 9.0, "{r:?} in {panel:?}");
}
/// A file dropped on the window (what the windowing layer hands egui).
#[derive(Debug)]
struct Dropped(std::path::PathBuf);
impl egui::DroppedFile for Dropped {
fn path(&self) -> &std::path::Path {
&self.0
}
fn bytes(&self) -> Result<Vec<u8>, String> {
std::fs::read(&self.0).map_err(|e| e.to_string())
}
}
/// Issue #44: double-clicking the Project panel's empty area asks for files to import (as After
/// Effects' File ▸ Import ▸ File... does), and files dropped on the window are imported. (Tests
/// have no media decoders: what is checked is that the import runs with those files.)
#[test]
fn project_empty_area_double_click_and_dropped_files_import() {
let (mut h, _, _) = harness();
let png = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/app-icon/hicolor/16x16/apps/ai.storyteller.effectcraft.png");
let png = std::fs::canonicalize(png).unwrap().to_string_lossy().to_string();
let asked = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let (a, p) = (asked.clone(), png.clone());
h.state_mut().hooks.pick_files = Some(Box::new(move |_| {
a.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
vec![p.clone()]
}));
let imports = |h: &Harness<'_, EffectcraftApp>| h.state().session.journal.iter().filter(|(c, p)| c == "file.import" && p["paths"] == json!([png])).count();
// Below the last row.
let last = rect(&h, "project.item.2");
let empty = rect(&h, "project.empty");
click_n(&mut h, pos2(empty.center().x, (last.max.y + empty.max.y) / 2.0), 2);
assert_eq!(asked.load(std::sync::atomic::Ordering::Relaxed), 1, "the file dialog opened");
assert_eq!(imports(&h), 1, "the picked file was imported");
// A double-click on a row opens that item instead.
let row = rect(&h, "project.item.2.name");
click_n(&mut h, row.center(), 2);
assert_eq!(asked.load(std::sync::atomic::Ordering::Relaxed), 1);
// Dropping a file on the window imports it.
h.input_mut().dropped_files.push(std::sync::Arc::new(Dropped(png.clone().into())));
h.run_steps(2);
assert_eq!(imports(&h), 2, "the dropped file was imported");
}