//! State actions capture their own diagram; architecture selection stays separate. use super::{ state_edit::{self, Endpoint, Event, Tool}, states, }; use crate::{ app::{App, Message}, context_menu::{Entry, Menu}, model_menu::{self, Opened}, }; use oa_core::*; pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option> { let project = app.project(); let machine = super::selected_owner(app) .filter(|id| project.elements[id].kind() == Kind::StateMachine)?; let context = app.state_context(); let request = &opened.request; let id = request.object; let item = |caption: &str, message: Message, enabled: bool| { if clean && enabled { Entry::action( caption, Message::Menu(model_menu::Event::Invoke(opened.token, Box::new(message))), ) } else { Entry::disabled(caption) } }; let diagram_event = Message::StateDiagram; let form = |tool| { Message::Behavior(super::Event::States(states::Event::Start( machine, tool, id, ))) }; let mut entries = vec![Entry::title(crate::views::short(project.named(id), 38))]; if !clean { entries.push(Entry::title("Apply or cancel pending edits first")); } if let Some(context) = context.filter(|context| context.diagram == id) { entries.extend([ item("View properties", Message::EditProperties(id), true), item("Duplicate view", Message::DuplicateDiagram(id), true), item("Fit diagram", diagram_event(Event::Fit(context)), true), item( "Create another state view", diagram_event(Event::CreateView(machine)), true, ), Entry::separator(), item( "Create region…", diagram_event(Event::Choose(Tool::Region)), true, ), item( "New transition with details…", form(states::Tool::Transition), true, ), ]); } else { entries.extend([ item("Properties", Message::EditProperties(id), true), item("Open source", Message::SourceForSelection, true), item( "Connections & references", Message::InspectorTab(crate::navigation::InspectorTab::Connections), true, ), ]); if let Some(element) = project.elements.get(&id) && projection::state_machine_contains(project, machine, id) { entries.push(Entry::separator()); let creates = if element.kind() == Kind::Region { Some(( "Create state…", Kind::State, states::Tool::Vertex, Tool::Vertex(Kind::State), )) } else if element.kind() == Kind::StateMachine || element.kind().is_stable_state() { Some(( "Create region…", Kind::Region, states::Tool::Region, Tool::Region, )) } else { None }; if let Some((caption, kind, typed, tool)) = creates { entries.push(if let Some(context) = context { item( caption, diagram_event(Event::Choose(tool)), state_edit::eligible_owner(project, context.diagram, id, kind), ) } else { item(caption, form(typed), true) }); } entries.push(item( "New transition with details…", form(states::Tool::Transition), true, )); if let Some(context) = context && let Some(node) = request .placement .and_then(|placement| project.diagrams[&context.diagram].nodes.get(&placement)) .filter(|node| node.element == id) { entries.push(Entry::separator()); entries.push(item( "Placement properties", Message::EditProperties(node.id), true, )); if element.kind() == Kind::StateMachine || element.kind() == Kind::Region || element.kind().is_stable_state() { let mut changed = node.clone(); changed.collapsed = !changed.collapsed; entries.push(item( if node.collapsed { "Unfold contents" } else { "Fold contents" }, Message::Command(Command::EditNode { diagram: context.diagram, original: node.id, node: changed, }), true, )); } entries.push( item( "Hide from this diagram", Message::Command(Command::Delete(node.id)), true, ) .shortcut("Delete"), ); } } if let Some(relation) = project.relations.get(&id) && relation.kind() == RelationKind::StateTransition && projection::state_machine_contains(project, machine, relation.source()) && projection::state_machine_contains(project, machine, relation.target()) { entries.push(Entry::separator()); let edit = context.map_or_else( || Message::Behavior(super::Event::States(states::Event::Edit(id))), |context| diagram_event(Event::EditTransition(context, id)), ); entries.push(item("Edit transition", edit, true)); if let Some(context) = context { let bend_geometry = state_edit::bend_route(project, context.diagram, id); let bend_count = bend_geometry.as_ref().map_or(0, |route| route.pins.len()); let can_add = bend_geometry.as_ref().is_some_and(|route| { bend_count < 128 && request.route_segment.map_or_else( || route.points.len() >= 4, |(index, point)| { index .checked_add(2) .is_some_and(|end| end <= route.points.len()) && point.x.is_finite() && point.y.is_finite() }, ) }); entries.push(item( if request.route_segment.is_some() { "Add bend here" } else { "Add bend" }, diagram_event(Event::AddBend { context, relation: id, segment: request.route_segment, }), can_add, )); if let Some(index) = request.bend { entries.push(item( "Remove bend", diagram_event(Event::RemoveBend { context, relation: id, index, }), bend_geometry.is_some() && index < bend_count, )); } entries.push(item( "Clear bends", diagram_event(Event::ClearBends { context, relation: id, }), bend_geometry.is_some() && bend_count > 0, )); let visible = projection::project_view(project, context.diagram) .edges .iter() .any(|edge| edge.id == id); for (caption, endpoint) in [ ("Reconnect source", Endpoint::Source), ("Reconnect target", Endpoint::Target), ] { entries.push(item( caption, diagram_event(Event::Choose(Tool::Reconnect { relation: id, endpoint, })), visible, )); } for (caption, reset, end) in [ ( "Adjust source attachment", "Reset source attachment", EdgeEnd::Source, ), ( "Adjust target attachment", "Reset target attachment", EdgeEnd::Target, ), ] { let owner = state_edit::attachment_node(project, context.diagram, id, end); entries.push(item( caption, diagram_event(Event::Choose(Tool::AdjustAttachment { relation: id, end })), owner.is_some(), )); if let Some(owner) = owner { let authored = project.diagrams[&context.diagram] .routes .iter() .find(|route| route.edge == id) .and_then(|route| route.attachment(end)) .is_some(); entries.push(item( reset, diagram_event(Event::SetAttachment { context, relation: id, end, placement: owner.id, attachment: None, }), authored, )); } } if !visible { entries.push(Entry::title( "Use transition details for hidden or internal endpoints", )); } } } if id != project.id { entries.push(Entry::separator()); entries.push(item("Delete from model…", Message::Delete, true).shortcut( if context.is_some_and(|context| { context.diagram != id && (request.placement.is_some() || projection::project_view(project, context.diagram) .edges .iter() .any(|edge| edge.id == id) || super::state_canvas::visible_row_relation( project, context.diagram, &projection::project_view(project, context.diagram), id, )) }) { if request.placement.is_some() { "Shift+Delete" } else { "Delete / Shift+Delete" } } else { "" }, )); } } Some(Menu { token: opened.token, anchor: request.anchor, entries, on_close: Message::Menu(model_menu::Event::Close), }) }