//! Exact saved-view selection and atomic group commands. use super::*; use crate::diagram_selection::{Context, Scope, Selection, Snapshot}; use crate::diagram_usage::{Surface, Target}; #[derive(Debug, Clone, Copy)] pub(crate) enum Choice { Replace, Add, Remove, Toggle, Inspect, } #[derive(Debug, Clone)] pub(crate) enum Event { Choose(Context, Target, Choice), Collapse(Snapshot, Target), Clear(Context), Move(Snapshot, Position), Hide(Snapshot), ProcessReorder( Snapshot, crate::behavior::topology::group_reorder::Destination, ), Refuse(Snapshot, String), } pub(crate) fn context(c: crate::route_edit::Context) -> Context { Context { scope: Scope { document: c.document, surface: Surface::Saved(c.diagram), }, revision: c.revision, epoch: c.epoch, } } pub(crate) fn process_context(c: crate::behavior::topology::Context) -> Context { Context { scope: Scope { document: c.document, surface: Surface::Process(c.owner), }, revision: c.revision, epoch: c.epoch, } } /// The entire typed set must support a placement action. Never filter a mixture. pub(crate) fn placements(snapshot: &Snapshot) -> Option> { snapshot.primary()?; if snapshot.targets().is_empty() { return None; } snapshot .targets() .iter() .map(|target| match target { Target::Placement(id) => Some(*id), _ => None, }) .collect() } pub(super) fn visible( project: &Project, diagram: Id, view: &projection::ProjectedView, target: &Target, ) -> bool { match target { Target::Placement(id) => view.nodes.iter().any(|n| n.id == *id), Target::Port { port, placement } => view .ports .iter() .any(|p| p.element == *port && p.owner == *placement), Target::Edge(id) => view.edges.iter().any(|e| e.id == *id), Target::StateRow { placement, relation, } => crate::behavior::state_canvas::visible_row_target( project, diagram, view, *placement, *relation, ), _ => false, } } impl App { pub(crate) fn multiselect_boundary(&self) -> impl PartialEq + use<> { ( self.canvas_document, self.document.revision(), self.page, self.active_diagram, self.selected, self.selected_node, (self.page == Page::Behavior).then(|| self.state_view_identity()), (self.page == Page::Behavior).then_some(self.behavior.diagram.placement), self.topology_context(), self.behavior.topology.selection_address(), ) } pub(crate) fn multiselect_context(&self) -> Option { match self.page { Page::Diagram => self.route_context().map(context), Page::Behavior => { if let Some(current) = self.topology_context() { return Some(process_context(current)); } let current = self.state_context()?; crate::behavior::state_edit::validate_context(self.project(), current, current) .ok()?; Some(context(current)) } _ => None, } } pub(crate) fn multiselect_target_visible(&self, target: &Target) -> bool { if let Some(context) = self.topology_context() { return self .behavior .topology .selection_targets(self.project(), context.owner) .contains(target); } let Some(Context { scope: Scope { surface: Surface::Saved(diagram), .. }, .. }) = self.multiselect_context() else { return false; }; let view = projection::project_view_with_index( self.project(), diagram, self.document.view_index(), ); visible(self.project(), diagram, &view, target) } pub(crate) fn multiselect_snapshot(&self) -> Option { self.diagram_selection .as_ref()? .snapshot(self.multiselect_context()?) } pub(crate) fn multiselect_envelope(&self, event: &Event) -> bool { let Some(current) = self.multiselect_context() else { return false; }; match event { Event::Choose(c, ..) | Event::Clear(c) => *c == current, Event::Move(s, _) | Event::Hide(s) | Event::ProcessReorder(s, _) | Event::Refuse(s, _) | Event::Collapse(s, _) => self .diagram_selection .as_ref() .is_some_and(|selection| selection.matches(s, current)), } } fn legacy_target(&self, diagram: Id, view: &projection::ProjectedView) -> Option { let placement = if self.page == Page::Behavior { self.behavior.diagram.placement } else { self.selected_node }; if let Some(id) = placement && view.nodes.iter().any(|node| { node.id == id && (node.element == self.selected || node.id == self.selected) }) { return Some(Target::Placement(id)); } if self.page == Page::Behavior { // Keep a row's exact occurrence while inspecting its relation. // Its owner placement is never a node-inspection or Hide target. if let Some(target @ Target::StateRow { relation, .. }) = self.diagram_selection.as_ref().and_then(|s| s.primary()) && *relation == self.selected && visible(self.project(), diagram, view, target) { return Some(target.clone()); } if let Some(node) = view.nodes.iter().find(|node| { crate::behavior::state_canvas::visible_row_target( self.project(), diagram, view, node.id, self.selected, ) }) { return Some(Target::StateRow { placement: node.id, relation: self.selected, }); } } if let Some(port) = view.ports.iter().find(|port| port.element == self.selected) { return Some(Target::Port { port: port.element, placement: port.owner, }); } view.edges .iter() .any(|edge| edge.id == self.selected) .then_some(Target::Edge(self.selected)) } pub(crate) fn reconcile_multiselect(&mut self) { if self.topology_context().is_some() { self.reconcile_process_multiselect(); return; } let Some(current) = self.multiselect_context() else { self.diagram_selection = None; return; }; let Surface::Saved(diagram) = current.scope.surface else { return; }; let view = projection::project_view_with_index( self.project(), diagram, self.document.view_index(), ); let legacy = self.legacy_target(diagram, &view); let lost_primary = self .diagram_selection .as_ref() .filter(|s| s.scope() == current.scope) .and_then(|s| s.primary()) .is_some_and(|target| !visible(self.project(), diagram, &view, target)); let primary_placement = if self.page == Page::Behavior { self.behavior.diagram.placement } else { self.selected_node }; let held_primary = self .diagram_selection .as_ref() .filter(|s| s.scope() == current.scope) .and_then(|s| s.primary()) .is_some_and(|target| match target { Target::Placement(id) => primary_placement == Some(*id), Target::Port { port, .. } => self.selected == *port, Target::Edge(id) => self.selected == *id, Target::StateRow { relation, .. } => { self.selected == *relation && primary_placement.is_none() } _ => false, }); let project = self.document.project(); let selection = self .diagram_selection .get_or_insert_with(|| Selection::new(current.scope)); selection.reconcile(current.scope, |target| { visible(project, diagram, &view, target) }); // Navigation/property selection remains explicit. A removed exact target // is never rebound through its semantic identity to a different placement. if held_primary || lost_primary { if selection.primary() != legacy.as_ref() { self.inspect_multiselect_primary(); } } else if selection.primary() != legacy.as_ref() { if let Some(target) = legacy { selection.replace(target, |target| visible(project, diagram, &view, target)); } else { selection.clear(); } } } pub(super) fn inspect_multiselect_primary(&mut self) { if self.topology_context().is_some() { self.inspect_process_multiselect_primary(); return; } let diagram = self .multiselect_context() .and_then(|c| match c.scope.surface { Surface::Saved(diagram) => Some(diagram), _ => None, }); let primary = self .diagram_selection .as_ref() .and_then(|s| s.primary()) .cloned(); self.selected_node = None; if self.page == Page::Behavior { self.behavior.diagram.placement = None; } match primary { Some(Target::Placement(id)) => { if let Some(node) = diagram .and_then(|d| self.project().diagrams.get(&d)) .and_then(|d| d.nodes.get(&id)) { self.selected = node.element; if self.page == Page::Behavior { self.behavior.diagram.placement = Some(id); } else { self.selected_node = Some(id); } } } Some(Target::Port { port, .. }) => self.selected = port, Some(Target::Edge(id)) => self.selected = id, Some(Target::StateRow { relation, .. }) => self.selected = relation, _ => self.selected = diagram.unwrap_or(self.project().id), } self.selected_bend = None; self.selected_attachment = None; self.refresh_draft(); } pub(crate) fn multiselect_event(&mut self, event: Event) { if !self.multiselect_envelope(&event) { return; } if self.topology_context().is_some() { self.process_multiselect_event(event); return; } if !diagram_removal::enabled(self) || self.architecture.is_some() || self.connection.is_some() || (self.page == Page::Behavior && self.behavior.diagram.tool != crate::behavior::state_edit::Tool::Select) { self.status = "Finish or cancel the active edit before changing the diagram selection.".into(); return; } let current = self.multiselect_context().unwrap(); let Surface::Saved(diagram) = current.scope.surface else { return; }; let view = projection::project_view_with_index( self.project(), diagram, self.document.view_index(), ); match event { Event::ProcessReorder(..) => {} Event::Collapse(_, target) => { if !visible(self.project(), diagram, &view, &target) { return; } let project = self.document.project(); self.diagram_selection .as_mut() .unwrap() .replace(target, |target| visible(project, diagram, &view, target)); self.inspect_multiselect_primary(); } Event::Choose(_, target, choice) => { if !visible(self.project(), diagram, &view, &target) { return; } self.reconcile_multiselect(); let project = self.document.project(); let selection = self.diagram_selection.as_mut().unwrap(); let valid = |target: &Target| visible(project, diagram, &view, target); match choice { Choice::Replace => { selection.replace(target, valid); } Choice::Add => { selection.add(target, valid); } Choice::Remove => { selection.remove(&target); } Choice::Toggle => { selection.toggle(target, valid); } Choice::Inspect => { selection.set_primary(&target); } } self.inspect_multiselect_primary(); } Event::Clear(_) => { if let Some(selection) = &mut self.diagram_selection { selection.clear(); } self.inspect_multiselect_primary(); } Event::Refuse(_, reason) => self.status = reason, Event::Move(snapshot, delta) => { let Some(ids) = placements(&snapshot) else { self.status = "Move requires only placed elements. Remove ports, rows and connections from the selection.".into(); return; }; if !snapshot .targets() .iter() .all(|target| visible(self.project(), diagram, &view, target)) { return; } if self.project().diagrams[&diagram].projection == ProjectionKind::Allocation && ids.iter().any(|id| { self.project().elements [&self.project().diagrams[&diagram].nodes[id].element] .kind() .is_work() }) { self.status = "Use an Allocation drag to assign this selection, or arm Arrange next drag." .into(); return; } if delta.x == 0. && delta.y == 0. { return; } match oa_core::diagram_edit::translate_placements( self.project(), diagram, &ids, delta, ) { Ok(command) => { if self.execute(command) { self.inspect_multiselect_primary(); self.status = format!( "Moved {} selected elements · Undo restores the group", ids.len() ); } } Err(error) => self.status = error.to_string(), } } Event::Hide(snapshot) => { let Some(ids) = placements(&snapshot) else { self.status = "Hide requires only placed elements. Model objects remain unchanged." .into(); return; }; if !snapshot .targets() .iter() .all(|target| visible(self.project(), diagram, &view, target)) { return; } if self.execute(Command::Batch( ids.iter().copied().map(Command::Delete).collect(), )) { if let Some(selection) = &mut self.diagram_selection { selection.clear(); } self.inspect_multiselect_primary(); self.status = format!("Hid {} placements · Model objects retained", ids.len()); } } } } }