Merge pull request #210 from jonassneu/fix/blank-panel-context-menus
G2: add context menus to blank Project and Timeline areas
This commit is contained in:
commit
d60cb71e29
|
|
@ -327,7 +327,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
// a click deselects, a drag draws a selection box, a double-click imports, as in After
|
||||
// Effects (File ▸ Import ▸ File...).
|
||||
let empty = ui.interact(list, egui::Id::new("proj-empty"), Sense::click_and_drag());
|
||||
app.auto.add("project.empty", list, "Double-click to import files; drag to select items");
|
||||
app.auto.add("project.empty", list, "Double-click to import files; drag to select items; right-click for project actions");
|
||||
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 {
|
||||
|
|
@ -721,6 +721,18 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if empty.double_clicked() {
|
||||
actions.push(("file.import".into(), json!({})));
|
||||
}
|
||||
empty.context_menu(|ui| {
|
||||
for (key, label, command) in
|
||||
[("newComp", "New Composition…", "comp.new"), ("newFolder", "New Folder", "project.newFolder"), ("import", "Import File…", "file.import")]
|
||||
{
|
||||
let response = ui.button(label);
|
||||
app.auto.add(&format!("project.context.{key}"), response.rect, label);
|
||||
if response.clicked() {
|
||||
actions.push((command.into(), json!({})));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Selection box over the rows it touches (#203); Shift or Cmd/Ctrl adds to the selection.
|
||||
let adding = ui.input(|i| i.modifiers.shift || i.modifiers.command);
|
||||
if empty.clicked() && !adding {
|
||||
|
|
|
|||
|
|
@ -2352,6 +2352,39 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
ui.set_clip_rect(full_clip);
|
||||
// The background responses sit below rows, bars and editors, preserving their menus.
|
||||
if !graph_on {
|
||||
app.auto.add("timeline.empty", empty_rect, "Right-click to create a layer");
|
||||
}
|
||||
for (response, area) in [(&outline_empty, "outline"), (&empty, "graph")] {
|
||||
response.context_menu(|ui| {
|
||||
let menu = ui.menu_button("New", |ui| {
|
||||
for (key, label, command, params) in [
|
||||
("text", "Text", "layer.newText", json!({"text": "Text"})),
|
||||
("solid", "Solid…", "layer.newSolid", json!({})),
|
||||
("light", "Light…", "layer.newLight", json!({})),
|
||||
("camera", "Camera…", "layer.newCamera", json!({})),
|
||||
("null", "Null Object", "layer.newNull", json!({})),
|
||||
("shape", "Shape Layer", "layer.newShape", json!({"kind": "none"})),
|
||||
("adjustment", "Adjustment Layer", "layer.newAdjustment", json!({})),
|
||||
] {
|
||||
let response = ui.button(label);
|
||||
app.auto.add(&format!("timeline.context.{area}.{key}"), response.rect, label);
|
||||
if response.clicked() {
|
||||
actions.push((command.into(), params));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.auto.add(&format!("timeline.context.{area}.new"), menu.response.rect, "New");
|
||||
let response = ui.button("Composition Settings…");
|
||||
app.auto.add(&format!("timeline.context.{area}.settings"), response.rect, "Composition Settings…");
|
||||
if response.clicked() {
|
||||
actions.push(("comp.settings".into(), json!({})));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
if outline_empty.clicked() && !ui.input(|i| i.modifiers.shift || i.modifiers.command) {
|
||||
actions.push(("layer.select".into(), json!({"layers": []})));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
//! Right-click actions in the blank Project and Timeline areas.
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2};
|
||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> Harness<'static, EffectcraftApp> {
|
||||
let mut session = Session::default();
|
||||
session.execute("comp.new", json!({"name":"Empty", "width":640,"height":360,"duration":4})).unwrap();
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(session));
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}"));
|
||||
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
|
||||
}
|
||||
fn snapshot(h: &mut Harness<'_, EffectcraftApp>, name: &str) {
|
||||
if let Ok(dir) = std::env::var("BLANK_CONTEXT_MENU_SNAPSHOTS") {
|
||||
h.render().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||||
}
|
||||
}
|
||||
fn right_click(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_blank_menu_creates_a_folder_and_undo_restores_the_project() {
|
||||
let mut h = harness();
|
||||
let before = h.state().session.project.items.len();
|
||||
let p = rect(&h, "project.empty").center();
|
||||
right_click(&mut h, p);
|
||||
snapshot(&mut h, "project");
|
||||
h.get_by_label("New Folder").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.project.items.len(), before + 1);
|
||||
assert!(h.state().session.project.items.values().any(|i| i.is_folder()));
|
||||
assert!(h.query_by_label("New Folder").is_none());
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(h.state().session.project.items.len(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_blank_menu_opens_composition_settings_and_offers_import() {
|
||||
let mut h = harness();
|
||||
let p = rect(&h, "project.empty").center();
|
||||
right_click(&mut h, p);
|
||||
assert!(h.query_by_label("Import File…").is_some());
|
||||
h.get_by_label("New Composition…").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::NewComp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeline_blank_outline_and_time_graph_create_layers() {
|
||||
for graph in [false, true] {
|
||||
let mut h = harness();
|
||||
let area = rect(&h, "timeline.layerMarquee");
|
||||
let p = if graph { pos2(area.max.x + 150.0, area.center().y) } else { area.center() };
|
||||
right_click(&mut h, p);
|
||||
h.get_by_label("New ⏵").click();
|
||||
h.run_steps(3);
|
||||
snapshot(&mut h, if graph { "timeline-graph" } else { "timeline-outline" });
|
||||
h.get_by_label("Null Object").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.active_comp().unwrap().layers.len(), 1);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(h.state().session.active_comp().unwrap().layers.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeline_blank_menu_opens_solid_settings() {
|
||||
let mut h = harness();
|
||||
let p = rect(&h, "timeline.layerMarquee").center();
|
||||
right_click(&mut h, p);
|
||||
h.get_by_label("New ⏵").click();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Solid…").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::SolidSettings));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_item_and_timeline_layer_keep_their_own_context_menus() {
|
||||
let mut h = harness();
|
||||
let comp = h.state().session.active_comp_id().unwrap();
|
||||
let p = rect(&h, &format!("project.item.{}.name", comp.0)).center();
|
||||
right_click(&mut h, p);
|
||||
assert!(h.query_by_label("Open Composition").is_some());
|
||||
assert!(h.query_by_label("New Folder").is_none());
|
||||
egui::Popup::close_all(&h.ctx);
|
||||
h.state_mut().session.execute("layer.newNull", json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
let id = h.state().session.active_comp().unwrap().layers[0].id;
|
||||
let r = rect(&h, &format!("timeline.layer.{}.row", id.0));
|
||||
right_click(&mut h, pos2(r.min.x + 210.0, r.center().y));
|
||||
assert!(h.query_by_label("Duplicate").is_some());
|
||||
assert!(h.query_by_label("Composition Settings…").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_blank_import_invokes_the_file_picker() {
|
||||
let mut h = harness();
|
||||
let called = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
let picked = called.clone();
|
||||
h.state_mut().hooks.pick_files = Some(Box::new(move |_| {
|
||||
picked.set(true);
|
||||
Vec::new()
|
||||
}));
|
||||
let p = rect(&h, "project.empty").center();
|
||||
right_click(&mut h, p);
|
||||
h.get_by_label("Import File…").click();
|
||||
h.run_steps(3);
|
||||
assert!(called.get());
|
||||
assert!(h.query_by_label("Import File…").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeline_blank_menu_opens_composition_settings() {
|
||||
let mut h = harness();
|
||||
let p = rect(&h, "timeline.layerMarquee").center();
|
||||
right_click(&mut h, p);
|
||||
h.get_by_label("Composition Settings…").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::CompSettings));
|
||||
}
|
||||
Loading…
Reference in New Issue