//! Exact Process presentation selection and one atomic order transaction. use super::*; use crate::behavior::topology::group_reorder::{Plan, Prepared}; use crate::diagram_selection::Selection; use crate::diagram_usage::{Surface, Target}; use multiselect::{Choice, Event}; fn subject(target: &Target) -> Option { match target { Target::ProcessOccurrence(endpoint) => Some(endpoint.occurrence), Target::ProcessLink(id) | Target::ProcessPreviewLink { relation: id, .. } => Some(*id), _ => None, } } impl App { pub(crate) fn process_preview_primary(&self) -> Option<&Target> { let context = self.topology_context()?; let selection = self.diagram_selection.as_ref()?; if selection.scope() != multiselect::process_context(context).scope { return None; } selection .primary() .filter(|target| subject(target) == Some(self.selected)) .filter(|target| match target { Target::ProcessOccurrence(endpoint) => !endpoint.references.is_empty(), Target::ProcessPreviewLink { .. } => true, _ => false, }) } pub(super) fn reconcile_process_multiselect(&mut self) { let Some(context) = self.topology_context() else { return; }; let current = multiselect::process_context(context); let visible = self .behavior .topology .selection_targets(self.project(), context.owner); let old = self .diagram_selection .as_ref() .filter(|s| s.scope() == current.scope) .and_then(Selection::primary) .cloned(); let address = self.behavior.topology.selection_address(); let focused = self .diagram_focus(Surface::Process(context.owner)) .map(|usage| usage.target.clone()); let legacy = focused .clone() .or_else(|| { address .as_ref() .filter(|a| a.owner == context.owner && a.endpoint.occurrence == self.selected) .map(|a| Target::ProcessOccurrence(a.endpoint.clone())) }) .or_else(|| { visible .iter() .find(|target| match target { Target::ProcessOccurrence(endpoint) => { endpoint.references.is_empty() && endpoint.occurrence == self.selected } Target::ProcessLink(id) => *id == self.selected, _ => false, }) .cloned() }) .filter(|target| visible.contains(target)); let held = old.as_ref().is_some_and(|target| { subject(target) == Some(self.selected) && focused.as_ref().is_none_or(|focused| focused == target) && match target { Target::ProcessOccurrence(endpoint) if !endpoint.references.is_empty() => { address .as_ref() .is_some_and(|a| a.owner == context.owner && a.endpoint == *endpoint) } _ => true, } }); let lost = old.as_ref().is_some_and(|target| !visible.contains(target)); let selection = self .diagram_selection .get_or_insert_with(|| Selection::new(current.scope)); selection.reconcile(current.scope, |target| visible.contains(target)); if held || lost { if selection.primary() != old.as_ref() { self.inspect_process_multiselect_primary(); } } else if selection.primary() != legacy.as_ref() { if let Some(target) = legacy { selection.replace(target, |t| visible.contains(t)); } else { selection.clear(); } } if self.process_preview_primary().is_some() { self.draft = None; } } pub(super) fn inspect_process_multiselect_primary(&mut self) { let Some(context) = self.topology_context() else { return; }; let primary = self .diagram_selection .as_ref() .and_then(Selection::primary) .cloned(); self.behavior.topology.clear_context(); self.selected = primary.as_ref().and_then(subject).unwrap_or(context.owner); self.selected_node = None; self.selected_bend = None; self.selected_attachment = None; self.diagram_navigation_focus = None; if let Some(Target::ProcessOccurrence(endpoint)) = primary && !endpoint.references.is_empty() && let Ok(resolved) = oa_core::topology_context::TopologyIndex::new(self.project()) .resolve(context.owner, &endpoint) { self.behavior.topology.select_context(resolved.address); } self.refresh_draft(); if self.process_preview_primary().is_some() { self.draft = None; } } pub(super) fn process_multiselect_event(&mut self, event: Event) { let Some(context) = self.topology_context() else { return; }; if let Event::Refuse(_, reason) = &event && diagram_removal::enabled(self) { self.status = reason.clone(); return; } if !diagram_removal::enabled(self) || !self.behavior.topology.selecting() { self.status = "Finish or cancel the active edit before changing the process selection.".into(); return; } let visible = self .behavior .topology .selection_targets(self.project(), context.owner); match event { Event::Choose(_, target, choice) => { if !visible.contains(&target) { return; } self.reconcile_process_multiselect(); let selection = self.diagram_selection.as_mut().unwrap(); let valid = |t: &Target| visible.contains(t); 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_process_multiselect_primary(); } Event::Collapse(_, target) => { if !visible.contains(&target) { return; } self.diagram_selection .as_mut() .unwrap() .replace(target, |t| visible.contains(t)); self.inspect_process_multiselect_primary(); } Event::Clear(_) => { self.diagram_selection.as_mut().unwrap().clear(); self.inspect_process_multiselect_primary(); } Event::Refuse(_, reason) => self.status = reason, Event::Hide(_) => { self.status = "Process cards are model contributions, not placements. Hide is unavailable." .into() } Event::Move(..) => { self.status = "Process positions come from their paths. Reorder within one column instead." .into() } Event::ProcessReorder(snapshot, destination) => { if !snapshot .targets() .iter() .all(|target| visible.contains(target)) { return; } match Prepared::new(self.project(), context.owner, snapshot.targets()) .and_then(|prepared| prepared.plan(destination)) { Ok(Plan::Noop) => {} Ok(Plan::Change(command)) => { if self.execute(*command) { self.inspect_process_multiselect_primary(); self.status = format!( "Reordered {} contributions ยท Undo restores their order", snapshot.targets().len() ); } } Err(error) => self.status = error, } } } } /// A no-op must not retire a pending reveal or allocate a history entry. pub(super) fn process_order_noop(&self, message: &Message) -> bool { let Some(context) = self.topology_context() else { return false; }; match message { Message::DiagramSelection(Event::ProcessReorder(snapshot, destination)) => { Prepared::new(self.project(), context.owner, snapshot.targets()) .and_then(|prepared| prepared.plan(*destination)) .is_ok_and(|p| matches!(p, Plan::Noop)) } Message::TopologyCanvas(crate::behavior::topology::CanvasEvent::Reorder { context: captured, occurrence, before, }) if *captured == context => crate::behavior::topology::reorder( self.project(), context.owner, *occurrence, *before, ) .is_ok_and(|p| matches!(p, Plan::Noop)), _ => false, } } } #[cfg(test)] mod tests;