From 4724d987e9a3f388c70f12040679c4b3a8e11936 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Sun, 15 Dec 2024 22:42:53 +0300 Subject: [PATCH] added settings window in project manager + `open in ide` ability - `open in ide` uses configurable shell command --- editor/src/lib.rs | 4 +- editor/src/plugins/settings.rs | 6 +- fyrox-build-tools/src/lib.rs | 30 +++--- project-manager/resources/gear.png | Bin 0 -> 2902 bytes project-manager/src/manager.rs | 85 +++++++++++++-- project-manager/src/settings.rs | 159 ++++++++++++++++++++++++++++- 6 files changed, 251 insertions(+), 33 deletions(-) create mode 100644 project-manager/resources/gear.png diff --git a/editor/src/lib.rs b/editor/src/lib.rs index ea52cd538..63fee3aae 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -170,7 +170,7 @@ use crate::{ world::{graph::menu::SceneNodeContextMenu, graph::EditorSceneWrapper, WorldViewer}, }; use fyrox::gui::log::LogPanel; -use fyrox_build_tools::BuildCommand; +use fyrox_build_tools::CommandDescriptor; pub use message::Message; use plugins::inspector::InspectorPlugin; use std::process::Stdio; @@ -303,7 +303,7 @@ pub fn make_save_file_selector( pub enum Mode { Edit, Build { - queue: VecDeque, + queue: VecDeque, process: Option, }, Play { diff --git a/editor/src/plugins/settings.rs b/editor/src/plugins/settings.rs index b74c7fd59..ef6758ca9 100644 --- a/editor/src/plugins/settings.rs +++ b/editor/src/plugins/settings.rs @@ -67,7 +67,7 @@ use crate::{ }, Editor, MSG_SYNC_FLAG, }; -use fyrox_build_tools::{BuildCommand, BuildProfile, EnvironmentVariable}; +use fyrox_build_tools::{BuildProfile, CommandDescriptor, EnvironmentVariable}; use std::sync::Arc; fn make_property_editors_container( @@ -100,8 +100,8 @@ fn make_property_editors_container( container.insert(InspectablePropertyEditorDefinition::::new()); container.insert(VecCollectionPropertyEditorDefinition::::new()); container.insert(InspectablePropertyEditorDefinition::::new()); - container.insert(VecCollectionPropertyEditorDefinition::::new()); - container.insert(InspectablePropertyEditorDefinition::::new()); + container.insert(VecCollectionPropertyEditorDefinition::::new()); + container.insert(InspectablePropertyEditorDefinition::::new()); container.insert(HotKeyPropertyEditorDefinition); Arc::new(container) } diff --git a/fyrox-build-tools/src/lib.rs b/fyrox-build-tools/src/lib.rs index eec064308..6b39bece6 100644 --- a/fyrox-build-tools/src/lib.rs +++ b/fyrox-build-tools/src/lib.rs @@ -32,13 +32,13 @@ pub struct EnvironmentVariable { #[derive(Deserialize, Serialize, PartialEq, Clone, Debug, Default, Reflect, TypeUuidProvider)] #[type_uuid(id = "67b93136-17fe-4776-b5f0-f4a9ef3d8972")] -pub struct BuildCommand { +pub struct CommandDescriptor { pub command: String, pub args: Vec, pub environment_variables: Vec, } -impl BuildCommand { +impl CommandDescriptor { pub fn make_command(&self) -> std::process::Command { let mut command = std::process::Command::new(&self.command); @@ -52,7 +52,7 @@ impl BuildCommand { } } -impl Display for BuildCommand { +impl Display for CommandDescriptor { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for var in self.environment_variables.iter() { write!(f, "{}=\"{}\" ", var.name, var.value)?; @@ -73,11 +73,11 @@ impl Display for BuildCommand { pub struct BuildProfile { pub name: String, #[reflect(description = "A set of commands that will be used to build your game.")] - pub build_commands: Vec, + pub build_commands: Vec, #[reflect(description = "A set of commands that will be used to run your game. \ This set of commands will be executed right after build commands (if the \ build was successful)")] - pub run_command: BuildCommand, + pub run_command: CommandDescriptor, } impl BuildProfile { @@ -91,7 +91,7 @@ impl BuildProfile { } } - pub fn build_and_run_queue(&self) -> VecDeque { + pub fn build_and_run_queue(&self) -> VecDeque { let mut queue = self.build_commands.iter().cloned().collect::>(); queue.push_back(self.run_command.clone()); queue @@ -100,7 +100,7 @@ impl BuildProfile { pub fn debug() -> Self { BuildProfile { name: "Debug".to_string(), - build_commands: vec![BuildCommand { + build_commands: vec![CommandDescriptor { command: "cargo".to_string(), args: vec![ "build".to_string(), @@ -109,7 +109,7 @@ impl BuildProfile { ], environment_variables: vec![], }], - run_command: BuildCommand { + run_command: CommandDescriptor { command: "cargo".to_string(), args: vec![ "run".to_string(), @@ -133,7 +133,7 @@ impl BuildProfile { name: "Debug (HR)".to_string(), build_commands: vec![ Self::build_game(), // Build the executor. - BuildCommand { + CommandDescriptor { command: "cargo".to_string(), args: vec![ "build".to_string(), @@ -163,9 +163,9 @@ impl BuildProfile { release_hot_reloading } - fn build_game() -> BuildCommand { + fn build_game() -> CommandDescriptor { // Build the game plugin DLL first. - BuildCommand { + CommandDescriptor { command: "cargo".to_string(), args: vec![ "build".to_string(), @@ -184,8 +184,8 @@ impl BuildProfile { } } - fn run_hot_reload(package_name: &str) -> BuildCommand { - BuildCommand { + fn run_hot_reload(package_name: &str) -> CommandDescriptor { + CommandDescriptor { command: "cargo".to_string(), args: vec![ "run".to_string(), @@ -222,7 +222,7 @@ impl BuildProfile { pub fn debug_editor() -> Self { BuildProfile { name: "Debug Editor".to_string(), - build_commands: vec![BuildCommand { + build_commands: vec![CommandDescriptor { command: "cargo".to_string(), args: vec![ "build".to_string(), @@ -231,7 +231,7 @@ impl BuildProfile { ], environment_variables: vec![], }], - run_command: BuildCommand { + run_command: CommandDescriptor { command: "cargo".to_string(), args: vec![ "run".to_string(), diff --git a/project-manager/resources/gear.png b/project-manager/resources/gear.png new file mode 100644 index 0000000000000000000000000000000000000000..1592698c67d666a5adffc7d646c17b565b1a9b62 GIT binary patch literal 2902 zcmV-c3#s&pP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGh)&Kwv)&Y=jd7Jq*1^;R#@M}+vILRCnJF@gvWh!38W_*4~3c`+IjG~z=| zyd-jqF@d8bB9u!Dg>XqRCaok~T7qJMQtI!&*6gy|)7`Vz+}5|I^Gp6&d#!W!T-JPh zX4cGnvlf-g?%lgrq7CQ`H^qztNi7)mpM9Bp(_{eaR8AV(WrR6n4Y zJ^=0i5ggS1zoWU3j-BWj`T?)Dr^BYC8sMWSb>WM{(Z|}4!>1$z$XPWP(+p&30;rxp z$l-J7Ep!*v=f8A4Cd4tvAy)_OUh)vt2uS~l94gA^Jop)iDDAf2!h7}NQ}Z*gRW^V#OHrV!@QNZ z`3<^kW@bjy(?J9oK!(DvOeTsEcpE@1d=ms4ftLZ)!Z$^b5%`Y6eGk>bw}G5ie%yZS zPUjkcU;halwC-Nhq7@tLMjxUUc(Og69-kUObK$k{ZJ{5s*X*Q^0qk(ciY@fG+!>!H zK=ajV0*-4gR4kw$ z7z22%J?)0qV<3^~ne!^7-e;v>zT;c?!gm}%MsOd8U$tkIp16&2xZQpB!m9}w;l>1> zLMhy_D+fludR6^u0=)6A0R)jh&tbMbt7K+%XyJ>X8bG)X;e%+eg%Fjb1a&fnfyd5PrQqGsQFLZgdZN z6m90_y}@xF&W_z{5}Eh1G9WHvCkic`=bY|lg)Z2<- z#qH9N3oSXf`?yWwx3l!bLJT3e8?Q&29Coo~5l&7d^3NlT9I@lqFU zFhN7X`=H099BxaqJO=&LeKvzjQ;uPA-Q!E!kA}c1pKRpl+(?y218ZfehZMN{Bvl12uhZRp*gIuhwwqpg*PLM>-sdE>*euc0he}7OIt%_eY475 zTf1n!{3Fd}dz{5h44TjH8}6^h?jLE-%&}1hpw%%JojsqS@U_M4;Nc!W&B5BrBC%6L z!B#zewT!D2tmftLm~#QK(Hg!|Fq;?42A~C;rC>HM>H}Ex@6&VZk@NxfyLxwMaEmo! zs^@NVOU|v<09#8beBtt<5lwSgHGpE?%QuF}JXtk>qF&JKFqtRIoxn;4tQvrP^-9BJ zo~#;x95qYBWS;6{Uxjn?vo3OWWn`SyJq|n~{0F)4y&Km7b`ZL`8*!{kBPB9_lu6 z)*x|bqMYyeuNy^Lg9P5uBsCxEd>$-Y#opOV6pWNqQ zVjI934%fD4W+?5IG5en$!cxluRFn}m;4~b#`6u!dI zFH93dmGo%hNAC{W+@AUqs}pz$s)e5q&BSVFw_t*60Dk>eI(`Qu!5zXFHNXw17Jfgh z<43u@J?)0K0TAIf4w^BmM$jKB{4Y@!-YbP-3+fzS+4M0>Q$WD~P7a@IsJ&=&+IMn7~J}I`~Q{jI(M&XMn1|Sj{!OKW< z4H|L03t8OLa9@t5R3)?;^Mc)~|J8iQt+`=D01`cEd4WOv$hA zcoby-Y8=9xL%Eo9(ME-Em!PcPN72Rsrh(_zSJ4Y7#q+#2OUgx(xVSH`=>E9VEMzr+ z#Au7OT;z-^DbcIqzM$%qN3fi?, + queue: VecDeque, process: Option, current_dir: PathBuf, }, @@ -94,7 +95,10 @@ pub struct ProjectManager { deletion_confirmation_dialog: Handle, upgrade: Handle, locate: Handle, + open_settings: Handle, + open_ide: Handle, upgrade_tool: Option, + settings_window: Option, } fn make_project_item( @@ -294,9 +298,18 @@ impl ProjectManager { .with_height(25.0), ) .build(ctx); + let open_settings = make_image_button_with_tooltip( + ctx, + 18.0, + 18.0, + load_image(include_bytes!("../resources/gear.png")), + "Settings\nHotkey: Ctrl+S", + Some(7), + ); + ctx[open_settings].set_column(3); let message_count; - let open_log = ButtonBuilder::new(WidgetBuilder::new().on_column(3).with_visibility(false)) + let open_log = ButtonBuilder::new(WidgetBuilder::new().on_column(4).with_visibility(false)) .with_content( GridBuilder::new( WidgetBuilder::new() @@ -341,12 +354,14 @@ impl ProjectManager { .with_child(create) .with_child(import) .with_child(search_bar) + .with_child(open_settings) .with_child(open_log), ) .add_column(Column::auto()) .add_column(Column::auto()) .add_column(Column::stretch()) .add_column(Column::auto()) + .add_column(Column::auto()) .add_row(Row::auto()) .build(ctx); @@ -362,13 +377,25 @@ impl ProjectManager { experimental and unsafe nature of code hot reloading.\ \nHotkey: Ctrl+H"; let locate_tooltip = "Opens project folder in the default OS file manager.\ - \nHotkey: Ctrl+O"; + \nHotkey: Ctrl+L"; + let open_ide_tooltip = "Opens project folder in the currently selected IDE \ + (can be changed in settings).\nHotkey: Ctrl+O"; let edit = make_button("Edit", 130.0, 25.0, 5, 0, 0, Some(edit_tooltip), ctx); let run = make_button("Run", 130.0, 25.0, 6, 0, 0, Some(run_tooltip), ctx); let delete = make_button("Delete", 130.0, 25.0, 7, 0, 0, Some(delete_tooltip), ctx); let upgrade = make_button("Upgrade", 130.0, 25.0, 8, 0, 0, Some(upgrade_tooltip), ctx); let locate = make_button("Locate", 130.0, 25.0, 9, 0, 0, Some(locate_tooltip), ctx); + let open_ide = make_button( + "Open IDE", + 130.0, + 25.0, + 9, + 0, + 0, + Some(open_ide_tooltip), + ctx, + ); let hot_reload = CheckBoxBuilder::new( WidgetBuilder::new() .with_tab_index(Some(4)) @@ -392,7 +419,8 @@ impl ProjectManager { .with_child(run) .with_child(delete) .with_child(upgrade) - .with_child(locate), + .with_child(locate) + .with_child(open_ide), ) .build(ctx); @@ -468,7 +496,10 @@ impl ProjectManager { deletion_confirmation_dialog: Default::default(), upgrade, locate, + open_settings, + open_ide, upgrade_tool: None, + settings_window: None, } } @@ -559,6 +590,8 @@ impl ProjectManager { if let Some(build_window) = self.build_window.as_mut() { build_window.update(ui); } + + self.settings.try_save(); } fn try_import(&mut self, path: &Path, ui: &mut UserInterface) { @@ -689,6 +722,10 @@ impl ProjectManager { self.on_delete_clicked(ui); } else if button == self.locate { self.on_locate_click(); + } else if button == self.open_ide { + self.on_open_ide_click(); + } else if button == self.open_settings { + self.on_open_settings_click(ui); } } @@ -698,6 +735,32 @@ impl ProjectManager { Log::verify(open::that_detached(folder)); } + fn on_open_ide_click(&mut self) { + let project = some_or_return!(self.selection.and_then(|i| self.settings.projects.get(i))); + let mut open_ide_command = self.settings.open_ide_command.clone(); + if let Some(manifest_path_arg) = open_ide_command + .args + .iter_mut() + .find(|cmd| cmd.as_str() == MANIFEST_PATH_VAR) + { + *manifest_path_arg = project.manifest_path.to_string_lossy().to_string(); + } else { + Log::warn(format!("{} variable is not specified!", MANIFEST_PATH_VAR)); + } + let mut command = open_ide_command.make_command(); + if let Err(err) = command.spawn() { + Log::err(format!( + "Unable to open the IDE using {} command. Reason: {:?}", + open_ide_command, err + )); + } + } + + fn on_open_settings_click(&mut self, ui: &mut UserInterface) { + let ctx = &mut ui.build_ctx(); + self.settings_window = Some(SettingsWindow::new(&self.settings, ctx)); + } + fn run_build_profile( &mut self, name: &str, @@ -740,7 +803,9 @@ impl ProjectManager { KeyCode::KeyI if modifiers.control => self.on_import_clicked(ui), KeyCode::KeyC if modifiers.control => self.on_create_clicked(ui), KeyCode::KeyH if modifiers.control => self.on_hot_reload_changed(true, ui), - KeyCode::KeyO if modifiers.control => self.on_locate_click(), + KeyCode::KeyL if modifiers.control => self.on_locate_click(), + KeyCode::KeyO if modifiers.control => self.on_open_ide_click(), + KeyCode::KeyS if modifiers.control => self.on_open_settings_click(ui), _ => (), } } @@ -758,6 +823,10 @@ impl ProjectManager { build_window.handle_ui_message(message, ui); } + if let Some(settings_window) = self.settings_window.take() { + self.settings_window = settings_window.handle_ui_message(&mut self.settings, message); + } + if let Some(upgrade_tool) = self.upgrade_tool.take() { if let Some(index) = self.selection { if let Some(project) = self.settings.projects.get(index) { diff --git a/project-manager/src/settings.rs b/project-manager/src/settings.rs index 6b2d01361..fa5134aa5 100644 --- a/project-manager/src/settings.rs +++ b/project-manager/src/settings.rs @@ -18,16 +18,55 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +use fyrox::{ + core::{log::Log, pool::Handle, reflect::prelude::*}, + gui::{ + inspector::{ + editors::{ + collection::VecCollectionPropertyEditorDefinition, + inspectable::InspectablePropertyEditorDefinition, + PropertyEditorDefinitionContainer, + }, + InspectorBuilder, InspectorContext, + }, + inspector::{InspectorMessage, PropertyAction}, + message::{MessageDirection, UiMessage}, + scroll_viewer::ScrollViewerBuilder, + widget::WidgetBuilder, + window::{WindowBuilder, WindowMessage, WindowTitle}, + BuildContext, UiNode, + }, +}; +use fyrox_build_tools::{CommandDescriptor, EnvironmentVariable}; use ron::ser::PrettyConfig; use serde::{Deserialize, Serialize}; -use std::{fs::File, io::Write, path::PathBuf}; +use std::ops::{Deref, DerefMut}; +use std::{fs::File, io::Write, path::PathBuf, sync::Arc}; -#[derive(Default, Serialize, Deserialize)] -pub struct Settings { +pub const MANIFEST_PATH_VAR: &str = "%MANIFEST_PATH%"; + +#[derive(Default, Serialize, Deserialize, Reflect, Debug)] +pub struct SettingsData { + #[serde(default = "default_open_ide_command")] + #[reflect( + description = "Defines a command to run an IDE in a project folder. This command \ + should use %MANIFEST_PATH% built-in variable to provide the selected project path to the \ + chosen IDE." + )] + pub open_ide_command: CommandDescriptor, + #[reflect(hidden)] pub projects: Vec, } -impl Settings { +fn default_open_ide_command() -> CommandDescriptor { + CommandDescriptor { + command: "rustrover64".to_string(), + args: vec![MANIFEST_PATH_VAR.to_string()], + environment_variables: vec![], + } +} + +impl SettingsData { pub const PATH: &'static str = "pm_settings.ron"; pub fn load() -> Self { @@ -47,9 +86,119 @@ impl Settings { } } -#[derive(Serialize, Deserialize)] +pub struct Settings { + data: SettingsData, + need_save: bool, +} + +impl Deref for Settings { + type Target = SettingsData; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for Settings { + fn deref_mut(&mut self) -> &mut Self::Target { + self.need_save = true; + &mut self.data + } +} + +impl Settings { + pub fn load() -> Self { + Self { + data: SettingsData::load(), + need_save: false, + } + } + + pub fn try_save(&mut self) { + if self.need_save { + self.need_save = false; + self.data.save(); + } + } +} + +#[derive(Serialize, Deserialize, Debug)] pub struct Project { pub manifest_path: PathBuf, pub name: String, pub hot_reload: bool, } + +pub struct SettingsWindow { + window: Handle, + inspector: Handle, +} + +impl SettingsWindow { + pub fn new(settings: &Settings, ctx: &mut BuildContext) -> Self { + let property_editors = PropertyEditorDefinitionContainer::with_default_editors(); + property_editors.insert(InspectablePropertyEditorDefinition::::new()); + property_editors + .insert(VecCollectionPropertyEditorDefinition::::new()); + property_editors.insert(InspectablePropertyEditorDefinition::::new()); + let property_editors = Arc::new(property_editors); + let context = InspectorContext::from_object( + settings.deref(), + ctx, + property_editors, + None, + 1, + 0, + true, + Default::default(), + 200.0, + ); + let inspector = InspectorBuilder::new(WidgetBuilder::new()) + .with_context(context) + .build(ctx); + + let window = WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(400.0)) + .with_content( + ScrollViewerBuilder::new(WidgetBuilder::new()) + .with_content(inspector) + .build(ctx), + ) + .open(false) + .with_title(WindowTitle::text("Settings")) + .with_remove_on_close(true) + .build(ctx); + + ctx.sender() + .send(WindowMessage::open_modal( + window, + MessageDirection::ToWidget, + true, + true, + )) + .unwrap(); + + Self { window, inspector } + } + + pub fn handle_ui_message(self, settings: &mut Settings, message: &UiMessage) -> Option { + if let Some(WindowMessage::Close) = message.data() { + if message.destination() == self.window { + return None; + } + } else if let Some(InspectorMessage::PropertyChanged(args)) = message.data() { + if message.destination() == self.inspector + && message.direction() == MessageDirection::FromWidget + { + PropertyAction::from_field_kind(&args.value).apply( + &args.path(), + settings.deref_mut(), + &mut |result| { + Log::verify(result); + }, + ); + } + } + + Some(self) + } +}