From 22f46704628bbea2bd411e137b7d487c30b4a60a Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 16 Apr 2024 11:40:18 +0300 Subject: [PATCH] finished keyboard navigation for menu widget + fixed context menus - fixed context menus in the editor to use ContextMenu widget instead of simple Popup --- editor/src/absm/blendspace.rs | 10 +- editor/src/absm/state_graph/context.rs | 28 ++--- editor/src/absm/state_viewer/context.rs | 70 +++++++------ editor/src/animation/ruler.rs | 10 +- editor/src/animation/track.rs | 10 +- editor/src/asset/mod.rs | 10 +- editor/src/log.rs | 10 +- editor/src/material.rs | 10 +- fyrox-ui/src/curve/mod.rs | 10 +- fyrox-ui/src/file_browser/menu.rs | 84 +++++++-------- fyrox-ui/src/inspector/mod.rs | 10 +- fyrox-ui/src/menu.rs | 134 +++++++++++++++++------- fyrox-ui/src/node/constructor.rs | 4 +- 13 files changed, 245 insertions(+), 155 deletions(-) diff --git a/editor/src/absm/blendspace.rs b/editor/src/absm/blendspace.rs index 8db483d42..8ca84ff0c 100644 --- a/editor/src/absm/blendspace.rs +++ b/editor/src/absm/blendspace.rs @@ -42,6 +42,7 @@ use crate::{ message::MessageSender, send_sync_message, }; +use fyrox::gui::menu::ContextMenuBuilder; use std::{ cell::Cell, fmt::{Debug, Formatter}, @@ -486,8 +487,8 @@ impl BlendSpaceFieldBuilder { fn build(self, ctx: &mut BuildContext) -> Handle { let add_point; let remove_point; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -500,8 +501,9 @@ impl BlendSpaceFieldBuilder { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); let field = BlendSpaceField { diff --git a/editor/src/absm/state_graph/context.rs b/editor/src/absm/state_graph/context.rs index 18259c67b..fcb935d3c 100644 --- a/editor/src/absm/state_graph/context.rs +++ b/editor/src/absm/state_graph/context.rs @@ -27,6 +27,7 @@ use crate::{ message::MessageSender, scene::{commands::ChangeSelectionCommand, Selection}, }; +use fyrox::gui::menu::ContextMenuBuilder; use std::fmt::Debug; use super::fetch_state_node_model_handle; @@ -43,8 +44,8 @@ impl CanvasContextMenu { pub fn new(ctx: &mut BuildContext) -> Self { let create_state; let connect_all_nodes; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new(WidgetBuilder::new().with_children([ { create_state = create_menu_item("Create State", vec![], ctx); @@ -56,8 +57,9 @@ impl CanvasContextMenu { }, ])) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { @@ -145,8 +147,8 @@ impl NodeContextMenu { let set_as_entry_state; let enter_state; let connect_to_all_nodes; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -176,8 +178,9 @@ impl NodeContextMenu { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { @@ -319,15 +322,16 @@ pub struct TransitionContextMenu { impl TransitionContextMenu { pub fn new(ctx: &mut BuildContext) -> Self { let remove; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new(WidgetBuilder::new().with_child({ remove = create_menu_item("Remove Transition", vec![], ctx); remove })) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { diff --git a/editor/src/absm/state_viewer/context.rs b/editor/src/absm/state_viewer/context.rs index 6d5c3fa24..56ff25188 100644 --- a/editor/src/absm/state_viewer/context.rs +++ b/editor/src/absm/state_viewer/context.rs @@ -33,6 +33,7 @@ use crate::{ message::MessageSender, scene::{commands::ChangeSelectionCommand, Selection}, }; +use fyrox::gui::menu::ContextMenuBuilder; pub struct CanvasContextMenu { create_play_animation: Handle, @@ -50,32 +51,35 @@ impl CanvasContextMenu { let create_blend_animations; let create_blend_by_index; let create_blend_space; - let menu = PopupBuilder::new( - WidgetBuilder::new() - .with_enabled(false) // Disabled by default. - .with_visibility(false), - ) - .with_content( - StackPanelBuilder::new( + let menu = ContextMenuBuilder::new( + PopupBuilder::new( WidgetBuilder::new() - .with_child({ - create_play_animation = create_menu_item("Play Animation", vec![], ctx); - create_play_animation - }) - .with_child({ - create_blend_animations = create_menu_item("Blend Animations", vec![], ctx); - create_blend_animations - }) - .with_child({ - create_blend_by_index = create_menu_item("Blend By Index", vec![], ctx); - create_blend_by_index - }) - .with_child({ - create_blend_space = create_menu_item("Blend Space", vec![], ctx); - create_blend_space - }), + .with_enabled(false) // Disabled by default. + .with_visibility(false), ) - .build(ctx), + .with_content( + StackPanelBuilder::new( + WidgetBuilder::new() + .with_child({ + create_play_animation = create_menu_item("Play Animation", vec![], ctx); + create_play_animation + }) + .with_child({ + create_blend_animations = + create_menu_item("Blend Animations", vec![], ctx); + create_blend_animations + }) + .with_child({ + create_blend_by_index = create_menu_item("Blend By Index", vec![], ctx); + create_blend_by_index + }) + .with_child({ + create_blend_space = create_menu_item("Blend Space", vec![], ctx); + create_blend_space + }), + ) + .build(ctx), + ), ) .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); @@ -187,8 +191,8 @@ impl NodeContextMenu { pub fn new(ctx: &mut BuildContext) -> Self { let remove; let set_as_root; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -201,8 +205,9 @@ impl NodeContextMenu { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { @@ -279,15 +284,16 @@ pub struct ConnectionContextMenu { impl ConnectionContextMenu { pub fn new(ctx: &mut BuildContext) -> Self { let remove; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new(WidgetBuilder::new().with_child({ remove = create_menu_item("Remove Connection", vec![], ctx); remove })) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { diff --git a/editor/src/animation/ruler.rs b/editor/src/animation/ruler.rs index adeb43903..93d69b157 100644 --- a/editor/src/animation/ruler.rs +++ b/editor/src/animation/ruler.rs @@ -24,6 +24,7 @@ use crate::fyrox::{ }, }; use crate::menu::create_menu_item; +use fyrox::gui::menu::ContextMenuBuilder; use std::{ cell::{Cell, RefCell}, fmt::{Debug, Formatter}, @@ -65,8 +66,8 @@ impl ContextMenu { fn new(ctx: &mut BuildContext) -> Self { let add_signal; let remove_signal; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -79,8 +80,9 @@ impl ContextMenu { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { diff --git a/editor/src/animation/track.rs b/editor/src/animation/track.rs index 0591fa4a4..e2a869a56 100644 --- a/editor/src/animation/track.rs +++ b/editor/src/animation/track.rs @@ -72,6 +72,7 @@ use crate::{ }, send_sync_message, utils, }; +use fyrox::gui::menu::ContextMenuBuilder; use std::{ any::TypeId, cmp::Ordering, @@ -105,8 +106,8 @@ impl TrackContextMenu { let set_target; let rebind; let duplicate; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -127,8 +128,9 @@ impl TrackContextMenu { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { diff --git a/editor/src/asset/mod.rs b/editor/src/asset/mod.rs index a64d62b55..d0fe41e13 100644 --- a/editor/src/asset/mod.rs +++ b/editor/src/asset/mod.rs @@ -52,6 +52,7 @@ use crate::{ Message, Mode, }; use fyrox::core::parking_lot::RwLock; +use fyrox::gui::menu::ContextMenuBuilder; use std::{ ffi::OsStr, path::{Path, PathBuf}, @@ -111,8 +112,8 @@ impl ContextMenu { let copy_path; let copy_file_name; let dependencies; - let menu = PopupBuilder::new(WidgetBuilder::new()) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new()).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -153,8 +154,9 @@ impl ContextMenu { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { diff --git a/editor/src/log.rs b/editor/src/log.rs index 1f61f666a..f353d1e7b 100644 --- a/editor/src/log.rs +++ b/editor/src/log.rs @@ -28,6 +28,7 @@ use crate::{ gui::{make_dropdown_list_option, make_image_button_with_tooltip}, load_image, Brush, Color, DropdownListBuilder, Engine, }; +use fyrox::gui::menu::ContextMenuBuilder; use std::sync::mpsc::Receiver; struct ContextMenu { @@ -39,8 +40,8 @@ struct ContextMenu { impl ContextMenu { pub fn new(ctx: &mut BuildContext) -> Self { let copy; - let menu = PopupBuilder::new(WidgetBuilder::new()) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new()).with_content( StackPanelBuilder::new(WidgetBuilder::new().with_child({ copy = MenuItemBuilder::new(WidgetBuilder::new()) .with_content(MenuItemContent::text("Copy")) @@ -48,8 +49,9 @@ impl ContextMenu { copy })) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); Self { diff --git a/editor/src/material.rs b/editor/src/material.rs index 9149b82fc..4cda36650 100644 --- a/editor/src/material.rs +++ b/editor/src/material.rs @@ -50,6 +50,7 @@ use crate::{ scene::commands::material::{SetMaterialPropertyValueCommand, SetMaterialShaderCommand}, send_sync_message, Engine, Message, }; +use fyrox::gui::menu::ContextMenuBuilder; use std::{path::Path, sync::Arc}; struct TextureContextMenu { @@ -63,8 +64,8 @@ impl TextureContextMenu { fn new(ctx: &mut BuildContext) -> Self { let show_in_asset_browser; let unassign; - let popup = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let popup = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -81,8 +82,9 @@ impl TextureContextMenu { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let popup = RcUiNodeHandle::new(popup, ctx.sender()); Self { diff --git a/fyrox-ui/src/curve/mod.rs b/fyrox-ui/src/curve/mod.rs index ad0630f76..5fd7793bd 100644 --- a/fyrox-ui/src/curve/mod.rs +++ b/fyrox-ui/src/curve/mod.rs @@ -1,3 +1,4 @@ +use crate::menu::ContextMenuBuilder; use crate::{ brush::Brush, core::{ @@ -1310,8 +1311,8 @@ impl CurveEditorBuilder { let key_properties; let key_value; let key_location; - let context_menu = PopupBuilder::new(WidgetBuilder::new()) - .with_content( + let context_menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new()).with_content( StackPanelBuilder::new( WidgetBuilder::new() .with_child({ @@ -1414,8 +1415,9 @@ impl CurveEditorBuilder { }), ) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let context_menu = RcUiNodeHandle::new(context_menu, ctx.sender()); if self.widget_builder.foreground.is_none() { diff --git a/fyrox-ui/src/file_browser/menu.rs b/fyrox-ui/src/file_browser/menu.rs index 5548fb578..26e4ab5db 100644 --- a/fyrox-ui/src/file_browser/menu.rs +++ b/fyrox-ui/src/file_browser/menu.rs @@ -6,10 +6,10 @@ use crate::{ }, draw::DrawingContext, grid::{Column, GridBuilder, Row}, - menu::{MenuItemBuilder, MenuItemContent, MenuItemMessage}, + menu::{ContextMenu, ContextMenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage}, message::{MessageDirection, OsEvent, UiMessage}, messagebox::{MessageBoxBuilder, MessageBoxButtons, MessageBoxMessage, MessageBoxResult}, - popup::{Placement, Popup, PopupBuilder, PopupMessage}, + popup::{Placement, PopupBuilder, PopupMessage}, stack_panel::StackPanelBuilder, text::{TextBuilder, TextMessage}, text_box::TextBoxBuilder, @@ -38,7 +38,7 @@ pub struct FolderNameDialog { #[derive(Clone, Visit, Reflect, Debug, ComponentProvider)] pub struct ItemContextMenu { #[component(include)] - pub popup: Popup, + pub base_menu: ContextMenu, pub delete: Handle, pub make_folder: Handle, pub delete_message_box: Cell>, @@ -49,13 +49,13 @@ impl Deref for ItemContextMenu { type Target = Widget; fn deref(&self) -> &Self::Target { - &self.popup.widget + &self.base_menu.popup.widget } } impl DerefMut for ItemContextMenu { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.popup.widget + &mut self.base_menu.popup.widget } } @@ -63,27 +63,27 @@ uuid_provider!(ItemContextMenu = "6a9d597f-6a9f-4bad-b569-4cff1a6deff7"); impl Control for ItemContextMenu { fn on_remove(&self, sender: &Sender) { - self.popup.on_remove(sender) + self.base_menu.on_remove(sender) } fn measure_override(&self, ui: &UserInterface, available_size: Vector2) -> Vector2 { - self.popup.measure_override(ui, available_size) + self.base_menu.measure_override(ui, available_size) } fn arrange_override(&self, ui: &UserInterface, final_size: Vector2) -> Vector2 { - self.popup.arrange_override(ui, final_size) + self.base_menu.arrange_override(ui, final_size) } fn draw(&self, drawing_context: &mut DrawingContext) { - self.popup.draw(drawing_context) + self.base_menu.draw(drawing_context) } fn update(&mut self, dt: f32, ui: &mut UserInterface) { - self.popup.update(dt, ui) + self.base_menu.update(dt, ui) } fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { - self.popup.handle_routed_message(ui, message); + self.base_menu.handle_routed_message(ui, message); if let Some(PopupMessage::Placement(Placement::Cursor(_))) = message.data() { if message.destination() == self.handle { @@ -210,7 +210,7 @@ impl Control for ItemContextMenu { } fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) { - self.popup.preview_message(ui, message); + self.base_menu.preview_message(ui, message); if let Some(MessageBoxMessage::Close(result)) = message.data() { if message.destination() == self.delete_message_box.get() { @@ -286,7 +286,7 @@ impl Control for ItemContextMenu { ui: &mut UserInterface, event: &OsEvent, ) { - self.popup.handle_os_event(self_handle, ui, event) + self.base_menu.handle_os_event(self_handle, ui, event) } } @@ -294,38 +294,40 @@ impl ItemContextMenu { pub fn build(ctx: &mut BuildContext) -> Handle { let delete; let make_folder; - let popup = PopupBuilder::new( - WidgetBuilder::new() - .with_preview_messages(true) - .with_visibility(false), - ) - .with_content( - StackPanelBuilder::new( + let base_menu = ContextMenuBuilder::new( + PopupBuilder::new( WidgetBuilder::new() - .with_width(120.0) - .with_child({ - delete = MenuItemBuilder::new( - WidgetBuilder::new().with_margin(Thickness::uniform(2.0)), - ) - .with_content(MenuItemContent::text("Delete")) - .build(ctx); - delete - }) - .with_child({ - make_folder = MenuItemBuilder::new( - WidgetBuilder::new().with_margin(Thickness::uniform(2.0)), - ) - .with_content(MenuItemContent::text("Make Folder")) - .build(ctx); - make_folder - }), + .with_preview_messages(true) + .with_visibility(false), ) - .build(ctx), + .with_content( + StackPanelBuilder::new( + WidgetBuilder::new() + .with_width(120.0) + .with_child({ + delete = MenuItemBuilder::new( + WidgetBuilder::new().with_margin(Thickness::uniform(2.0)), + ) + .with_content(MenuItemContent::text("Delete")) + .build(ctx); + delete + }) + .with_child({ + make_folder = MenuItemBuilder::new( + WidgetBuilder::new().with_margin(Thickness::uniform(2.0)), + ) + .with_content(MenuItemContent::text("Make Folder")) + .build(ctx); + make_folder + }), + ) + .build(ctx), + ), ) - .build_popup(ctx); + .build_context_menu(ctx); let menu = Self { - popup, + base_menu, delete, make_folder, delete_message_box: Default::default(), @@ -336,7 +338,7 @@ impl ItemContextMenu { } fn item_path(&self, ui: &UserInterface) -> Option { - ui.try_get(self.popup.placement.target()) + ui.try_get(self.base_menu.popup.placement.target()) .and_then(|n| n.user_data_cloned::()) } } diff --git a/fyrox-ui/src/inspector/mod.rs b/fyrox-ui/src/inspector/mod.rs index 9185a56b4..d903a2d84 100644 --- a/fyrox-ui/src/inspector/mod.rs +++ b/fyrox-ui/src/inspector/mod.rs @@ -2,6 +2,7 @@ //! structure or enumeration recursively. It's primary usage is provide unified and simple way of introspection. //! See [`Inspector`] docs for more info and usage examples. +use crate::menu::ContextMenuBuilder; use crate::{ border::BorderBuilder, check_box::CheckBoxBuilder, @@ -881,8 +882,8 @@ impl InspectorContext { }); let copy_value_as_string; - let menu = PopupBuilder::new(WidgetBuilder::new().with_visibility(false)) - .with_content( + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content( StackPanelBuilder::new(WidgetBuilder::new().with_child({ copy_value_as_string = MenuItemBuilder::new(WidgetBuilder::new()) .with_content(MenuItemContent::text("Copy Value as String")) @@ -890,8 +891,9 @@ impl InspectorContext { copy_value_as_string })) .build(ctx), - ) - .build(ctx); + ), + ) + .build(ctx); let menu = RcUiNodeHandle::new(menu, ctx.sender()); let stack_panel = StackPanelBuilder::new( diff --git a/fyrox-ui/src/menu.rs b/fyrox-ui/src/menu.rs index eee83012e..d4c21b531 100644 --- a/fyrox-ui/src/menu.rs +++ b/fyrox-ui/src/menu.rs @@ -375,6 +375,13 @@ pub struct MenuItem { crate::define_widget_deref!(MenuItem); +impl MenuItem { + fn is_opened(&self, ui: &UserInterface) -> bool { + ui.try_get_of_type::(*self.items_panel) + .map_or(false, |items_panel| *items_panel.popup.is_open) + } +} + // MenuItem uses popup to show its content, popup can be top-most only if it is // direct child of root canvas of UI. This fact adds some complications to search // of parent menu - we can't just traverse the tree because popup is not a child @@ -383,7 +390,7 @@ crate::define_widget_deref!(MenuItem); fn find_menu(from: Handle, ui: &UserInterface) -> Handle { let mut handle = from; while handle.is_some() { - if let Some((_, panel)) = ui.find_component_up::(handle) { + if let Some((_, panel)) = ui.find_component_up::(handle) { // Continue search from parent menu item of popup. handle = panel.parent_menu_item; } else { @@ -413,9 +420,9 @@ fn is_any_menu_item_contains_point(ui: &UserInterface, pt: Vector2) -> bool fn close_menu_chain(from: Handle, ui: &UserInterface) { let mut handle = from; while handle.is_some() { - let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::()); + let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::()); - if let Some(panel) = ui.node(popup_handle).query_component::() { + if let Some(panel) = ui.try_get_of_type::(popup_handle) { ui.send_message(PopupMessage::close( popup_handle, MessageDirection::ToWidget, @@ -506,6 +513,15 @@ impl Control for MenuItem { )); } } + WidgetMessage::MouseLeave => { + if !self.is_opened(ui) { + ui.send_message(MenuItemMessage::select( + self.handle, + MessageDirection::ToWidget, + false, + )); + } + } WidgetMessage::KeyDown(key_code) => { if !message.handled() && *self.is_selected && *key_code == KeyCode::Enter { ui.send_message(MenuItemMessage::click( @@ -527,16 +543,19 @@ impl Control for MenuItem { MenuItemMessage::Select(selected) => { if *self.is_selected != *selected { self.is_selected.set_value_and_mark_modified(*selected); + ui.send_message(DecoratorMessage::select( *self.decorator, MessageDirection::ToWidget, *selected, )); - ui.send_message(WidgetMessage::focus( - self.handle, - MessageDirection::ToWidget, - )); + if *selected { + ui.send_message(WidgetMessage::focus( + self.handle, + MessageDirection::ToWidget, + )); + } } } MenuItemMessage::Open => { @@ -571,13 +590,23 @@ impl Control for MenuItem { *self.items_panel, MessageDirection::ToWidget, )); + if *deselect && *self.is_selected { - ui.send_message(DecoratorMessage::select( - *self.decorator, + ui.send_message(MenuItemMessage::select( + self.handle, MessageDirection::ToWidget, false, )); } + + // Recursively deselect everything in the sub-items container. + for &item in &*self.items_container.items { + ui.send_message(MenuItemMessage::close( + item, + MessageDirection::ToWidget, + true, + )); + } } MenuItemMessage::Click => {} MenuItemMessage::AddItem(item) => { @@ -638,7 +667,7 @@ impl Control for MenuItem { break; } else { let node = ui.node(handle); - if let Some(panel) = node.component_ref::() { + if let Some(panel) = node.component_ref::() { // Once we found popup in chain, we must extract handle // of parent menu item to continue search. handle = panel.parent_menu_item; @@ -668,10 +697,7 @@ impl Control for MenuItem { // Allow closing "orphaned" menus by clicking outside of them. if let OsEvent::MouseInput { state, .. } = event { if *state == ButtonState::Pressed { - if let Some(panel) = ui - .node(*self.items_panel) - .query_component::() - { + if let Some(panel) = ui.node(*self.items_panel).query_component::() { if *panel.popup.is_open { // Ensure that cursor is outside of any menus. if !is_any_menu_item_contains_point(ui, ui.cursor_position()) @@ -956,21 +982,19 @@ impl<'a, 'b> MenuItemBuilder<'a, 'b> { } let panel; - let popup = PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0))) - .with_content({ - panel = StackPanelBuilder::new( - WidgetBuilder::new().with_children(self.items.iter().cloned()), - ) - .build(ctx); - panel - }) - // We'll manually control if popup is either open or closed. - .stays_open(true) - .build_popup(ctx); - let items_panel = ctx.add_node(UiNode::new(MenuItemsPanel { - popup, - parent_menu_item: Default::default(), - })); + let items_panel = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0))) + .with_content({ + panel = StackPanelBuilder::new( + WidgetBuilder::new().with_children(self.items.iter().cloned()), + ) + .build(ctx); + panel + }) + // We'll manually control if popup is either open or closed. + .stays_open(true), + ) + .build(ctx); let menu = MenuItem { widget: self @@ -994,7 +1018,7 @@ impl<'a, 'b> MenuItemBuilder<'a, 'b> { let handle = ctx.add_node(UiNode::new(menu)); // "Link" popup with its parent menu item. - if let Some(popup) = ctx[items_panel].cast_mut::() { + if let Some(popup) = ctx[items_panel].cast_mut::() { popup.parent_menu_item = handle; } @@ -1006,13 +1030,15 @@ impl<'a, 'b> MenuItemBuilder<'a, 'b> { /// an ability for keyboard navigation. #[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)] #[type_uuid(id = "ad8e9e76-c213-4232-9bab-80ebcabd69fa")] -pub struct MenuItemsPanel { +pub struct ContextMenu { + /// Inner popup widget of the context menu. #[component(include)] - popup: Popup, - parent_menu_item: Handle, + pub popup: Popup, + /// Parent menu item of the context menu. Allows you to build chained context menus. + pub parent_menu_item: Handle, } -impl Deref for MenuItemsPanel { +impl Deref for ContextMenu { type Target = Widget; fn deref(&self) -> &Self::Target { @@ -1020,13 +1046,13 @@ impl Deref for MenuItemsPanel { } } -impl DerefMut for MenuItemsPanel { +impl DerefMut for ContextMenu { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.popup.widget } } -impl Control for MenuItemsPanel { +impl Control for ContextMenu { fn on_remove(&self, sender: &Sender) { self.popup.on_remove(sender) } @@ -1084,6 +1110,42 @@ impl Control for MenuItemsPanel { } } +/// Creates [`ContextMenu`] widgets. +pub struct ContextMenuBuilder { + popup_builder: PopupBuilder, + parent_menu_item: Handle, +} + +impl ContextMenuBuilder { + /// Creates new builder instance using an instance of the [`PopupBuilder`]. + pub fn new(popup_builder: PopupBuilder) -> Self { + Self { + popup_builder, + parent_menu_item: Default::default(), + } + } + + /// Sets the desired parent menu item. + pub fn with_parent_menu_item(mut self, parent_menu_item: Handle) -> Self { + self.parent_menu_item = parent_menu_item; + self + } + + /// Finishes context menu building. + pub fn build_context_menu(self, ctx: &mut BuildContext) -> ContextMenu { + ContextMenu { + popup: self.popup_builder.build_popup(ctx), + parent_menu_item: self.parent_menu_item, + } + } + + /// Finishes context menu building and adds it to the user interface. + pub fn build(self, ctx: &mut BuildContext) -> Handle { + let context_menu = self.build_context_menu(ctx); + ctx.add_node(UiNode::new(context_menu)) + } +} + fn keyboard_navigation( ui: &UserInterface, key_code: KeyCode, diff --git a/fyrox-ui/src/node/constructor.rs b/fyrox-ui/src/node/constructor.rs index 09631dec9..34aac4b50 100644 --- a/fyrox-ui/src/node/constructor.rs +++ b/fyrox-ui/src/node/constructor.rs @@ -21,7 +21,7 @@ use crate::{ inspector::Inspector, key::{HotKeyEditor, KeyBindingEditor}, list_view::{ListView, ListViewItem}, - menu::{Menu, MenuItem, MenuItemsPanel}, + menu::{ContextMenu, Menu, MenuItem}, messagebox::MessageBox, nine_patch::NinePatch, numeric::NumericUpDown, @@ -95,7 +95,7 @@ impl WidgetConstructorContainer { container.add::(); container.add::(); container.add::(); - container.add::(); + container.add::(); container.add::(); container.add::();