Add persisted state diagram authoring and context-bound gestures
This commit is contained in:
@@ -53,7 +53,7 @@ impl Opened {
|
||||
if !app.project().contains(request.object)
|
||||
|| request
|
||||
.canvas
|
||||
.is_some_and(|c| Some(c) != app.route_context())
|
||||
.is_some_and(|c| Some(c) != current_canvas(app))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ impl Opened {
|
||||
epoch: app.canvas_epoch,
|
||||
page: app.page,
|
||||
layer: app.layer,
|
||||
diagram: app.active_diagram,
|
||||
diagram: current_diagram(app),
|
||||
})
|
||||
}
|
||||
pub fn valid(&self, app: &App) -> bool {
|
||||
@@ -74,8 +74,12 @@ impl Opened {
|
||||
&& self.epoch == app.canvas_epoch
|
||||
&& self.page == app.page
|
||||
&& self.layer == app.layer
|
||||
&& self.diagram == app.active_diagram
|
||||
&& self.diagram == current_diagram(app)
|
||||
&& app.project().contains(self.request.object)
|
||||
&& self
|
||||
.request
|
||||
.canvas
|
||||
.is_none_or(|c| Some(c) == current_canvas(app))
|
||||
&& self
|
||||
.request
|
||||
.source_file
|
||||
@@ -84,6 +88,22 @@ impl Opened {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_canvas(app: &App) -> Option<route_edit::Context> {
|
||||
if app.page == Page::Behavior {
|
||||
app.state_context()
|
||||
} else {
|
||||
app.route_context()
|
||||
}
|
||||
}
|
||||
|
||||
fn current_diagram(app: &App) -> Option<Id> {
|
||||
if app.page == Page::Behavior {
|
||||
app.state_context().map(|context| context.diagram)
|
||||
} else {
|
||||
app.active_diagram
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(object: Id, anchor: Point) -> Message {
|
||||
Message::Menu(Event::Open(Request::object(object, anchor)))
|
||||
}
|
||||
@@ -100,6 +120,17 @@ pub fn trigger(app: &App) -> iced::Element<'_, Message> {
|
||||
.filter(|(id, _)| *id == app.selected)
|
||||
.map(|(_, i)| i);
|
||||
request.canvas = app.route_context();
|
||||
} else if app.page == Page::Behavior {
|
||||
request.canvas = app.state_context();
|
||||
if let Some(context) = request.canvas
|
||||
&& let Some(node) = app.project().diagrams[&context.diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|node| node.element == app.selected || node.id == app.selected)
|
||||
{
|
||||
request.object = node.element;
|
||||
request.placement = Some(node.id);
|
||||
}
|
||||
}
|
||||
Message::Menu(Event::Open(request))
|
||||
},
|
||||
@@ -112,8 +143,9 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
let request = &opened.request;
|
||||
let p = app.project();
|
||||
let id = request.object;
|
||||
let clean =
|
||||
!app.busy && (request.source_file.is_some() || (!app.draft_dirty() && !app.source_dirty()));
|
||||
let clean = !app.busy
|
||||
&& (request.source_file.is_some()
|
||||
|| (!app.draft_dirty() && !app.source_dirty() && !app.behavior.has_pending()));
|
||||
let item = |caption: &str, message: Message, enabled: bool| {
|
||||
if clean && enabled {
|
||||
Entry::action(
|
||||
@@ -151,6 +183,11 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
on_close: Message::Menu(Event::Close),
|
||||
});
|
||||
}
|
||||
if app.page == Page::Behavior
|
||||
&& let Some(menu) = crate::behavior::state_menu::popup(app, opened, clean)
|
||||
{
|
||||
return Some(menu);
|
||||
}
|
||||
let mut entries = vec![Entry::title(crate::views::short(p.named(id), 38))];
|
||||
if !clean {
|
||||
entries.push(Entry::title("Apply or revert pending edits first"));
|
||||
@@ -453,6 +490,193 @@ mod tests {
|
||||
})
|
||||
.unwrap_or_else(|| panic!("Expected enabled menu action {caption}"))
|
||||
}
|
||||
#[test]
|
||||
fn state_menu_folds_and_hides_only_its_captured_placement_and_rejects_stale_views() {
|
||||
use crate::behavior::state_edit;
|
||||
let mut project = Project::empty();
|
||||
let machine = project
|
||||
.add_element(Kind::StateMachine, Layer::Oa, "Modes", None)
|
||||
.unwrap();
|
||||
let region = project
|
||||
.add_element(Kind::Region, Layer::Oa, "Region", Some(machine))
|
||||
.unwrap();
|
||||
let state = project
|
||||
.add_element(Kind::State, Layer::Oa, "Ready", Some(region))
|
||||
.unwrap();
|
||||
let architecture = project.ensure_diagram(Layer::Oa);
|
||||
project.place(architecture, state, Position { x: 80., y: 90. });
|
||||
let first = state_edit::create_view(&project, machine).unwrap();
|
||||
let first_id = first.diagram;
|
||||
project = execute_batch(&project, &[first.command]).unwrap();
|
||||
let second = state_edit::create_view(&project, machine).unwrap();
|
||||
let second_id = second.diagram;
|
||||
project = execute_batch(&project, &[second.command]).unwrap();
|
||||
let placement = project.diagrams[&first_id]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|node| node.element == state)
|
||||
.unwrap()
|
||||
.id;
|
||||
let mut app = App::with_project(project.clone());
|
||||
send(&mut app, Message::Select(machine));
|
||||
send(&mut app, Message::Page(Page::Behavior));
|
||||
app.behavior.diagram.diagram = Some(first_id);
|
||||
app.active_diagram = Some(architecture);
|
||||
let open_state = |app: &mut App| {
|
||||
let mut request = Request::object(state, Point::ORIGIN);
|
||||
request.canvas = app.state_context();
|
||||
request.placement = Some(placement);
|
||||
send(app, Message::Menu(Event::Open(request)));
|
||||
};
|
||||
|
||||
// A canvas menu from Architecture is stale on the state page even when
|
||||
// the architecture diagram itself remains the active architecture view.
|
||||
let mut wrong = Request::object(state, Point::ORIGIN);
|
||||
wrong.canvas = app.route_context();
|
||||
assert!(Opened::new(&app, wrong, 1).is_none());
|
||||
open_state(&mut app);
|
||||
let fold = choose(&app, "Fold contents");
|
||||
send(&mut app, fold);
|
||||
assert!(app.project().diagrams[&first_id].nodes[&placement].collapsed);
|
||||
assert_eq!(app.selected, state, "Folding retains semantic selection");
|
||||
assert_eq!(app.page, Page::Behavior);
|
||||
assert_eq!(app.state_context().unwrap().diagram, first_id);
|
||||
assert_eq!(app.active_diagram, Some(architecture));
|
||||
assert_eq!(
|
||||
app.project().diagrams[&architecture],
|
||||
project.diagrams[&architecture]
|
||||
);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&second_id],
|
||||
project.diagrams[&second_id]
|
||||
);
|
||||
assert_eq!(app.project().elements, project.elements);
|
||||
open_state(&mut app);
|
||||
let unfold = choose(&app, "Unfold contents");
|
||||
send(&mut app, unfold);
|
||||
assert_eq!(app.selected, state, "Unfolding retains semantic selection");
|
||||
assert_eq!(app.project(), &project);
|
||||
assert_eq!(app.active_diagram, Some(architecture));
|
||||
send(&mut app, Message::Undo);
|
||||
assert!(app.project().diagrams[&first_id].nodes[&placement].collapsed);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &project);
|
||||
|
||||
open_state(&mut app);
|
||||
let stale = choose(&app, "Fold contents");
|
||||
app.behavior.diagram.diagram = Some(second_id);
|
||||
send(&mut app, stale);
|
||||
assert_eq!(app.project(), &project);
|
||||
app.behavior.diagram.diagram = Some(first_id);
|
||||
open_state(&mut app);
|
||||
let hide = choose(&app, "Hide from this diagram");
|
||||
send(&mut app, hide);
|
||||
assert!(
|
||||
!app.project().diagrams[&first_id]
|
||||
.nodes
|
||||
.contains_key(&placement)
|
||||
);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&architecture],
|
||||
project.diagrams[&architecture]
|
||||
);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&second_id],
|
||||
project.diagrams[&second_id]
|
||||
);
|
||||
assert_eq!(app.project().elements, project.elements);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &project);
|
||||
|
||||
// Placement properties deliberately select a placement record, but an
|
||||
// accepted numeric edit must keep the Behavior view's own context.
|
||||
let source_before = app.document.bundle().clone();
|
||||
open_state(&mut app);
|
||||
let properties = choose(&app, "Placement properties");
|
||||
send(&mut app, properties);
|
||||
assert_eq!(app.selected, placement);
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().kind,
|
||||
crate::forms::RecordKind::Node(first_id)
|
||||
);
|
||||
assert_eq!(app.page, Page::Behavior);
|
||||
assert_eq!(app.active_diagram, Some(architecture));
|
||||
let x = project.diagrams[&first_id].nodes[&placement].position.x + 37.25;
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/position/x".into(), x.to_string()),
|
||||
);
|
||||
assert!(app.draft_dirty());
|
||||
send(&mut app, Message::Apply);
|
||||
assert!(!app.draft_dirty());
|
||||
assert_eq!(app.page, Page::Behavior);
|
||||
assert_eq!(app.behavior.active_owner, Some(machine));
|
||||
assert_eq!(app.state_context().unwrap().diagram, first_id);
|
||||
assert_eq!(
|
||||
app.active_diagram,
|
||||
Some(architecture),
|
||||
"Placement Apply must not replace the Architecture view"
|
||||
);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&first_id)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&placement)
|
||||
.unwrap()
|
||||
.position
|
||||
.x = x;
|
||||
assert_eq!(app.project(), &expected);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &project);
|
||||
assert_eq!(app.document.bundle(), &source_before);
|
||||
assert_eq!(app.page, Page::Behavior);
|
||||
assert_eq!(app.state_context().unwrap().diagram, first_id);
|
||||
assert_eq!(app.active_diagram, Some(architecture));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_menu_keeps_typed_creation_reachable_and_preserves_pending_properties() {
|
||||
let mut project = Project::empty();
|
||||
let machine = project
|
||||
.add_element(Kind::StateMachine, Layer::Oa, "Modes", None)
|
||||
.unwrap();
|
||||
let region = project
|
||||
.add_element(Kind::Region, Layer::Oa, "Region", Some(machine))
|
||||
.unwrap();
|
||||
let entry = project
|
||||
.add_element(Kind::EntryPoint, Layer::Oa, "Entry", Some(region))
|
||||
.unwrap();
|
||||
let mut app = App::with_project(project);
|
||||
send(&mut app, Message::Select(entry));
|
||||
send(&mut app, Message::Page(Page::Behavior));
|
||||
send(&mut app, open(entry, Point::ORIGIN));
|
||||
let start = choose(&app, "New transition with details…");
|
||||
send(&mut app, start);
|
||||
assert_eq!(app.behavior.pending_owner(), Some(machine));
|
||||
send(
|
||||
&mut app,
|
||||
Message::Behavior(crate::behavior::Event::States(
|
||||
crate::behavior::states::Event::Cancel,
|
||||
)),
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/name".into(), "Unapplied entry".into()),
|
||||
);
|
||||
let before = app.draft.as_ref().unwrap().materialize().unwrap();
|
||||
send(&mut app, open(entry, Point::ORIGIN));
|
||||
assert!(popup(&app).unwrap().entries.iter().all(|entry| !matches!(
|
||||
entry,
|
||||
Entry::Action {
|
||||
message: Some(_),
|
||||
..
|
||||
}
|
||||
)));
|
||||
assert_eq!(app.draft.as_ref().unwrap().materialize().unwrap(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_click_targets_actor_without_changing_selection_until_action() {
|
||||
let mut app = App::new();
|
||||
|
||||
Reference in New Issue
Block a user