Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More context menu 4: create a new space view with selected entities #5411

Merged
merged 10 commits into from
Mar 7, 2024
170 changes: 148 additions & 22 deletions crates/re_viewport/src/context_menu/actions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use egui::{Response, Ui};
use itertools::Itertools;
use nohash_hasher::IntSet;

use re_entity_db::InstancePath;
use re_log_types::{EntityPath, EntityPathFilter, EntityPathRule};
use re_space_view::{DataQueryBlueprint, SpaceViewBlueprint};
use re_log_types::{EntityPath, EntityPathFilter, EntityPathRule, RuleEffect};
use re_space_view::{determine_visualizable_entities, DataQueryBlueprint, SpaceViewBlueprint};
use re_viewer_context::{ContainerId, Item, SpaceViewClassIdentifier, SpaceViewId};

use super::{ContextMenuAction, ContextMenuContext};
Expand Down Expand Up @@ -325,15 +329,12 @@

impl ContextMenuAction for MoveContentsToNewContainerAction {
fn supports_selection(&self, ctx: &ContextMenuContext<'_>) -> bool {
if let Some((parent_container_id, _)) = Self::target_container_id_and_position(ctx) {
if let Some(parent_container) = ctx.viewport_blueprint.container(&parent_container_id) {
// same-kind linear containers cannot be nested
if (parent_container.container_kind == egui_tiles::ContainerKind::Vertical
|| parent_container.container_kind == egui_tiles::ContainerKind::Horizontal)
&& parent_container.container_kind == self.0
{
return false;
}
if let Some((parent_container, _)) = ctx.clicked_item_parent_and_position() {
if (parent_container.container_kind == egui_tiles::ContainerKind::Vertical
|| parent_container.container_kind == egui_tiles::ContainerKind::Horizontal)
&& parent_container.container_kind == self.0
{
return false;
}
}

Expand Down Expand Up @@ -366,8 +367,9 @@

fn process_selection(&self, ctx: &ContextMenuContext<'_>) {
if let Some(root_container_id) = ctx.viewport_blueprint.root_container {
let (target_container_id, target_position) =
Self::target_container_id_and_position(ctx).unwrap_or((root_container_id, 0));
let (target_container_id, target_position) = ctx
.clicked_item_parent_id_and_position()
.unwrap_or((root_container_id, 0));

let contents = ctx
.selection
Expand All @@ -388,14 +390,138 @@
}
}

impl MoveContentsToNewContainerAction {
fn target_container_id_and_position(
ctx: &ContextMenuContext<'_>,
) -> Option<(ContainerId, usize)> {
ctx.clicked_item
.clone()
.try_into()
.ok()
.and_then(|c| ctx.viewport_blueprint.find_parent_and_position_index(&c))
// ---

/// Create a new space view containing the selected entities.
///
/// The space view is created next to the clicked item's parent view (if a data result was clicked).
pub(super) struct AddEntitiesToNewSpaceViewAction;

impl ContextMenuAction for AddEntitiesToNewSpaceViewAction {
fn supports_multi_selection(&self, _ctx: &ContextMenuContext<'_>) -> bool {
true
}

fn supports_item(&self, _ctx: &ContextMenuContext<'_>, item: &Item) -> bool {
matches!(item, Item::DataResult(_, _) | Item::InstancePath(_))
}

fn ui(&self, ctx: &ContextMenuContext<'_>, ui: &mut Ui) -> Response {
let space_view_class_registry = ctx.viewer_context.space_view_class_registry;

let recommended_space_view_classes = recommended_space_views_for_selection(ctx);
let other_space_view_classes: IntSet<_> = space_view_class_registry
.iter_registry()
.map(|entry| entry.class.identifier())
.collect::<IntSet<SpaceViewClassIdentifier>>()
.difference(&recommended_space_view_classes)
.cloned()
.collect();

ui.menu_button("Add to new space view", |ui| {
let buttons_for_space_view_classes =
|ui: &mut egui::Ui, space_view_classes: &IntSet<SpaceViewClassIdentifier>| {
for (identifier, display_name) in space_view_classes
.iter()
.map(|identifier| {
(
identifier,
space_view_class_registry
.get_class_or_log_error(&identifier)

Check failure on line 430 in crates/re_viewport/src/context_menu/actions.rs

View workflow job for this annotation

GitHub Actions / Rust Checks / Rust lints (fmt, check, cranky, tests, doc)

this expression creates a reference which is immediately dereferenced by the compiler
.display_name(),
)
})
.sorted_by_key(|(_, display_name)| display_name.to_owned())
{
if ui.button(display_name).clicked() {
create_space_view_with_entities(ctx, *identifier);
}
}
};

ui.label(egui::WidgetText::from("Recommended:").italics());
buttons_for_space_view_classes(ui, &recommended_space_view_classes);
ui.label(egui::WidgetText::from("Others:").italics());
abey79 marked this conversation as resolved.
Show resolved Hide resolved
buttons_for_space_view_classes(ui, &other_space_view_classes);
})
.response
}
}

/// Builds a list of compatible space views for the provided selection.
fn recommended_space_views_for_selection(
ctx: &ContextMenuContext<'_>,
) -> IntSet<SpaceViewClassIdentifier> {
let entities_of_interest = ctx
abey79 marked this conversation as resolved.
Show resolved Hide resolved
.selection
.iter()
.filter_map(|(item, _)| item.entity_path())
.collect::<Vec<_>>();

let mut output: IntSet<SpaceViewClassIdentifier> = IntSet::default();

let space_view_class_registry = ctx.viewer_context.space_view_class_registry;
let entity_db = ctx.viewer_context.entity_db;
let applicable_entities_per_visualizer =
space_view_class_registry.applicable_entities_for_visualizer_systems(entity_db.store_id());

for entry in space_view_class_registry.iter_registry() {
let visualizable_entities = determine_visualizable_entities(
&applicable_entities_per_visualizer,
entity_db,
&space_view_class_registry.new_visualizer_collection(entry.class.identifier()),
&*entry.class,
&EntityPath::root(),
);

let covered = entities_of_interest.iter().all(|entity| {
visualizable_entities
.0
.iter()
.any(|(_, entities)| entities.0.contains(*entity))
});

if covered {
output.insert(entry.class.identifier());
}
}

output
}

/// Creates a space view of the give class, with root set as origin, and a filter set to include all
/// selected entities.
fn create_space_view_with_entities(
abey79 marked this conversation as resolved.
Show resolved Hide resolved
ctx: &ContextMenuContext<'_>,
identifier: SpaceViewClassIdentifier,
) {
let origin = EntityPath::root();

let mut filter = EntityPathFilter::default();
ctx.selection
.iter()
.filter_map(|(item, _)| item.entity_path())
.for_each(|path| {
filter.add_rule(
RuleEffect::Include,
EntityPathRule::including_subtree(path.clone()),
);
});

let target_container_id = ctx.clicked_item_parent_id_and_position().map(|(id, _)| id);

let space_view = SpaceViewBlueprint::new(
identifier,
&origin,
DataQueryBlueprint::new(identifier, filter),
);

ctx.viewport_blueprint.add_space_views(
std::iter::once(space_view),
ctx.viewer_context,
target_container_id,
None,
);
ctx.viewport_blueprint
.mark_user_interaction(ctx.viewer_context);
}
40 changes: 37 additions & 3 deletions crates/re_viewport/src/context_menu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use once_cell::sync::OnceCell;
use re_entity_db::InstancePath;
use re_viewer_context::{ContainerId, Item, Selection, SpaceViewId, ViewerContext};

use crate::ViewportBlueprint;
use crate::{ContainerBlueprint, Contents, ViewportBlueprint};

mod actions;
mod sub_menu;

use actions::{
AddContainerAction, AddSpaceViewAction, CloneSpaceViewAction, HideAction,
MoveContentsToNewContainerAction, RemoveAction, ShowAction,
AddContainerAction, AddEntitiesToNewSpaceViewAction, AddSpaceViewAction, CloneSpaceViewAction,
HideAction, MoveContentsToNewContainerAction, RemoveAction, ShowAction,
};
use sub_menu::SubMenu;

Expand Down Expand Up @@ -143,6 +143,7 @@ fn action_list(
)),
],
})],
vec![Box::new(AddEntitiesToNewSpaceViewAction)],
]
})
}
Expand Down Expand Up @@ -173,6 +174,11 @@ fn show_context_menu_for_selection(ctx: &ContextMenuContext<'_>, ui: &mut egui::

should_display_separator |= any_action_displayed;
}

