diff --git a/editor/src/plugins/inspector/editors/surface.rs b/editor/src/plugins/inspector/editors/surface.rs index 7debee4c5..17ca289c7 100644 --- a/editor/src/plugins/inspector/editors/surface.rs +++ b/editor/src/plugins/inspector/editors/surface.rs @@ -58,10 +58,7 @@ use crate::{ }; use fyrox::gui::brush::Brush; use fyrox::gui::message::MessageData; -use std::{ - any::TypeId, - sync::mpsc::Sender, -}; +use std::{any::TypeId, sync::mpsc::Sender}; #[derive(Debug, PartialEq, Clone)] pub enum SurfaceDataPropertyEditorMessage { diff --git a/editor/src/scene_viewer/mod.rs b/editor/src/scene_viewer/mod.rs index c3306081c..656e59700 100644 --- a/editor/src/scene_viewer/mod.rs +++ b/editor/src/scene_viewer/mod.rs @@ -74,6 +74,9 @@ use strum::{IntoEnumIterator, VariantNames}; use strum_macros::{AsRefStr, EnumIter, EnumString, VariantNames}; mod gizmo; +mod scene_tab_context_menu; + +pub use scene_tab_context_menu::SceneTabContextMenu; #[derive(Default, Clone, Debug, EnumIter, AsRefStr, EnumString, VariantNames)] pub enum GraphicsDebugSwitches { @@ -280,6 +283,7 @@ pub struct SceneViewer { scene_gizmo_image: Handle, debug_switches: Handle, grid_snap_menu: GridSnappingMenu, + scene_tab_context_menu: SceneTabContextMenu, } impl SceneViewer { @@ -306,6 +310,7 @@ impl SceneViewer { .build(ctx); let grid_snap_menu = GridSnappingMenu::new(ctx, settings); + let scene_tab_context_menu = SceneTabContextMenu::new(ctx); let debug_switches; let contextual_actions = StackPanelBuilder::new( @@ -573,6 +578,7 @@ impl SceneViewer { scene_gizmo_image, debug_switches, grid_snap_menu, + scene_tab_context_menu, build, } } @@ -685,6 +691,17 @@ impl SceneViewer { ) { self.grid_snap_menu.handle_ui_message(message, settings); + // Handle scene tab context menu + let scene_ids: Vec<_> = scenes.iter().map(|e| e.id).collect(); + let scene_paths: Vec<_> = scenes.iter().map(|e| e.path.as_deref()).collect(); + self.scene_tab_context_menu.handle_ui_message( + message, + &self.sender, + &scene_ids, + &scene_paths, + engine.user_interfaces.first(), + ); + let ui = engine.user_interfaces.first_mut(); if let Some(ButtonMessage::Click) = message.data::() { @@ -918,12 +935,19 @@ impl SceneViewer { // Add any missing tabs. for entry in scenes.iter() { if tabs.iter().all(|tab| tab.uuid != entry.id) { - let header = TextBuilder::new(WidgetBuilder::new().with_margin(Thickness { - left: 4.0, - top: 2.0, - right: 4.0, - bottom: 2.0, - })) + let header = TextBuilder::new( + WidgetBuilder::new() + .with_margin(Thickness { + left: 4.0, + top: 2.0, + right: 4.0, + bottom: 2.0, + }) + .with_context_menu(self.scene_tab_context_menu.menu.clone()) + .with_user_data(std::sync::Arc::new(fyrox::core::parking_lot::Mutex::new( + entry.id, + ))), + ) .with_text(entry.name()) .build(&mut engine.user_interfaces.first_mut().build_ctx()); diff --git a/editor/src/scene_viewer/scene_tab_context_menu.rs b/editor/src/scene_viewer/scene_tab_context_menu.rs new file mode 100644 index 000000000..4229839b8 --- /dev/null +++ b/editor/src/scene_viewer/scene_tab_context_menu.rs @@ -0,0 +1,205 @@ +// 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. + +//! Context menu for scene tabs in the scene viewer. + +use crate::{ + fyrox::{ + core::{pool::Handle, uuid::Uuid}, + graph::BaseSceneGraph, + gui::{ + menu::{ContextMenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage}, + message::UiMessage, + popup::{Placement, PopupBuilder, PopupMessage}, + stack_panel::StackPanelBuilder, + widget::WidgetBuilder, + BuildContext, RcUiNodeHandle, UiNode, UserInterface, + }, + }, + message::MessageSender, + Message, +}; +use std::path::Path; + +/// Context menu for scene tab headers. +pub struct SceneTabContextMenu { + /// The context menu popup handle. + pub menu: RcUiNodeHandle, + /// "Show in Explorer" menu item. + pub show_in_explorer: Handle, + /// "Close" menu item. + pub close: Handle, + /// "Close All Tabs" menu item. + pub close_all: Handle, + /// The UUID of the scene that was right-clicked. + pub target_scene_id: Option, +} + +impl SceneTabContextMenu { + /// Creates a new scene tab context menu. + pub fn new(ctx: &mut BuildContext) -> Self { + fn item(text: &str, ctx: &mut BuildContext) -> Handle { + MenuItemBuilder::new(WidgetBuilder::new()) + .with_content(MenuItemContent::text(text)) + .build(ctx) + } + + let show_in_explorer = item("Show In Explorer", ctx); + let close = item("Close", ctx); + let close_all = item("Close All Tabs", ctx); + + let menu = ContextMenuBuilder::new( + PopupBuilder::new(WidgetBuilder::new()) + .with_content( + StackPanelBuilder::new( + WidgetBuilder::new() + .with_child(show_in_explorer) + .with_child(close) + .with_child(close_all), + ) + .build(ctx), + ) + .with_restrict_picking(false), + ) + .build(ctx); + let menu = RcUiNodeHandle::new(menu, ctx.sender()); + + Self { + menu, + show_in_explorer, + close, + close_all, + target_scene_id: None, + } + } + + /// Handles UI messages for the context menu. + pub fn handle_ui_message( + &mut self, + message: &UiMessage, + sender: &MessageSender, + scene_ids: &[Uuid], + scene_paths: &[Option<&Path>], + ui: &UserInterface, + ) { + if let Some(PopupMessage::Placement(Placement::Cursor(target))) = message.data() { + if message.destination() == self.menu.handle() { + // Get the scene ID from the user data of the target widget + if let Some(uuid) = ui.node(*target).user_data_cloned::() { + self.target_scene_id = Some(uuid); + + // Enable/disable "Show in Explorer" based on whether the scene has a path + let has_path = scene_ids + .iter() + .zip(scene_paths.iter()) + .find(|(id, _)| **id == uuid) + .map(|(_, path)| path.is_some()) + .unwrap_or(false); + + ui.send( + self.show_in_explorer, + fyrox::gui::widget::WidgetMessage::Enabled(has_path), + ); + } + } + } else if let Some(MenuItemMessage::Click) = message.data() { + if let Some(target_id) = self.target_scene_id { + if message.destination() == self.show_in_explorer { + // Find the path for this scene + if let Some(path) = scene_ids + .iter() + .zip(scene_paths.iter()) + .find(|(id, _)| **id == target_id) + .and_then(|(_, path)| *path) + { + if let Ok(canonical_path) = path.canonicalize() { + show_in_explorer(canonical_path); + } + } + } else if message.destination() == self.close { + sender.send(Message::CloseScene(target_id)); + } else if message.destination() == self.close_all { + // Close all scenes + for &id in scene_ids { + sender.send(Message::CloseScene(id)); + } + } + } + } + } +} + +/// Opens the file explorer and highlights the specified path. +fn show_in_explorer>(path: P) { + #[cfg(target_os = "windows")] + { + use std::process::Command; + + fn execute_command(command: &mut Command) { + match command.spawn() { + Ok(mut process) => { + let _ = process.wait(); + } + Err(err) => { + fyrox::core::log::Log::err(format!( + "Failed to show in explorer. Reason: {err:?}" + )); + } + } + } + + let path = path.as_ref(); + if path.is_dir() { + execute_command(Command::new("explorer").arg(path)); + } else if let Some(parent) = path.parent() { + execute_command( + Command::new("explorer") + .arg("/select,") + .arg(path.as_os_str()), + ); + } + } + + #[cfg(target_os = "macos")] + { + let path = path.as_ref(); + if path.is_dir() { + let _ = std::process::Command::new("open").arg(path).spawn(); + } else { + let _ = std::process::Command::new("open") + .args(["-R", &path.to_string_lossy()]) + .spawn(); + } + } + + #[cfg(target_os = "linux")] + { + // Try to use xdg-open for the parent directory + let path = path.as_ref(); + let target = if path.is_dir() { + path.to_path_buf() + } else { + path.parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| path.to_path_buf()) + }; + let _ = std::process::Command::new("xdg-open").arg(target).spawn(); + } +} diff --git a/fyrox-ui/src/color/mod.rs b/fyrox-ui/src/color/mod.rs index bb3a8010f..4180c8412 100644 --- a/fyrox-ui/src/color/mod.rs +++ b/fyrox-ui/src/color/mod.rs @@ -45,10 +45,7 @@ use fyrox_core::uuid_provider; use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor}; use fyrox_graph::BaseSceneGraph; use fyrox_material::MaterialResource; -use std::{ - ops::Deref, - sync::mpsc::Sender, -}; +use std::{ops::Deref, sync::mpsc::Sender}; pub mod gradient; diff --git a/fyrox-ui/src/inspector/editors/array.rs b/fyrox-ui/src/inspector/editors/array.rs index dd60e2162..81269871d 100644 --- a/fyrox-ui/src/inspector/editors/array.rs +++ b/fyrox-ui/src/inspector/editors/array.rs @@ -42,10 +42,7 @@ use crate::{ use crate::message::{DeliveryMode, MessageData}; use fyrox_graph::BaseSceneGraph; use std::sync::Arc; -use std::{ - any::TypeId, - fmt::Debug, -}; +use std::{any::TypeId, fmt::Debug}; #[derive(Clone, Debug, PartialEq, Visit, Reflect, Default)] pub struct Item { diff --git a/project-manager/src/main.rs b/project-manager/src/main.rs index 771e541dd..3d288a459 100644 --- a/project-manager/src/main.rs +++ b/project-manager/src/main.rs @@ -111,23 +111,17 @@ fn main() { let mut previous = Instant::now(); - #[allow(unused_assignments)] - let mut time_step = 1.0 / 60.0; - event_loop .run(move |event, active_event_loop| { if project_manager.mode.is_build() { // Keep updating with reduced rate to keep printing to the build log, but do not // eat as much time as in normal update mode. - time_step = 1.0 / 10.0; - active_event_loop.set_control_flow(ControlFlow::wait_duration( - Duration::from_secs_f32(time_step), + Duration::from_secs_f32(1.0 / 10.0), )); } else { // Wait for an event. active_event_loop.set_control_flow(ControlFlow::Wait); - time_step = 1.0 / 60.0; } match event {