context menu for scene tabs

- fixes #826
This commit is contained in:
Dmitry Stepanov
2026-09-05 11:36:49 +03:00
parent d9bfd553d6
commit 2c50f66432
6 changed files with 176 additions and 43 deletions
+2 -2
View File
@@ -52,7 +52,7 @@ use crate::{
},
},
message::MessageSender,
Message,
utils, Message,
};
use std::{
fs::File,
@@ -382,7 +382,7 @@ impl AssetItemContextMenu {
return true;
} else if message.destination() == self.show_in_explorer {
if let Ok(canonical_path) = item.path.canonicalize() {
asset::show_in_explorer(canonical_path)
utils::show_in_explorer(canonical_path)
}
} else if message.destination() == self.open {
item.open();
-28
View File
@@ -85,34 +85,6 @@ pub mod preview;
mod selection;
pub mod selector;
fn show_in_explorer<P: AsRef<Path>>(path: P) {
// opener crate is bugged on Windows, so using explorer's command directly.
#[cfg(target_os = "windows")]
{
use std::process::Command;
fn execute_command(command: &mut Command) {
match command.spawn() {
Ok(mut process) => Log::verify(process.wait()),
Err(err) => Log::err(format!(
"Failed to show asset item in explorer. Reason: {err:?}"
)),
}
}
execute_command(Command::new("explorer").arg("/select,").arg(path.as_ref()))
}
#[cfg(not(target_os = "windows"))]
{
if let Err(err) = opener::reveal(path) {
Log::err(format!(
"Failed to show asset item in explorer. Reason: {err:?}"
))
}
}
}
fn open_in_explorer<P: AsRef<Path>>(path: P) {
if let Ok(path) = path.as_ref().canonicalize() {
Log::verify(open::that(path))
+107
View File
@@ -0,0 +1,107 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
fyrox::{
core::{
pool::Handle,
uuid::{uuid, Uuid},
},
gui::{
menu::{ContextMenuBuilder, MenuItem, MenuItemMessage},
message::UiMessage,
popup::PopupBuilder,
stack_panel::StackPanelBuilder,
widget::WidgetBuilder,
BuildContext, RcUiNodeHandle,
},
},
menu::create_menu_item,
};
use fyrox::graph::SceneGraph;
use fyrox::gui::popup::{Placement, PopupMessage};
use fyrox::gui::{UiNode, UserInterface};
pub struct SceneItemMenu {
pub menu: RcUiNodeHandle,
close: Handle<MenuItem>,
show_in_explorer: Handle<MenuItem>,
placement_target: Handle<UiNode>,
}
pub enum SceneItemAction {
None,
Close(Uuid),
ShowInExplorer(Uuid),
}
impl SceneItemMenu {
pub const CLOSE: Uuid = uuid!("e4c7f0eb-c9a7-4cf2-af4b-78c028be9c58");
pub const SHOW_IN_EXPLORER: Uuid = uuid!("97a43a80-7860-4fce-b0fa-26ae6677d132");
pub fn new(ctx: &mut BuildContext) -> Self {
let close = create_menu_item("Close", Self::CLOSE, vec![], ctx);
let show_in_explorer =
create_menu_item("Show In Explorer", Self::SHOW_IN_EXPLORER, vec![], ctx);
let content = StackPanelBuilder::new(
WidgetBuilder::new()
.with_child(close)
.with_child(show_in_explorer),
)
.build(ctx);
let menu = ContextMenuBuilder::new(
PopupBuilder::new(WidgetBuilder::new().with_visibility(false)).with_content(content),
)
.build(ctx);
let menu = RcUiNodeHandle::new(menu, ctx.sender());
Self {
menu,
close,
show_in_explorer,
placement_target: Default::default(),
}
}
pub fn handle_ui_message(
&mut self,
ui: &UserInterface,
message: &UiMessage,
) -> SceneItemAction {
if let Some(MenuItemMessage::Click) = message.data() {
if let Some(placement_target_id) = self.placement_target_id(ui) {
if message.destination == self.close {
return SceneItemAction::Close(placement_target_id);
} else if message.destination == self.show_in_explorer {
return SceneItemAction::ShowInExplorer(placement_target_id);
}
}
} else if let Some(PopupMessage::Placement(Placement::Cursor(target))) =
message.data_from(self.menu.handle())
{
self.placement_target = *target;
}
SceneItemAction::None
}
fn placement_target_id(&self, ui: &UserInterface) -> Option<Uuid> {
ui.try_get(self.placement_target).ok().map(|n| n.id)
}
}
+37 -13
View File
@@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::scene_viewer::menu::{SceneItemAction, SceneItemMenu};
use crate::{
fyrox::{
core::{color::Color, math::Rect, pool::Handle, uuid::Uuid},
@@ -60,6 +61,7 @@ use crate::{
scene::container::EditorSceneEntry,
scene_viewer::gizmo::{SceneGizmo, SceneGizmoAction},
settings::SettingsMessage,
utils,
utils::enable_widget,
DropdownListBuilder, GameScene, Message, Mode, SaveSceneConfirmationDialogAction,
SceneContainer, Settings,
@@ -82,6 +84,7 @@ use strum::{IntoEnumIterator, VariantNames};
use strum_macros::{AsRefStr, EnumIter, EnumString, VariantNames};
mod gizmo;
mod menu;
#[derive(Default, Clone, Debug, EnumIter, AsRefStr, EnumString, VariantNames)]
pub enum GraphicsDebugSwitches {
@@ -284,6 +287,7 @@ pub struct SceneViewer {
scene_gizmo_image: Handle<Image>,
debug_switches: Handle<DropdownList>,
grid_snap_menu: GridSnappingMenu,
scene_item_menu: SceneItemMenu,
}
impl SceneViewer {
@@ -558,6 +562,9 @@ impl SceneViewer {
.with_title(WindowTitle::text("Scene Preview"))
.with_tab_label("Scene")
.build(ctx);
let scene_item_menu = SceneItemMenu::new(ctx);
Self {
sender,
window,
@@ -577,6 +584,7 @@ impl SceneViewer {
debug_switches,
grid_snap_menu,
build,
scene_item_menu,
}
}
}
@@ -671,6 +679,19 @@ impl SceneViewer {
self.sync_interaction_modes(new_scene, ui)
}
fn close_scene(&self, scenes: &mut SceneContainer, uuid: &Uuid) {
if let Some(entry) = scenes.entry_by_scene_id(*uuid) {
if entry.need_save() {
self.sender.send(Message::OpenSaveSceneConfirmationDialog {
id: entry.id,
action: SaveSceneConfirmationDialogAction::CloseScene(entry.id),
});
} else {
self.sender.send(Message::CloseScene(entry.id));
}
}
}
pub fn handle_ui_message(
&mut self,
message: &mut UiMessage,
@@ -683,6 +704,20 @@ impl SceneViewer {
let ui = engine.user_interfaces.first_mut();
match self.scene_item_menu.handle_ui_message(ui, message) {
SceneItemAction::None => {}
SceneItemAction::Close(scene_id) => self.close_scene(scenes, &scene_id),
SceneItemAction::ShowInExplorer(scene_id) => {
if let Some(entry) = scenes.entry_by_scene_id(scene_id) {
if let Some(entry_path) = entry.path.as_ref() {
if let Ok(canonical_path) = entry_path.canonicalize() {
utils::show_in_explorer(canonical_path);
}
}
}
}
}
if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
for (mode_id, mode_button) in self.interaction_modes.iter() {
if message.destination() == *mode_button {
@@ -744,18 +779,7 @@ impl SceneViewer {
&& message.direction() == MessageDirection::FromWidget
{
match msg {
TabControlMessage::CloseTab(uuid) => {
if let Some(entry) = scenes.entry_by_scene_id(*uuid) {
if entry.need_save() {
self.sender.send(Message::OpenSaveSceneConfirmationDialog {
id: entry.id,
action: SaveSceneConfirmationDialogAction::CloseScene(entry.id),
});
} else {
self.sender.send(Message::CloseScene(entry.id));
}
}
}
TabControlMessage::CloseTab(uuid) => self.close_scene(scenes, uuid),
TabControlMessage::ActiveTab(Some(uuid)) => {
if let Some(entry) = scenes.entry_by_scene_id(*uuid) {
self.sender.send(Message::SetCurrentScene(entry.id));
@@ -920,7 +944,7 @@ impl SceneViewer {
content: Default::default(),
can_be_closed: true,
user_data: None,
context_menu: None,
context_menu: Some(self.scene_item_menu.menu.clone()),
}),
);
}
+29
View File
@@ -36,6 +36,7 @@ use crate::{
},
load_image,
};
use fyrox::core::log::Log;
use fyrox::core::pool::ObjectOrVariant;
use fyrox::gui::button::Button;
use fyrox::gui::file_browser::{FileSelector, FileSelectorMode, FileType};
@@ -176,6 +177,34 @@ pub fn is_native_scene(path: &Path) -> bool {
}
}
pub fn show_in_explorer<P: AsRef<Path>>(path: P) {
// opener crate is bugged on Windows, so using explorer's command directly.
#[cfg(target_os = "windows")]
{
use std::process::Command;
fn execute_command(command: &mut Command) {
match command.spawn() {
Ok(mut process) => Log::verify(process.wait()),
Err(err) => Log::err(format!(
"Failed to show asset item in explorer. Reason: {err:?}"
)),
}
}
execute_command(Command::new("explorer").arg("/select,").arg(path.as_ref()))
}
#[cfg(not(target_os = "windows"))]
{
if let Err(err) = opener::reveal(path) {
Log::err(format!(
"Failed to show asset item in explorer. Reason: {err:?}"
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;