Keep caller identity and property subjects aligned when opening and returning from qualified contributions, and when explicitly navigating to local references. Validate camera, source, history and read-only boundaries.
261 lines
9.9 KiB
Rust
261 lines
9.9 KiB
Rust
//! 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<Id> {
|
|
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;
|