477 lines
17 KiB
Rust
477 lines
17 KiB
Rust
//! 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<std::collections::BTreeSet<Id>> {
|
|
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<Context> {
|
|
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<Snapshot> {
|
|
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<Target> {
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|