Wire saved Scenario arrangements through workspace navigation
This commit is contained in:
+214
-28
@@ -10,13 +10,14 @@ mod hosted;
|
||||
mod reference_authoring;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
mod scenario_diagram;
|
||||
pub(crate) mod source_rename;
|
||||
mod source_symbols;
|
||||
#[cfg(test)]
|
||||
mod transition_context_tests;
|
||||
#[cfg(test)]
|
||||
mod view_copy_tests;
|
||||
pub(crate) const SAVED_SCENARIO_UNAVAILABLE: &str = "Saved Scenario layout rendering and canvas editing are not available yet. Select the Scenario in Behavior to use its existing semantic preview.";
|
||||
pub(crate) const SAVED_SCENARIO_UNAVAILABLE: &str = "This action uses architecture diagram tools and is unavailable for a saved Scenario. Open the view in Behavior; saved positions are preserved.";
|
||||
use crate::forms::Draft;
|
||||
use crate::{
|
||||
behavior,
|
||||
@@ -530,6 +531,7 @@ pub struct App {
|
||||
pub(crate) diagram_navigation_focus: Option<crate::diagram_usage::Focus>,
|
||||
source_diagram_job: Option<Id>,
|
||||
diagram_lookup_in_flight: Option<Id>,
|
||||
pending_scenario_reveal_visit: Option<Id>,
|
||||
pending_diagram_reveal: Option<(
|
||||
crate::diagram_usage::Context,
|
||||
crate::diagram_usage::Usage,
|
||||
@@ -651,6 +653,7 @@ impl App {
|
||||
source_diagram_job: None,
|
||||
diagram_lookup_in_flight: None,
|
||||
pending_diagram_reveal: None,
|
||||
pending_scenario_reveal_visit: None,
|
||||
model_issues,
|
||||
migration: None,
|
||||
source_version: 0,
|
||||
@@ -665,6 +668,7 @@ impl App {
|
||||
workflow: None,
|
||||
};
|
||||
app.route_state_diagram_workspace();
|
||||
app.reconcile_scenario_workspace();
|
||||
app.last_workspace_page = app.page;
|
||||
app
|
||||
}
|
||||
@@ -775,6 +779,17 @@ impl App {
|
||||
self.sync_with_alias_editor(None);
|
||||
}
|
||||
fn sync_with_alias_editor(&mut self, hint: Option<source_rename::EditorRefresh<'_>>) {
|
||||
// A saved view may be retargeted across layers by one source transaction.
|
||||
// Follow that explicit identity before generic active-view normalization.
|
||||
if let Some(rep @ behavior::scenario_layout::Representation::Saved { diagram }) =
|
||||
self.behavior.scenario.representation()
|
||||
&& self.active_diagram == Some(diagram)
|
||||
&& let Some(owner) =
|
||||
behavior::scenario_layout::presentation::checked_owner(self.project(), rep)
|
||||
{
|
||||
self.layer = self.project().elements[&owner].layer();
|
||||
self.behavior.active_owner = Some(owner);
|
||||
}
|
||||
self.explorer_index = crate::explorer::Index::new(self.project(), self.layer);
|
||||
self.model_issues = validate(self.project());
|
||||
self.normalize_creation();
|
||||
@@ -879,6 +894,26 @@ impl App {
|
||||
.map_err(|e| e.to_string())
|
||||
}) {
|
||||
Ok(id) => {
|
||||
if self.behavior.scenario.representation()
|
||||
== Some(behavior::scenario_layout::Representation::Saved { diagram: original })
|
||||
&& id != original
|
||||
{
|
||||
let representation =
|
||||
behavior::scenario_layout::Representation::Saved { diagram: id };
|
||||
if let Some(owner) = behavior::scenario_layout::presentation::checked_owner(
|
||||
self.project(),
|
||||
representation,
|
||||
) {
|
||||
self.behavior.scenario.enter_representation(
|
||||
self.canvas_document,
|
||||
owner,
|
||||
representation,
|
||||
);
|
||||
if self.page != Page::Behavior {
|
||||
self.behavior.scenario.leave();
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.active_diagram == Some(original) {
|
||||
self.active_diagram = Some(id);
|
||||
}
|
||||
@@ -1083,6 +1118,17 @@ impl App {
|
||||
&& let Some(owner) = crate::behavior::selected_behavior(self.project(), id)
|
||||
{
|
||||
self.behavior.active_owner = Some(owner);
|
||||
if self.behavior.scenario.representation().is_some_and(|rep| {
|
||||
behavior::scenario_layout::presentation::checked_owner(self.project(), rep)
|
||||
!= Some(owner)
|
||||
}) && self.project().elements[&owner].kind() == Kind::Scenario
|
||||
{
|
||||
self.enter_scenario_representation(
|
||||
behavior::scenario_layout::Representation::Derived { owner },
|
||||
);
|
||||
} else if self.project().elements[&owner].kind() != Kind::Scenario {
|
||||
self.behavior.scenario.forget_view();
|
||||
}
|
||||
}
|
||||
if self.reset_state_view_after_navigation(previous_state_view)
|
||||
&& self.page == Page::Behavior
|
||||
@@ -1232,6 +1278,38 @@ impl App {
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Prior view visits are inert before they can retire a newer reveal,
|
||||
// camera lifetime or focus operation. Passive events may relax epoch,
|
||||
// but never the destination identity.
|
||||
if let Message::ScenarioCanvas(event) = &message {
|
||||
let captured = event.context();
|
||||
if self.scenario_context().is_none_or(|current| {
|
||||
current.document != captured.document
|
||||
|| current.scenario != captured.scenario
|
||||
|| current.representation != captured.representation
|
||||
|| current.visit != captured.visit
|
||||
|| (!matches!(event, behavior::scenario::CanvasEvent::Restored(..))
|
||||
&& current.revision != captured.revision)
|
||||
}) {
|
||||
return Task::none();
|
||||
}
|
||||
if let behavior::scenario::CanvasEvent::Camera(_, lifetime, _) = event
|
||||
&& *lifetime != self.camera_epoch
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if let Message::BehaviorControl(intent) = &message
|
||||
&& behavior::canvas_workspace::Scope::current(self).is_none_or(|scope| {
|
||||
scope.document != intent.scope.document
|
||||
|| scope.owner != intent.scope.owner
|
||||
|| scope.scenario_view != intent.scope.scenario_view
|
||||
|| scope.revision != intent.scope.revision
|
||||
|| scope.epoch != intent.scope.epoch
|
||||
})
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let usage_viewport = self.diagram_reveal_viewport(&message);
|
||||
let window_resized = matches!(&message, Message::WindowResized(_));
|
||||
// Reject stale measurements before legacy viewport handlers can change
|
||||
@@ -1284,6 +1362,7 @@ impl App {
|
||||
let valid_scenario = |c: behavior::scenario::Context| {
|
||||
c.document == scope.document
|
||||
&& c.scenario == scope.owner
|
||||
&& scope.scenario_view == Some((c.representation, c.visit))
|
||||
&& c.revision == scope.revision
|
||||
&& self.camera_toolbar_epoch(c.epoch)
|
||||
};
|
||||
@@ -1667,6 +1746,7 @@ impl App {
|
||||
self.refresh_source_rename_context();
|
||||
self.reconcile_reference_chooser();
|
||||
self.route_state_diagram_workspace();
|
||||
self.reconcile_scenario_workspace();
|
||||
if self.page != Page::Home {
|
||||
self.last_workspace_page = self.page;
|
||||
}
|
||||
@@ -1675,11 +1755,6 @@ impl App {
|
||||
.topology
|
||||
.enter(context.document, context.owner);
|
||||
}
|
||||
if let Some(context) = self.scenario_context() {
|
||||
self.behavior
|
||||
.scenario
|
||||
.enter(context.document, context.scenario);
|
||||
}
|
||||
if self.connection.as_ref().is_some_and(|session| {
|
||||
Some(session.context) != self.route_context()
|
||||
|| self.page != Page::Diagram
|
||||
@@ -1723,15 +1798,23 @@ impl App {
|
||||
}
|
||||
}
|
||||
pub(crate) fn scenario_context(&self) -> Option<crate::behavior::scenario::Context> {
|
||||
let scenario = crate::behavior::selected_owner(self)?;
|
||||
(self.page == Page::Behavior
|
||||
&& self.project().elements.get(&scenario)?.kind() == Kind::Scenario)
|
||||
.then_some(crate::behavior::scenario::Context {
|
||||
document: self.canvas_document,
|
||||
scenario,
|
||||
revision: self.document.revision(),
|
||||
epoch: self.canvas_epoch,
|
||||
})
|
||||
if self.page != Page::Behavior {
|
||||
return None;
|
||||
}
|
||||
let scenario = if matches!(
|
||||
self.behavior.scenario.representation(),
|
||||
Some(behavior::scenario_layout::Representation::Saved { .. })
|
||||
) {
|
||||
self.behavior.scenario.owner()?
|
||||
} else {
|
||||
crate::behavior::selected_owner(self)?
|
||||
};
|
||||
self.behavior.scenario.context(
|
||||
self.canvas_document,
|
||||
scenario,
|
||||
self.document.revision(),
|
||||
self.canvas_epoch,
|
||||
)
|
||||
}
|
||||
pub(crate) fn scenario_focus_owner(&self) -> Option<crate::behavior::scenario::focus::Stamp> {
|
||||
if self.busy
|
||||
@@ -1759,10 +1842,7 @@ impl App {
|
||||
.active_diagram
|
||||
.is_some_and(|id| self.saved_scenario_diagram(id));
|
||||
match message {
|
||||
Message::Diagram(choice) => self.saved_scenario_diagram(choice.id),
|
||||
Message::RevealInDiagram(diagram, _) | Message::Command(Command::Layout(diagram)) => {
|
||||
self.saved_scenario_diagram(*diagram)
|
||||
}
|
||||
Message::Command(Command::Layout(diagram)) => self.saved_scenario_diagram(*diagram),
|
||||
Message::Projection(projection) => active || projection.scenario().is_some(),
|
||||
Message::SelectNode(_)
|
||||
| Message::PlaceSelected
|
||||
@@ -1811,6 +1891,8 @@ impl App {
|
||||
if self.scenario_context().is_some_and(|current| {
|
||||
current.document == context.document
|
||||
&& current.scenario == context.scenario
|
||||
&& current.representation == context.representation
|
||||
&& current.visit == context.visit
|
||||
&& current.revision == context.revision
|
||||
}) && self.scenario_focus_owner().is_some()
|
||||
&& self.behavior.scenario.take_form_focus(mount)
|
||||
@@ -1823,6 +1905,8 @@ impl App {
|
||||
if self.scenario_context().is_some_and(|current| {
|
||||
current.document == context.document
|
||||
&& current.scenario == context.scenario
|
||||
&& current.representation == context.representation
|
||||
&& current.visit == context.visit
|
||||
&& current.revision == context.revision
|
||||
}) && self.behavior.scenario.viewport(size)
|
||||
{
|
||||
@@ -1832,7 +1916,10 @@ impl App {
|
||||
}
|
||||
if let CanvasEvent::Restored(_, token, area, offset) = event {
|
||||
if self.scenario_context().is_some_and(|current| {
|
||||
current.document == context.document && current.scenario == context.scenario
|
||||
current.document == context.document
|
||||
&& current.scenario == context.scenario
|
||||
&& current.representation == context.representation
|
||||
&& current.visit == context.visit
|
||||
}) && self
|
||||
.behavior
|
||||
.scenario
|
||||
@@ -1849,6 +1936,8 @@ impl App {
|
||||
if self.scenario_context().is_some_and(|current| {
|
||||
current.document == context.document
|
||||
&& current.scenario == context.scenario
|
||||
&& current.representation == context.representation
|
||||
&& current.visit == context.visit
|
||||
&& current.revision == context.revision
|
||||
}) && self.behavior.scenario.scrolled(area, offset)
|
||||
{
|
||||
@@ -1862,6 +1951,8 @@ impl App {
|
||||
|| !self.scenario_context().is_some_and(|c| {
|
||||
c.document == context.document
|
||||
&& c.scenario == context.scenario
|
||||
&& c.representation == context.representation
|
||||
&& c.visit == context.visit
|
||||
&& c.revision == context.revision
|
||||
})
|
||||
{
|
||||
@@ -1870,6 +1961,7 @@ impl App {
|
||||
let scope = canvas_workspace::Scope {
|
||||
document: context.document,
|
||||
owner: context.scenario,
|
||||
scenario_view: Some((context.representation, context.visit)),
|
||||
revision: context.revision,
|
||||
epoch: lifetime,
|
||||
};
|
||||
@@ -1892,7 +1984,10 @@ impl App {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1)
|
||||
}
|
||||
input => {
|
||||
if self.behavior.scenario.camera(input) {
|
||||
let mut state = std::mem::take(&mut self.behavior.scenario);
|
||||
let changed = state.camera(self.project(), context.scenario, input);
|
||||
self.behavior.scenario = state;
|
||||
if changed {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
@@ -1903,6 +1998,32 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
match event {
|
||||
CanvasEvent::Representation(_, requested) => {
|
||||
use behavior::scenario_layout::{
|
||||
Representation, presentation::checked_owner,
|
||||
};
|
||||
if checked_owner(self.project(), requested) != Some(context.scenario) {
|
||||
return Task::none();
|
||||
}
|
||||
if !self.apply() {
|
||||
return Task::none();
|
||||
}
|
||||
let requested = match requested {
|
||||
Representation::Saved { diagram }
|
||||
if context.representation
|
||||
== (Representation::Saved { diagram }) =>
|
||||
{
|
||||
Representation::Saved {
|
||||
diagram: self.active_diagram.unwrap_or(diagram),
|
||||
}
|
||||
}
|
||||
Representation::Derived { owner } => Representation::Derived {
|
||||
owner: self.behavior.active_owner.unwrap_or(owner),
|
||||
},
|
||||
other => other,
|
||||
};
|
||||
self.enter_scenario_representation(requested);
|
||||
}
|
||||
CanvasEvent::Focus(_) => {
|
||||
self.behavior.scenario.toggle_focus();
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
@@ -1931,7 +2052,11 @@ impl App {
|
||||
Ok(target) => {
|
||||
// A command can preselect this target while the
|
||||
// caller remains visible, bypassing select below.
|
||||
self.behavior.active_owner = Some(target);
|
||||
self.enter_scenario_representation(
|
||||
behavior::scenario_layout::Representation::Derived {
|
||||
owner: target,
|
||||
},
|
||||
);
|
||||
self.select(target);
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
@@ -1953,7 +2078,7 @@ impl App {
|
||||
// A command may already have selected this UUID while
|
||||
// leaving the referenced definition open. select's
|
||||
// same-UUID fast path must not keep that old owner.
|
||||
self.behavior.active_owner = Some(caller.owner);
|
||||
self.enter_scenario_representation(caller.representation);
|
||||
self.select(caller.occurrence);
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
if let Some(context) = self.scenario_context() {
|
||||
@@ -1981,6 +2106,15 @@ impl App {
|
||||
}
|
||||
}
|
||||
CanvasEvent::Choose(_, tool) => {
|
||||
if tool != scenario::CanvasTool::Select
|
||||
&& !self
|
||||
.behavior
|
||||
.scenario
|
||||
.coordinate_input(self.project(), context)
|
||||
{
|
||||
self.status = "Canvas insertion and reordering are unavailable for this presentation.".into();
|
||||
return Task::none();
|
||||
}
|
||||
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty()
|
||||
{
|
||||
self.status =
|
||||
@@ -1992,6 +2126,17 @@ impl App {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
mutation => {
|
||||
if !behavior::scenario_layout::presentation::present(
|
||||
self.project(),
|
||||
context.representation,
|
||||
self.selected,
|
||||
)
|
||||
.capabilities()
|
||||
.coordinate_input
|
||||
{
|
||||
self.status = "Layout editing is unavailable for this presentation. Saved positions are preserved.".into();
|
||||
return Task::none();
|
||||
}
|
||||
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty()
|
||||
{
|
||||
self.status =
|
||||
@@ -2092,6 +2237,13 @@ impl App {
|
||||
})
|
||||
{
|
||||
self.behavior.active_owner = Some(choice.id);
|
||||
if self.project().elements[&choice.id].kind() == Kind::Scenario {
|
||||
self.enter_scenario_representation(
|
||||
behavior::scenario_layout::Representation::Derived { owner: choice.id },
|
||||
);
|
||||
} else {
|
||||
self.behavior.scenario.forget_view();
|
||||
}
|
||||
self.behavior.diagram = Default::default();
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.select(choice.id);
|
||||
@@ -2457,6 +2609,15 @@ impl App {
|
||||
crate::behavior::selected_behavior(self.project(), self.selected)
|
||||
{
|
||||
self.behavior.active_owner = Some(owner);
|
||||
if self.project().elements[&owner].kind() == Kind::Scenario
|
||||
&& self.behavior.scenario.owner() != Some(owner)
|
||||
{
|
||||
self.enter_scenario_representation(
|
||||
behavior::scenario_layout::Representation::Derived { owner },
|
||||
);
|
||||
} else if self.project().elements[&owner].kind() != Kind::Scenario {
|
||||
self.behavior.scenario.forget_view();
|
||||
}
|
||||
}
|
||||
self.behavior.active_owner = crate::behavior::selected_owner(self);
|
||||
self.reset_state_view_after_navigation(previous_state_view);
|
||||
@@ -2693,6 +2854,7 @@ impl App {
|
||||
}
|
||||
Message::Layer(layer) => {
|
||||
if self.apply() {
|
||||
self.behavior.scenario.forget_view();
|
||||
let previous_state_view = self.state_view_identity();
|
||||
self.layer = layer;
|
||||
self.create_kind = if layer == Layer::Oa {
|
||||
@@ -2865,6 +3027,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::RevealInDiagram(diagram, selected) => {
|
||||
if self.saved_scenario_diagram(diagram) {
|
||||
return self.reveal_saved_scenario(diagram, selected);
|
||||
}
|
||||
let original = self.selected;
|
||||
if !self.apply() {
|
||||
return Task::none();
|
||||
@@ -3096,6 +3261,16 @@ impl App {
|
||||
self.status = "Diagram no longer exists.".into();
|
||||
return Task::none();
|
||||
}
|
||||
if self.saved_scenario_diagram(choice.id)
|
||||
&& behavior::scenario_layout::presentation::checked_owner(
|
||||
self.project(),
|
||||
behavior::scenario_layout::Representation::Saved { diagram: choice.id },
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
self.status = "This Scenario view has an incompatible or missing owner.".into();
|
||||
return Task::none();
|
||||
}
|
||||
let previous_diagram = self.active_diagram;
|
||||
let follow_view = previous_diagram == Some(self.selected);
|
||||
if self.apply() {
|
||||
@@ -3115,7 +3290,14 @@ impl App {
|
||||
}
|
||||
self.selected_node = None;
|
||||
self.refresh_draft();
|
||||
self.fit();
|
||||
if self.saved_scenario_diagram(diagram) {
|
||||
self.enter_scenario_representation(
|
||||
behavior::scenario_layout::Representation::Saved { diagram },
|
||||
);
|
||||
} else {
|
||||
self.page = Page::Diagram;
|
||||
self.fit();
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Projection(projection) => {
|
||||
@@ -5748,12 +5930,17 @@ mod saved_scenario_dispatch_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_saved_picker_and_reveal_do_not_apply_pending_properties() {
|
||||
fn saved_picker_with_invalid_properties_and_reveal_with_pending_properties_do_not_apply() {
|
||||
for reveal in [false, true] {
|
||||
let (mut app, architecture, saved, owner) = fixture();
|
||||
let _ = app.update(Message::Field(
|
||||
"/name".into(),
|
||||
"Unapplied scenario name".into(),
|
||||
if reveal { "/name" } else { "/id" }.into(),
|
||||
if reveal {
|
||||
"Unapplied scenario name"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
.into(),
|
||||
));
|
||||
assert!(app.draft_dirty());
|
||||
let before = app.document.bundle().clone();
|
||||
@@ -5773,7 +5960,6 @@ mod saved_scenario_dispatch_tests {
|
||||
assert_eq!(app.active_diagram, Some(architecture));
|
||||
assert_eq!(app.selected, selected);
|
||||
assert!(app.draft_dirty());
|
||||
assert_eq!(app.status, SAVED_SCENARIO_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user