Wire exact Scenario group selection and contextual admission
This commit is contained in:
@@ -19,6 +19,7 @@ pub struct Request {
|
||||
pub creation_point: Option<Position>,
|
||||
pub canvas: Option<route_edit::Context>,
|
||||
pub process: Option<Box<crate::behavior::topology::Context>>,
|
||||
pub scenario: Option<Box<crate::behavior::scenario::Context>>,
|
||||
pub source_file: Option<String>,
|
||||
}
|
||||
impl Request {
|
||||
@@ -34,6 +35,7 @@ impl Request {
|
||||
creation_point: None,
|
||||
canvas: None,
|
||||
process: None,
|
||||
scenario: None,
|
||||
source_file: None,
|
||||
}
|
||||
}
|
||||
@@ -55,6 +57,7 @@ pub struct Opened {
|
||||
page: Page,
|
||||
layer: Layer,
|
||||
diagram: Option<Id>,
|
||||
scenario_selection: Option<crate::diagram_selection::Snapshot>,
|
||||
}
|
||||
impl Opened {
|
||||
pub fn new(app: &App, request: Request, token: u64) -> Option<Self> {
|
||||
@@ -67,9 +70,18 @@ impl Opened {
|
||||
.process
|
||||
.as_deref()
|
||||
.is_some_and(|c| Some(*c) != app.topology_context())
|
||||
|| request
|
||||
.scenario
|
||||
.as_deref()
|
||||
.is_some_and(|c| Some(*c) != app.scenario_context())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let scenario_selection = if request.scenario.is_some() {
|
||||
Some(app.multiselect_snapshot()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Self {
|
||||
token,
|
||||
request,
|
||||
@@ -79,6 +91,7 @@ impl Opened {
|
||||
page: app.page,
|
||||
layer: app.layer,
|
||||
diagram: current_diagram(app),
|
||||
scenario_selection,
|
||||
})
|
||||
}
|
||||
pub fn valid(&self, app: &App) -> bool {
|
||||
@@ -99,6 +112,15 @@ impl Opened {
|
||||
.process
|
||||
.as_deref()
|
||||
.is_none_or(|c| Some(*c) == app.topology_context())
|
||||
&& self
|
||||
.request
|
||||
.scenario
|
||||
.as_deref()
|
||||
.is_none_or(|c| Some(*c) == app.scenario_context())
|
||||
&& self
|
||||
.scenario_selection
|
||||
.as_ref()
|
||||
.is_none_or(|captured| app.multiselect_snapshot().as_ref() == Some(captured))
|
||||
&& self
|
||||
.request
|
||||
.source_file
|
||||
@@ -109,9 +131,19 @@ impl Opened {
|
||||
|
||||
fn target_matches(app: &App, request: &Request) -> bool {
|
||||
use crate::diagram_usage::Target;
|
||||
if request.scenario.is_some()
|
||||
&& (request.canvas.is_some() || request.process.is_some() || request.placement.is_some())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(target) = request.selection_target.as_deref() else {
|
||||
return true;
|
||||
};
|
||||
if let Some(context) = request.scenario.as_deref() {
|
||||
return Some(*context) == app.scenario_context()
|
||||
&& matches!(target, Target::ScenarioObject(id) if *id == request.object)
|
||||
&& app.multiselect_target_visible(target);
|
||||
}
|
||||
if (request.canvas.is_none() && request.process.is_none())
|
||||
|| !app.multiselect_target_visible(target)
|
||||
{
|
||||
@@ -159,6 +191,17 @@ fn current_diagram(app: &App) -> Option<Id> {
|
||||
if app.page == Page::Behavior {
|
||||
app.topology_context()
|
||||
.map(|context| context.owner)
|
||||
.or_else(|| {
|
||||
app.scenario_context()
|
||||
.map(|context| match context.representation {
|
||||
crate::behavior::scenario_layout::Representation::Derived { owner } => {
|
||||
owner
|
||||
}
|
||||
crate::behavior::scenario_layout::Representation::Saved { diagram } => {
|
||||
diagram
|
||||
}
|
||||
})
|
||||
})
|
||||
.or_else(|| app.state_context().map(|context| context.diagram))
|
||||
} else {
|
||||
app.active_diagram
|
||||
@@ -172,49 +215,56 @@ pub fn open(object: Id, anchor: Point) -> Message {
|
||||
pub fn trigger(app: &App) -> iced::Element<'_, Message> {
|
||||
crate::context_menu::Area::left(
|
||||
crate::views::action("Actions ▾", Message::Menu(Event::Close)),
|
||||
move |anchor| {
|
||||
let mut request = Request::object(app.selected, anchor);
|
||||
if app.page == Page::Diagram {
|
||||
request.placement = app.selected_node;
|
||||
request.bend = app
|
||||
.selected_bend
|
||||
.filter(|(id, _)| *id == app.selected)
|
||||
.map(|(_, i)| i);
|
||||
request.canvas = app.route_context();
|
||||
} else if app.page == Page::Behavior {
|
||||
request.canvas = app.state_context();
|
||||
request.process = app.topology_context().map(Box::new);
|
||||
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
|
||||
request.selection_target = Some(Box::new(target.clone()));
|
||||
request.placement = match target {
|
||||
crate::diagram_usage::Target::Placement(id) => {
|
||||
if let Some(node) = request
|
||||
.canvas
|
||||
.and_then(|c| app.project().diagrams.get(&c.diagram))
|
||||
.and_then(|d| d.nodes.get(id))
|
||||
{
|
||||
request.object = node.element;
|
||||
}
|
||||
Some(*id)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
} else if let Some(context) = request.canvas
|
||||
&& let Some(node) = app.project().diagrams[&context.diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|node| node.element == app.selected || node.id == app.selected)
|
||||
{
|
||||
request.object = node.element;
|
||||
request.placement = Some(node.id);
|
||||
}
|
||||
}
|
||||
Message::Menu(Event::Open(request))
|
||||
},
|
||||
move |anchor| Message::Menu(Event::Open(actions_request(app, anchor))),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn actions_request(app: &App, anchor: Point) -> Request {
|
||||
let mut request = Request::object(app.selected, anchor);
|
||||
if app.page == Page::Diagram {
|
||||
request.placement = app.selected_node;
|
||||
request.bend = app
|
||||
.selected_bend
|
||||
.filter(|(id, _)| *id == app.selected)
|
||||
.map(|(_, i)| i);
|
||||
request.canvas = app.route_context();
|
||||
} else if app.page == Page::Behavior {
|
||||
request.canvas = app.state_context();
|
||||
request.process = app.topology_context().map(Box::new);
|
||||
request.scenario = app.scenario_context().map(Box::new);
|
||||
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
|
||||
request.selection_target = Some(Box::new(target.clone()));
|
||||
request.placement = match target {
|
||||
crate::diagram_usage::Target::Placement(id) => {
|
||||
if let Some(node) = request
|
||||
.canvas
|
||||
.and_then(|c| app.project().diagrams.get(&c.diagram))
|
||||
.and_then(|d| d.nodes.get(id))
|
||||
{
|
||||
request.object = node.element;
|
||||
}
|
||||
Some(*id)
|
||||
}
|
||||
crate::diagram_usage::Target::ScenarioObject(id) => {
|
||||
request.object = *id;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
} else if let Some(context) = request.canvas
|
||||
&& let Some(node) = app.project().diagrams[&context.diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|node| node.element == app.selected || node.id == app.selected)
|
||||
{
|
||||
request.object = node.element;
|
||||
request.placement = Some(node.id);
|
||||
}
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
let opened = app.model_menu.as_ref().filter(|menu| menu.valid(app))?;
|
||||
let request = &opened.request;
|
||||
@@ -261,6 +311,78 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
});
|
||||
}
|
||||
let mut entries = vec![Entry::title(crate::views::short(p.named(id), 38))];
|
||||
if let Some(context) = request.scenario.as_deref().copied() {
|
||||
use crate::app::multiselect::Choice;
|
||||
use crate::behavior::scenario::{CanvasEvent, selection};
|
||||
let snapshot = opened.scenario_selection.as_ref()?;
|
||||
let selection_message = |intent| {
|
||||
Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(selection::Event {
|
||||
context,
|
||||
snapshot: snapshot.clone(),
|
||||
intent,
|
||||
})))
|
||||
};
|
||||
if let Some(target) = request.selection_target.as_deref() {
|
||||
let selected = snapshot.targets().contains(target);
|
||||
entries.push(item(
|
||||
if selected {
|
||||
"Remove from selection"
|
||||
} else {
|
||||
"Add to selection"
|
||||
},
|
||||
selection_message(selection::Intent::Choose(
|
||||
target.clone(),
|
||||
if selected {
|
||||
Choice::Remove
|
||||
} else {
|
||||
Choice::Add
|
||||
},
|
||||
)),
|
||||
true,
|
||||
));
|
||||
if snapshot.targets().len() > 1 {
|
||||
if selected {
|
||||
entries.push(item(
|
||||
"Inspect this element",
|
||||
selection_message(selection::Intent::Choose(
|
||||
target.clone(),
|
||||
Choice::Inspect,
|
||||
)),
|
||||
true,
|
||||
));
|
||||
}
|
||||
entries.push(item(
|
||||
"Select only this element",
|
||||
selection_message(selection::Intent::Choose(target.clone(), Choice::Replace)),
|
||||
true,
|
||||
));
|
||||
}
|
||||
entries.push(Entry::separator());
|
||||
}
|
||||
// A menu on an unselected subject must not expose singleton model actions
|
||||
// while the canvas still depicts a group. Selection changes are explicit.
|
||||
if snapshot.targets().len() > 1 {
|
||||
entries.push(Entry::title(format!(
|
||||
"{} selected",
|
||||
snapshot.targets().len()
|
||||
)));
|
||||
entries.push(Entry::disabled(
|
||||
"Move selected occurrences (not yet available)",
|
||||
));
|
||||
entries.push(Entry::disabled(
|
||||
"Remove selected occurrences (not yet available)",
|
||||
));
|
||||
entries.push(Entry::title(
|
||||
"Select one occurrence for its individual actions",
|
||||
));
|
||||
return Some(Menu {
|
||||
token: opened.token,
|
||||
anchor: request.anchor,
|
||||
entries,
|
||||
on_close: Message::Menu(Event::Close),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (request.canvas.is_some() || request.process.is_some())
|
||||
&& let Some(context) = app.multiselect_context()
|
||||
{
|
||||
@@ -798,6 +920,10 @@ fn relation_actions(kind: Kind) -> &'static [(&'static str, RelationKind, bool)]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_menu_scenario_tests.rs"]
|
||||
mod scenario_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user