diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index 32eebbd7..5582902d 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -1308,7 +1308,7 @@ impl App { && (self.scenario_context() != Some(captured) || self.selected != *id || !self.project().contains(*id) - || behavior::selected_behavior(self.project(), *id) != Some(captured.scenario) + || !behavior::scenario::properties_target(self.project(), captured, *id) || self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() diff --git a/crates/oa-desktop/src/behavior/scenario.rs b/crates/oa-desktop/src/behavior/scenario.rs index cea3a738..11c525f7 100644 --- a/crates/oa-desktop/src/behavior/scenario.rs +++ b/crates/oa-desktop/src/behavior/scenario.rs @@ -18,6 +18,14 @@ mod workspace; pub(crate) use workspace::NAME_INPUT; pub use workspace::{Shelf, view}; +// A picker may select the Saved diagram record itself. Another view of the +// same Scenario is not the property target of this displayed representation. +pub(crate) fn properties_target(p: &Project, context: Context, id: Id) -> bool { + super::selected_behavior(p, id) == Some(context.scenario) + || matches!(context.representation, Representation::Saved { diagram } if diagram == id) + && presentation::checked_owner(p, context.representation) == Some(context.scenario) +} + #[cfg(test)] #[path = "scenario_operation_tests.rs"] mod operation_tests; diff --git a/crates/oa-desktop/src/behavior/scenario/workspace.rs b/crates/oa-desktop/src/behavior/scenario/workspace.rs index f3db8696..ee77e17e 100644 --- a/crates/oa-desktop/src/behavior/scenario/workspace.rs +++ b/crates/oa-desktop/src/behavior/scenario/workspace.rs @@ -372,7 +372,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a let mut details = column![ text(format!("Selected: {}", p.named(app.selected))).size(12), container(button(text("Properties").size(12)).on_press_maybe( - (clean && super::super::selected_behavior(p, app.selected) == Some(owner)).then_some( + (clean && properties_target(p, context, app.selected)).then_some( Message::ScenarioCanvas(CanvasEvent::Properties(context, app.selected)), ), )) diff --git a/crates/oa-desktop/src/behavior/scenario/workspace/details_tests.rs b/crates/oa-desktop/src/behavior/scenario/workspace/details_tests.rs index 1d289e59..4189f1db 100644 --- a/crates/oa-desktop/src/behavior/scenario/workspace/details_tests.rs +++ b/crates/oa-desktop/src/behavior/scenario/workspace/details_tests.rs @@ -227,6 +227,111 @@ fn details_properties_refuses_pending_property_source_and_behavior_drafts_withou } } +#[tokio::test] +async fn focused_details_opens_current_saved_view_properties_after_following_diagram_picker() { + let (mut app, a, _) = setup(); + let b = id(&app, "incident_wide"); + // Selecting the current view record makes the real picker follow that + // record selection to B. Do not seed selected=B or its property draft. + let _ = app.update(Message::Select(a)); + assert_eq!(app.selected, a); + let _ = app.update(Message::Diagram(crate::app::Choice { + id: b, + label: "B".into(), + })); + assert_eq!(app.selected, b); + assert_eq!(app.draft.as_ref().unwrap().original, b); + assert_eq!( + app.scenario_context().unwrap().representation, + Representation::Saved { diagram: b } + ); + let _ = app.update(Message::InspectorTab( + crate::navigation::InspectorTab::Connections, + )); + let mut mounted = Mounted::new(&mut app).await; + open_focused_details(&mut app); + mounted.settle(&mut app).await; + let context = app.scenario_context().unwrap(); + let camera = stored_camera(&app); + let snapshot = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + let before = mounted.geometry(&app); + assert!(!before.scrolls.contains_key(&widget::Id::new("inspector"))); + let (details, offset) = before.scrolls[&widget::Id::new("scenario-details")]; + let point = before.bounds("scenario-properties").center() - offset; + assert!(details.contains(point)); + let events = mounted.click(&mut app, point).await; + assert!(events.iter().any(|event| matches!(event, + Message::ScenarioCanvas(CanvasEvent::Properties(c, selected)) + if *c == context && *selected == b))); + mounted.settle(&mut app).await; + let shown = mounted.geometry(&app); + assert!( + !shown + .scrolls + .contains_key(&widget::Id::new("scenario-details")) + ); + let (inspector, offset) = shown.scrolls[&widget::Id::new("inspector")]; + assert!( + shown + .text_inputs + .iter() + .any(|bounds| inspector.contains(bounds.center() - offset)), + "current Saved diagram has a visible editable inspector" + ); + assert!(!app.behavior.scenario.focused()); + assert_eq!( + app.inspector_tab, + crate::navigation::InspectorTab::Properties + ); + assert_eq!(app.selected, b); + assert_eq!(app.draft.as_ref().unwrap().original, b); + assert_eq!(app.active_diagram, Some(b)); + assert_eq!(app.page, crate::app::Page::Behavior); + assert_eq!( + app.scenario_context().unwrap().representation, + context.representation + ); + assert_eq!(app.scenario_context().unwrap().visit, context.visit); + assert_eq!(stored_camera(&app), camera); + assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[test] +fn details_properties_refuses_another_saved_view_even_with_the_same_scenario_owner() { + let (mut app, a, _) = setup(); + let b = id(&app, "incident_wide"); + let _ = app.update(Message::Diagram(crate::app::Choice { + id: b, + label: "B".into(), + })); + let _ = app.update(Message::Select(a)); + assert_eq!(app.selected, a); + open_focused_details(&mut app); + let context = app.scenario_context().unwrap(); + assert_eq!(context.representation, Representation::Saved { diagram: b }); + assert_eq!( + presentation::checked_owner(app.project(), Representation::Saved { diagram: a }), + Some(context.scenario) + ); + assert!(!properties_target(app.project(), context, a)); + let camera = app.behavior.scenario.camera_boundary(); + let epochs = (app.canvas_epoch, app.camera_epoch); + let snapshot = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Properties(context, a))); + assert!(app.behavior.scenario.camera_boundary() == camera); + assert_eq!((app.canvas_epoch, app.camera_epoch), epochs); + assert_eq!(app.scenario_context(), Some(context)); + assert_eq!(app.selected, a); + assert_eq!(app.draft.as_ref().unwrap().original, a); + assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + #[test] fn details_properties_rejects_retired_view_selection_and_epoch_without_changing_panels() { for kind in 0..3 { @@ -294,6 +399,34 @@ fn opening_scenario_properties_cancels_held_spacing_and_rejects_its_late_finish( let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(begin))); assert_eq!(app.behavior.scenario.spacing_token(), Some(begin.token())); let snapshot = app.document.snapshot(); + let end = press_screen + Vector::new(0., 12.); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing( + Spacing::Move { + stamp: begin.stamp(), + token: begin.token(), + screen: end, + }, + ))); + let paint = app + .behavior + .scenario + .spacing_paint(begin.stamp().context, begin.stamp().camera_epoch) + .unwrap(); + assert!(paint.refusal.is_none()); + let line = paint + .scene + .lines + .iter() + .find(|line| line.id == message) + .unwrap(); + assert!( + line.points[0].y > 160., + "a real proposed scene moved the first message" + ); + assert!( + Arc::ptr_eq(&snapshot, &app.document.snapshot()), + "preview is not authored" + ); let camera = stored_camera(&app); send(&mut app, |c| CanvasEvent::Properties(c, message)); assert!(app.behavior.scenario.spacing_token().is_none()); @@ -302,7 +435,7 @@ fn opening_scenario_properties_cancels_held_spacing_and_rejects_its_late_finish( Spacing::Finish { stamp: begin.stamp(), token: begin.token(), - screen: press_screen + Vector::new(0., 40.), + screen: end, }, ))); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));