// nothing was shown, make sure the context menu isn't empty
if !should_display_separator {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[not actionable] this method is getting weirder, heh. Last time I already was staring at it trying to figure out how to express this whole thing simpler, but I'm still a bit at a loss 🤷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that flag has double-duty. I started by adding a new was_anything_added flag, and it turned out to be implemented exactly like should_display_separator. Let's see if some renaming may help here.

ui.label(egui::RichText::from("No action available for the current selection").italics());
}
}

/// Context information provided to context menu actions
Expand All @@ -183,6 +189,34 @@ struct ContextMenuContext<'a> {
clicked_item: &'a Item,
}

impl<'a> ContextMenuContext<'a> {
/// Return the clicked item's parent container id and position within it.
///
/// Valid only for space views, containers, and data results.
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
pub fn clicked_item_parent_id_and_position(&self) -> Option<(ContainerId, usize)> {
abey79 marked this conversation as resolved.
Show resolved Hide resolved
match self.clicked_item {
Item::SpaceView(space_view_id) | Item::DataResult(space_view_id, _) => {
Some(Contents::SpaceView(*space_view_id))
}
Item::Container(container_id) => Some(Contents::Container(*container_id)),
_ => None,
}
.and_then(|c: Contents| self.viewport_blueprint.find_parent_and_position_index(&c))
}

/// Return the clicked item's parent container and position within it.
///
/// Valid only for space views, containers, and data results.
pub fn clicked_item_parent_and_position(&self) -> Option<(&'a ContainerBlueprint, usize)> {
abey79 marked this conversation as resolved.
Show resolved Hide resolved
self.clicked_item_parent_id_and_position()
.and_then(|(container_id, pos)| {
self.viewport_blueprint
.container(&container_id)
.map(|container| (container, pos))
})
}
}

/// Context menu actions must implement this trait.
///
/// Actions must do three things, corresponding to three core methods:
Expand Down
Loading