From d1e377d7bd412c9f54774d07a0e7183cfdf14f85 Mon Sep 17 00:00:00 2001 From: Mathew O'Dwyer Date: Thu, 3 Aug 2023 23:14:12 +1000 Subject: [PATCH 1/7] [WIP] Editor From f0140e1939ef5949cc60ea366275b10517a75009 Mon Sep 17 00:00:00 2001 From: Software Antics <50978201+softwareantics@users.noreply.github.com> Date: Fri, 4 Aug 2023 02:06:02 +1000 Subject: [PATCH 2/7] [WIP] [FEATURE] Create WPF Main Window (#226) * [WIP] [FEATURE REQUEST] Create WPF Main Window * Added MainView, MainViewModel and an ApplicationContext service to fill the requirements of #222. * Added unit tests and documentation. --- .../FinalEngine.Editor.Common.csproj | 31 +++++ .../Properties/AssemblyInfo.cs | 13 ++ .../Application/ApplicationContext.cs | 27 ++++ .../Application/IApplicationContext.cs | 29 +++++ FinalEngine.Editor.Desktop/App.xaml | 14 ++ FinalEngine.Editor.Desktop/App.xaml.cs | 92 ++++++++++++++ .../FinalEngine.Editor.Desktop.csproj | 40 ++++++ .../Properties/AssemblyInfo.cs | 15 +++ .../Views/MainView.xaml | 39 ++++++ .../Views/MainView.xaml.cs | 22 ++++ .../FinalEngine.Editor.ViewModels.csproj | 37 ++++++ .../GlobalSuppressions.cs | 7 + .../IMainViewModel.cs | 29 +++++ .../Interactions/ICloseable.cs | 16 +++ .../MainViewModel.cs | 106 ++++++++++++++++ .../Properties/AssemblyInfo.cs | 13 ++ .../Application/ApplicationContextTests.cs | 57 +++++++++ .../Editor/ViewModels/MainViewModelTests.cs | 120 ++++++++++++++++++ FinalEngine.Tests/FinalEngine.Tests.csproj | 2 + FinalEngine.sln | 25 +++- 20 files changed, 733 insertions(+), 1 deletion(-) create mode 100644 FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj create mode 100644 FinalEngine.Editor.Common/Properties/AssemblyInfo.cs create mode 100644 FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs create mode 100644 FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs create mode 100644 FinalEngine.Editor.Desktop/App.xaml create mode 100644 FinalEngine.Editor.Desktop/App.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj create mode 100644 FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs create mode 100644 FinalEngine.Editor.Desktop/Views/MainView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/MainView.xaml.cs create mode 100644 FinalEngine.Editor.ViewModels/FinalEngine.Editor.ViewModels.csproj create mode 100644 FinalEngine.Editor.ViewModels/GlobalSuppressions.cs create mode 100644 FinalEngine.Editor.ViewModels/IMainViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Interactions/ICloseable.cs create mode 100644 FinalEngine.Editor.ViewModels/MainViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Properties/AssemblyInfo.cs create mode 100644 FinalEngine.Tests/Editor/Common/Services/Application/ApplicationContextTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/MainViewModelTests.cs diff --git a/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj b/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj new file mode 100644 index 00000000..0c106d7c --- /dev/null +++ b/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + 11.0 + enable + All + false + true + x64 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/FinalEngine.Editor.Common/Properties/AssemblyInfo.cs b/FinalEngine.Editor.Common/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..18cf0906 --- /dev/null +++ b/FinalEngine.Editor.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: CLSCompliant(true)] +[assembly: ComVisible(false)] +[assembly: AssemblyTitle("FinalEngine.Editor.Common")] +[assembly: AssemblyDescription("A common library containing services to connect view models to view for the Final Engine editor.")] +[assembly: Guid("3D606010-8CAF-4781-98D1-EB67BCDD8CC1")] diff --git a/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs b/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs new file mode 100644 index 00000000..1b195bc7 --- /dev/null +++ b/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Services.Application; + +using System; +using System.Reflection; + +/// +/// Provides a standard implementation of an . +/// +/// +public sealed class ApplicationContext : IApplicationContext +{ + /// + public string Title + { + get { return $"Final Engine - {this.Version}"; } + } + + /// + public Version Version + { + get { return Assembly.GetExecutingAssembly().GetName().Version!; } + } +} diff --git a/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs b/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs new file mode 100644 index 00000000..45f8e68e --- /dev/null +++ b/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Services.Application; + +using System; + +/// +/// Defines an interface that represents contextual information related to the current application. +/// +public interface IApplicationContext +{ + /// + /// Gets the title of the application. + /// + /// + /// The title of the application. + /// + string Title { get; } + + /// + /// Gets the version of the application. + /// + /// + /// The version of the application. + /// + Version Version { get; } +} diff --git a/FinalEngine.Editor.Desktop/App.xaml b/FinalEngine.Editor.Desktop/App.xaml new file mode 100644 index 00000000..412083d9 --- /dev/null +++ b/FinalEngine.Editor.Desktop/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/FinalEngine.Editor.Desktop/App.xaml.cs b/FinalEngine.Editor.Desktop/App.xaml.cs new file mode 100644 index 00000000..40752e62 --- /dev/null +++ b/FinalEngine.Editor.Desktop/App.xaml.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop; + +using System.Diagnostics; +using System.Windows; +using FinalEngine.Editor.Common.Services.Application; +using FinalEngine.Editor.Desktop.Views; +using FinalEngine.Editor.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +/// +/// Interaction logic for App.xaml. +/// +public partial class App : Application +{ + /// + /// Initializes a new instance of the class. + /// + public App() + { + AppHost = Host.CreateDefaultBuilder() + .ConfigureServices(ConfigureServices) + .Build(); + } + + /// + /// Gets or sets the application host. + /// + /// + /// The application host. + /// + private static IHost? AppHost { get; set; } + + /// + /// Exits the main application, disposing of any existing resources. + /// + /// + /// The instance containing the event data. + /// + protected override async void OnExit(ExitEventArgs e) + { + await AppHost!.StopAsync().ConfigureAwait(false); + base.OnExit(e); + } + + /// + /// Starts up the main application on the current platform. + /// + /// + /// A that contains the event data. + /// + protected override async void OnStartup(StartupEventArgs e) + { + await AppHost!.StartAsync().ConfigureAwait(false); + + var viewModel = AppHost.Services.GetRequiredService(); + + var view = new MainView() + { + DataContext = viewModel, + }; + + view.ShowDialog(); + + base.OnStartup(e); + } + + /// + /// Configures the services to be consumed by the application. + /// + /// + /// The application host builder context. + /// + /// + /// The services to be consumed by the application. + /// + private static void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole().SetMinimumLevel(Debugger.IsAttached ? LogLevel.Debug : LogLevel.Information); + }); + + services.AddSingleton(); + services.AddTransient(); + } +} diff --git a/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj b/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj new file mode 100644 index 00000000..e43a46dd --- /dev/null +++ b/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj @@ -0,0 +1,40 @@ + + + + WinExe + net7.0-windows + enable + true + 11.0 + All + false + true + x64 + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + \ No newline at end of file diff --git a/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs b/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7d52a865 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows; + +[assembly: CLSCompliant(false)] +[assembly: ComVisible(false)] +[assembly: AssemblyTitle("FinalEngine.Editor.Desktop")] +[assembly: AssemblyDescription("The main editor application for Final Engine")] +[assembly: Guid("18F55601-F1DE-4260-88A8-2F54CA08CA93")] +[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] diff --git a/FinalEngine.Editor.Desktop/Views/MainView.xaml b/FinalEngine.Editor.Desktop/Views/MainView.xaml new file mode 100644 index 00000000..6f6fc888 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Views/MainView.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FinalEngine.Editor.Desktop/Views/MainView.xaml.cs b/FinalEngine.Editor.Desktop/Views/MainView.xaml.cs new file mode 100644 index 00000000..7cdb0b31 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Views/MainView.xaml.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Views; + +using FinalEngine.Editor.ViewModels.Interactions; +using MahApps.Metro.Controls; + +/// +/// Interaction logic for MainView.xaml. +/// +public partial class MainView : MetroWindow, ICloseable +{ + /// + /// Initializes a new instance of the class. + /// + public MainView() + { + this.InitializeComponent(); + } +} diff --git a/FinalEngine.Editor.ViewModels/FinalEngine.Editor.ViewModels.csproj b/FinalEngine.Editor.ViewModels/FinalEngine.Editor.ViewModels.csproj new file mode 100644 index 00000000..db59f6ba --- /dev/null +++ b/FinalEngine.Editor.ViewModels/FinalEngine.Editor.ViewModels.csproj @@ -0,0 +1,37 @@ + + + + net7.0 + 11.0 + enable + All + false + true + x64 + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/FinalEngine.Editor.ViewModels/GlobalSuppressions.cs b/FinalEngine.Editor.ViewModels/GlobalSuppressions.cs new file mode 100644 index 00000000..fe08196f --- /dev/null +++ b/FinalEngine.Editor.ViewModels/GlobalSuppressions.cs @@ -0,0 +1,7 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "KISS")] diff --git a/FinalEngine.Editor.ViewModels/IMainViewModel.cs b/FinalEngine.Editor.ViewModels/IMainViewModel.cs new file mode 100644 index 00000000..6b87e69b --- /dev/null +++ b/FinalEngine.Editor.ViewModels/IMainViewModel.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.ViewModels; + +using System.Windows.Input; + +/// +/// Defines an interface that represents a model of the main view. +/// +public interface IMainViewModel +{ + /// + /// Gets the exit command. + /// + /// + /// The exit command. + /// + ICommand ExitCommand { get; } + + /// + /// Gets the title. + /// + /// + /// The title. + /// + string Title { get; } +} diff --git a/FinalEngine.Editor.ViewModels/Interactions/ICloseable.cs b/FinalEngine.Editor.ViewModels/Interactions/ICloseable.cs new file mode 100644 index 00000000..39d9d9ec --- /dev/null +++ b/FinalEngine.Editor.ViewModels/Interactions/ICloseable.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.ViewModels.Interactions; + +/// +/// Defines an interface that represents an interaction to close a view. +/// +public interface ICloseable +{ + /// + /// Closes the view. + /// + void Close(); +} diff --git a/FinalEngine.Editor.ViewModels/MainViewModel.cs b/FinalEngine.Editor.ViewModels/MainViewModel.cs new file mode 100644 index 00000000..04474703 --- /dev/null +++ b/FinalEngine.Editor.ViewModels/MainViewModel.cs @@ -0,0 +1,106 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.ViewModels; + +using System; +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using FinalEngine.Editor.Common.Services.Application; +using FinalEngine.Editor.ViewModels.Interactions; +using Microsoft.Extensions.Logging; + +/// +/// Provides a standard implementation of an . +/// +/// +/// +public sealed class MainViewModel : ObservableObject, IMainViewModel +{ + /// + /// The logger. + /// + private readonly ILogger logger; + + /// + /// The exit command. + /// + private ICommand? exitCommand; + + /// + /// The title of the application. + /// + private string? title; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The logger. + /// + /// + /// The application context. + /// + /// + /// The specified or parameter cannot be null. + /// + public MainViewModel( + ILogger logger, + IApplicationContext context) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + this.Title = context.Title; + } + + /// + /// Gets the exit command. + /// + /// + /// The exit command. + /// + public ICommand ExitCommand + { + get { return this.exitCommand ??= new RelayCommand(this.Close); } + } + + /// + /// Gets the title of the application. + /// + /// + /// The title of the application. + /// + public string Title + { + get { return this.title ?? string.Empty; } + private set { this.SetProperty(ref this.title, value); } + } + + /// + /// Closes the main view using the specified interaction. + /// + /// + /// The closeable interaction used to close the main view. + /// . + /// + /// The specified parameter cannot be null. + /// + private void Close(ICloseable? closeable) + { + if (closeable == null) + { + throw new ArgumentNullException(nameof(closeable)); + } + + this.logger.LogDebug($"Closing {nameof(MainViewModel)}..."); + + closeable.Close(); + } +} diff --git a/FinalEngine.Editor.ViewModels/Properties/AssemblyInfo.cs b/FinalEngine.Editor.ViewModels/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8d81eb98 --- /dev/null +++ b/FinalEngine.Editor.ViewModels/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: CLSCompliant(false)] +[assembly: ComVisible(false)] +[assembly: AssemblyTitle("FinalEngine.Editor.ViewModels")] +[assembly: AssemblyDescription("A library containing view models for the Final Engine editor.")] +[assembly: Guid("BDA4CEFA-DD44-4FD7-AE54-330C571809C9")] diff --git a/FinalEngine.Tests/Editor/Common/Services/Application/ApplicationContextTests.cs b/FinalEngine.Tests/Editor/Common/Services/Application/ApplicationContextTests.cs new file mode 100644 index 00000000..09e0c5ca --- /dev/null +++ b/FinalEngine.Tests/Editor/Common/Services/Application/ApplicationContextTests.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Tests.Editor.Common.Services.Application; + +using System.Reflection; +using FinalEngine.Editor.Common.Services.Application; +using NUnit.Framework; + +[TestFixture] +public sealed class ApplicationContextTests +{ + private ApplicationContext context; + + [Test] + public void ConstructorShouldNotThrowExceptionWhenInvoked() + { + // Act and assert + Assert.DoesNotThrow(() => + { + new ApplicationContext(); + }); + } + + [SetUp] + public void Setup() + { + this.context = new ApplicationContext(); + } + + [Test] + public void TitleShouldReturnFinalEngineAndVersionWhenInvoked() + { + // Arrange + string expected = $"Final Engine - {Assembly.GetExecutingAssembly().GetName().Version}"; + + // Act + string actual = this.context.Title; + + // Assert + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void VersionShouldReturnAssemblyVersionWhenInvoked() + { + // Arrange + var expected = Assembly.GetExecutingAssembly().GetName().Version; + + // Act + var actual = this.context.Version; + + // Assert + Assert.That(actual, Is.EqualTo(expected)); + } +} diff --git a/FinalEngine.Tests/Editor/ViewModels/MainViewModelTests.cs b/FinalEngine.Tests/Editor/ViewModels/MainViewModelTests.cs new file mode 100644 index 00000000..67fe3117 --- /dev/null +++ b/FinalEngine.Tests/Editor/ViewModels/MainViewModelTests.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Tests.Editor.ViewModels; + +using System; +using CommunityToolkit.Mvvm.Input; +using FinalEngine.Editor.Common.Services.Application; +using FinalEngine.Editor.ViewModels; +using FinalEngine.Editor.ViewModels.Interactions; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +[TestFixture] +public sealed class MainViewModelTests +{ + private Mock context; + + private Mock> logger; + + private MainViewModel viewModel; + + [Test] + public void ConstructorShouldSetTitleToApplicationContextTitleWhenInvoked() + { + // Arrange + const string expected = "Final Engine"; + + // Act + string actual = this.viewModel.Title; + + // Assert + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void ConstructorShouldSetTitleToStringEmptyWhenApplicationContextTitleIsNull() + { + // Arrange + string expected = string.Empty; + this.context.Setup(x => x.Title).Returns(() => + { + return null; + }); + + var viewModel = new MainViewModel(this.logger.Object, this.context.Object); + + // Act + string actual = viewModel.Title; + + // Assert + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void ConstructorShouldThrowArgumentNullExceptionWhenContextIsNull() + { + // Act and assert + Assert.Throws(() => + { + new MainViewModel(this.logger.Object, null); + }); + } + + [Test] + public void ConstructorShouldThrowArgumentNullExceptionWhenLoggerIsNull() + { + // Act and assert + Assert.Throws(() => + { + new MainViewModel(null, this.context.Object); + }); + } + + [Test] + public void ExitCommandExecuteShouldInvokeCloseableCloseWhenInvoked() + { + // Arrange + var closeable = new Mock(); + + // Act + this.viewModel.ExitCommand.Execute(closeable.Object); + + // Assert + closeable.Verify(x => x.Close(), Times.Once); + } + + [Test] + public void ExitCommandExecuteShouldThrowArgumentNullExceptionWhenCloseableIsNull() + { + // Act and assert + Assert.Throws(() => + { + this.viewModel.ExitCommand.Execute(null); + }); + } + + [Test] + public void ExitCommandShouldReturnNewRelayCommandWhenInvoked() + { + // Act + var command = this.viewModel.ExitCommand; + + // Assert + Assert.That(command, Is.InstanceOf>()); + } + + [SetUp] + public void Setup() + { + this.logger = new Mock>(); + + this.context = new Mock(); + this.context.Setup(x => x.Title).Returns("Final Engine"); + + this.viewModel = new MainViewModel(this.logger.Object, this.context.Object); + } +} diff --git a/FinalEngine.Tests/FinalEngine.Tests.csproj b/FinalEngine.Tests/FinalEngine.Tests.csproj index 993ab3b6..155f40d9 100644 --- a/FinalEngine.Tests/FinalEngine.Tests.csproj +++ b/FinalEngine.Tests/FinalEngine.Tests.csproj @@ -40,6 +40,8 @@ + + diff --git a/FinalEngine.sln b/FinalEngine.sln index d068e713..4cd0e42e 100644 --- a/FinalEngine.sln +++ b/FinalEngine.sln @@ -62,7 +62,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Audio.OpenAL", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{C3EB22AD-1D4F-4270-B5D6-ABAFCC0558ED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Examples.Game", "FinalEngine.Examples.Game\FinalEngine.Examples.Game.csproj", "{5B492FC2-A4FD-4C2D-AD17-3F9699E5D112}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Examples.Game", "FinalEngine.Examples.Game\FinalEngine.Examples.Game.csproj", "{5B492FC2-A4FD-4C2D-AD17-3F9699E5D112}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Editor", "Editor", "{A000B9CF-02DF-4BC6-9F88-36D723041F73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Editor.Common", "FinalEngine.Editor.Common\FinalEngine.Editor.Common.csproj", "{AF7D33F8-7581-4B2C-83A1-B78370C0C094}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Editor.ViewModels", "FinalEngine.Editor.ViewModels\FinalEngine.Editor.ViewModels.csproj", "{F3F731DE-A676-40E5-9308-4FBA387483E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Editor.Desktop", "FinalEngine.Editor.Desktop\FinalEngine.Editor.Desktop.csproj", "{4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -142,6 +150,18 @@ Global {5B492FC2-A4FD-4C2D-AD17-3F9699E5D112}.Debug|x64.Build.0 = Debug|x64 {5B492FC2-A4FD-4C2D-AD17-3F9699E5D112}.Release|x64.ActiveCfg = Release|Any CPU {5B492FC2-A4FD-4C2D-AD17-3F9699E5D112}.Release|x64.Build.0 = Release|Any CPU + {AF7D33F8-7581-4B2C-83A1-B78370C0C094}.Debug|x64.ActiveCfg = Debug|x64 + {AF7D33F8-7581-4B2C-83A1-B78370C0C094}.Debug|x64.Build.0 = Debug|x64 + {AF7D33F8-7581-4B2C-83A1-B78370C0C094}.Release|x64.ActiveCfg = Release|Any CPU + {AF7D33F8-7581-4B2C-83A1-B78370C0C094}.Release|x64.Build.0 = Release|Any CPU + {F3F731DE-A676-40E5-9308-4FBA387483E8}.Debug|x64.ActiveCfg = Debug|x64 + {F3F731DE-A676-40E5-9308-4FBA387483E8}.Debug|x64.Build.0 = Debug|x64 + {F3F731DE-A676-40E5-9308-4FBA387483E8}.Release|x64.ActiveCfg = Release|Any CPU + {F3F731DE-A676-40E5-9308-4FBA387483E8}.Release|x64.Build.0 = Release|Any CPU + {4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}.Debug|x64.ActiveCfg = Debug|x64 + {4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}.Debug|x64.Build.0 = Debug|x64 + {4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}.Release|x64.ActiveCfg = Release|Any CPU + {4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -165,6 +185,9 @@ Global {E153C9B7-F8A5-43B6-B112-84739E7E9224} = {B4084D99-8DC5-4E1C-B5BF-9C7351CE71B8} {D67794A8-89AC-46E0-B303-94110BB382F8} = {B4084D99-8DC5-4E1C-B5BF-9C7351CE71B8} {5B492FC2-A4FD-4C2D-AD17-3F9699E5D112} = {C3EB22AD-1D4F-4270-B5D6-ABAFCC0558ED} + {AF7D33F8-7581-4B2C-83A1-B78370C0C094} = {A000B9CF-02DF-4BC6-9F88-36D723041F73} + {F3F731DE-A676-40E5-9308-4FBA387483E8} = {A000B9CF-02DF-4BC6-9F88-36D723041F73} + {4CFA5490-082F-46F5-9CBB-3A3318FCE5FC} = {A000B9CF-02DF-4BC6-9F88-36D723041F73} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A278B383-0D14-41B3-A71C-4505E1D503DF} From 68e9f2f286b4c4a152f3db5ae946f51539f7d66e Mon Sep 17 00:00:00 2001 From: Mathew O'Dwyer Date: Tue, 8 Aug 2023 20:30:59 +1000 Subject: [PATCH 3/7] Fixed merge issue --- FinalEngine.sln | 13 +++---------- SharedAssemblyInfo.cs | 4 ++-- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/FinalEngine.sln b/FinalEngine.sln index 4cd0e42e..cd0b9e0d 100644 --- a/FinalEngine.sln +++ b/FinalEngine.sln @@ -24,8 +24,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E9320D29 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Tests", "FinalEngine.Tests\FinalEngine.Tests.csproj", "{9835B6B5-0A5F-4645-82B6-3B34F0DA91E1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.IO", "FinalEngine.IO\FinalEngine.IO.csproj", "{533E347F-03B4-4C45-8F21-85EA20BF3448}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rendering", "Rendering", "{B2CD257E-E2BC-4259-A9B5-4DA90A7DE0B3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Rendering", "FinalEngine.Rendering\FinalEngine.Rendering.csproj", "{0A41C3A7-1881-43F8-BE83-8B35D6B04221}" @@ -66,11 +64,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Examples.Game", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Editor", "Editor", "{A000B9CF-02DF-4BC6-9F88-36D723041F73}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Editor.Common", "FinalEngine.Editor.Common\FinalEngine.Editor.Common.csproj", "{AF7D33F8-7581-4B2C-83A1-B78370C0C094}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Editor.Common", "FinalEngine.Editor.Common\FinalEngine.Editor.Common.csproj", "{AF7D33F8-7581-4B2C-83A1-B78370C0C094}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Editor.ViewModels", "FinalEngine.Editor.ViewModels\FinalEngine.Editor.ViewModels.csproj", "{F3F731DE-A676-40E5-9308-4FBA387483E8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Editor.ViewModels", "FinalEngine.Editor.ViewModels\FinalEngine.Editor.ViewModels.csproj", "{F3F731DE-A676-40E5-9308-4FBA387483E8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinalEngine.Editor.Desktop", "FinalEngine.Editor.Desktop\FinalEngine.Editor.Desktop.csproj", "{4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinalEngine.Editor.Desktop", "FinalEngine.Editor.Desktop\FinalEngine.Editor.Desktop.csproj", "{4CFA5490-082F-46F5-9CBB-3A3318FCE5FC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -94,10 +92,6 @@ Global {9835B6B5-0A5F-4645-82B6-3B34F0DA91E1}.Debug|x64.Build.0 = Debug|x64 {9835B6B5-0A5F-4645-82B6-3B34F0DA91E1}.Release|x64.ActiveCfg = Release|x64 {9835B6B5-0A5F-4645-82B6-3B34F0DA91E1}.Release|x64.Build.0 = Release|x64 - {533E347F-03B4-4C45-8F21-85EA20BF3448}.Debug|x64.ActiveCfg = Debug|x64 - {533E347F-03B4-4C45-8F21-85EA20BF3448}.Debug|x64.Build.0 = Debug|x64 - {533E347F-03B4-4C45-8F21-85EA20BF3448}.Release|x64.ActiveCfg = Release|x64 - {533E347F-03B4-4C45-8F21-85EA20BF3448}.Release|x64.Build.0 = Release|x64 {0A41C3A7-1881-43F8-BE83-8B35D6B04221}.Debug|x64.ActiveCfg = Debug|x64 {0A41C3A7-1881-43F8-BE83-8B35D6B04221}.Debug|x64.Build.0 = Debug|x64 {0A41C3A7-1881-43F8-BE83-8B35D6B04221}.Release|x64.ActiveCfg = Release|x64 @@ -171,7 +165,6 @@ Global {B123653E-BA9E-463C-B109-4C294385D74E} = {8500D452-62BA-4DAE-BA1D-5A51632955EC} {F22EC955-61B2-4021-88F2-BD889DED8716} = {198F3F27-340D-4617-86BB-AE9D9D5494A2} {9835B6B5-0A5F-4645-82B6-3B34F0DA91E1} = {E9320D29-CE2F-4A99-8F58-8E52E89E0147} - {533E347F-03B4-4C45-8F21-85EA20BF3448} = {198F3F27-340D-4617-86BB-AE9D9D5494A2} {0A41C3A7-1881-43F8-BE83-8B35D6B04221} = {B2CD257E-E2BC-4259-A9B5-4DA90A7DE0B3} {14B5098C-79C8-44BB-A485-6638873F10BC} = {B2CD257E-E2BC-4259-A9B5-4DA90A7DE0B3} {82A89B14-C583-4762-AAB1-F81E1C29E5D8} = {198F3F27-340D-4617-86BB-AE9D9D5494A2} diff --git a/SharedAssemblyInfo.cs b/SharedAssemblyInfo.cs index 390c9ec4..ba6da6e7 100644 --- a/SharedAssemblyInfo.cs +++ b/SharedAssemblyInfo.cs @@ -9,8 +9,8 @@ [assembly: AssemblyCopyright("© 2023 Software Antics")] [assembly: AssemblyTrademark("Software Antics™")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("2023.3.170.0442")] -[assembly: AssemblyFileVersion("2023.3.170.0442")] +[assembly: AssemblyVersion("2023.3.186.0442")] +[assembly: AssemblyFileVersion("2023.3.186.0442")] #if DEBUG [assembly: AssemblyConfiguration("Debug")] #else From fd657ddc611b8aee6acc7d6c4df94ec75ac266d5 Mon Sep 17 00:00:00 2001 From: Software Antics <50978201+softwareantics@users.noreply.github.com> Date: Sat, 12 Aug 2023 00:30:35 +1000 Subject: [PATCH 4/7] [FEATURE] Create Common Editor Dockable Tool Windows and Panes (#227) --- .../Extensions/ServiceCollectionExtensions.cs | 53 +++ .../FinalEngine.Editor.Common.csproj | 2 + .../Application/ApplicationContext.cs | 49 +++ .../Application/IApplicationContext.cs | 14 +- .../Environment/EnvironmentContext.cs | 22 + .../Environment/IEnvironmentContext.cs | 16 + .../Services/Factories/Factory.cs | 42 ++ .../Services/Factories/IFactory.cs | 22 + FinalEngine.Editor.Desktop/App.xaml | 7 + FinalEngine.Editor.Desktop/App.xaml.cs | 43 +- .../Layout/ToolPaneNotFoundException.cs | 71 ++++ .../Layout/WindowLayoutNotFoundException.cs | 71 ++++ .../FinalEngine.Editor.Desktop.csproj | 21 +- .../GlobalSuppressions.cs | 8 + .../Properties/AssemblyInfo.cs | 2 + .../Resources/Images/Splashes/splash.png | Bin 0 -> 26599 bytes .../Resources/Layouts/default.config | 39 ++ .../Docking/Panes/PaneTemplateSelector.cs | 93 ++++ .../Styles/Docking/Panes/PaneStyleSelector.cs | 54 +++ .../Services/Actions/UserActionRequester.cs | 50 +++ .../Factories/Layout/LayoutManagerFactory.cs | 75 ++++ .../Services/Layout/LayoutManager.cs | 250 +++++++++++ .../Styles/Controls/ButtonStyle.xaml | 14 + .../Styles/Controls/LabelStyle.xaml | 9 + .../Styles/Controls/ListBoxItemStyle.xaml | 7 + .../Styles/Controls/ListBoxStyle.xaml | 11 + .../Styles/Controls/TextBoxStyle.xaml | 20 + .../Styles/Docking/Panes/PaneStyle.xaml | 14 + .../Styles/Docking/Tools/ToolStyle.xaml | 18 + .../Layout/ManageWindowLayoutsView.xaml | 49 +++ .../Layout/ManageWindowLayoutsView.xaml.cs | 23 + .../Dialogs/Layout/SaveWindowLayoutView.xaml | 35 ++ .../Layout/SaveWindowLayoutView.xaml.cs | 23 + .../Views/Docking/DockView.xaml | 84 ++++ .../Views/Docking/DockView.xaml.cs | 39 ++ .../Views/Inspectors/ConsoleView.xaml | 11 + .../Views/Inspectors/ConsoleView.xaml.cs | 21 + .../Views/Inspectors/PropertiesView.xaml | 11 + .../Views/Inspectors/PropertiesView.xaml.cs | 21 + .../Views/MainView.xaml | 27 ++ .../Views/Projects/ProjectExplorerView.xaml | 11 + .../Projects/ProjectExplorerView.xaml.cs | 21 + .../Views/Scenes/EntitySystemsView.xaml | 11 + .../Views/Scenes/EntitySystemsView.xaml.cs | 21 + .../Views/Scenes/SceneHierarchyView.xaml | 11 + .../Views/Scenes/SceneHierarchyView.xaml.cs | 21 + .../Views/Scenes/SceneView.xaml | 11 + .../Views/Scenes/SceneView.xaml.cs | 21 + .../Layout/IManageWindowLayoutsViewModel.cs | 66 +++ .../Layout/ISaveWindowLayoutViewModel.cs | 40 ++ .../Layout/ManageWindowLayoutsViewModel.cs | 169 ++++++++ .../Layout/SaveWindowLayoutViewModel.cs | 158 +++++++ .../Docking/DockViewModel.cs | 192 +++++++++ .../Docking/IDockViewModel.cs | 60 +++ .../Docking/Panes/IPaneViewModel.cs | 43 ++ .../Docking/Panes/PaneViewModelBase.cs | 71 ++++ .../Panes/Scenes/ISceneViewPaneViewModel.cs | 13 + .../Panes/Scenes/SceneViewPaneViewModel.cs | 35 ++ .../Docking/Tools/IToolViewModel.cs | 22 + .../Tools/Inspectors/ConsoleToolViewModel.cs | 35 ++ .../Tools/Inspectors/IConsoleToolViewModel.cs | 13 + .../Inspectors/IPropertiesToolViewModel.cs | 13 + .../Inspectors/PropertiesToolViewModel.cs | 35 ++ .../Projects/IProjectExplorerToolViewModel.cs | 13 + .../Projects/ProjectExplorerToolViewModel.cs | 35 ++ .../Scenes/EntitySystemsToolViewModel.cs | 35 ++ .../Scenes/IEntitySystemsToolViewModel.cs | 13 + .../Scenes/ISceneHierarchyToolViewModel.cs | 13 + .../Scenes/SceneHierarchyToolViewModel.cs | 35 ++ .../Docking/Tools/ToolViewModelBase.cs | 35 ++ .../GlobalSuppressions.cs | 1 + .../IMainViewModel.cs | 56 +++ .../IViewPresenter.cs | 30 ++ .../Interactions/IViewable.cs | 33 ++ .../MainViewModel.cs | 160 +++++-- .../Services/Actions/IUserActionRequester.cs | 36 ++ .../Factories/Layout/ILayoutManagerFactory.cs | 21 + .../Services/Layout/ILayoutManager.cs | 69 +++ .../Validation/FileNameAttribute.cs | 80 ++++ .../ViewPresenter.cs | 85 ++++ .../FinalEngine.Rendering.csproj | 8 +- .../Resources/Mocks/MockDisposableResource.cs | 18 + .../Core/Resources/ResourceManagerTests.cs | 38 ++ .../Application/ApplicationContextTests.cs | 80 +++- .../Common/Services/Factories/FactoryTests.cs | 58 +++ .../ManageWindowLayoutsViewModelTests.cs | 284 +++++++++++++ .../Layouts/SaveWindowLayoutViewModelTests.cs | 256 +++++++++++ .../ViewModels/Docking/DockViewModelTests.cs | 398 ++++++++++++++++++ .../Docking/Panes/Mocks/MockPaneViewModel.cs | 11 + .../Docking/Panes/PaneViewModelBaseTests.cs | 116 +++++ .../Scenes/SceneViewPaneViewModelTests.cs | 61 +++ .../Inspectors/ConsoleToolViewModelTests.cs | 62 +++ .../PropertiesToolViewModelTests.cs | 62 +++ .../Docking/Tools/Mocks/MockToolViewModel.cs | 11 + .../ProjectExplorerToolViewModelTests.cs | 62 +++ .../Scenes/EntitySystemsToolViewModelTests.cs | 62 +++ .../SceneHierarchyToolViewModelTests.cs | 62 +++ .../Docking/Tools/ToolViewModelBaseTests.cs | 40 ++ .../Editor/ViewModels/MainViewModelTests.cs | 226 ++++++++-- .../Validation/FileNameAttributeTests.cs | 95 +++++ .../Editor/ViewModels/ViewPresenterTests.cs | 123 ++++++ FinalEngine.Tests/Properties/AssemblyInfo.cs | 2 +- FinalEngine.sln | 16 +- SharedAssemblyInfo.cs | 4 +- 104 files changed, 5353 insertions(+), 86 deletions(-) create mode 100644 FinalEngine.Editor.Common/Extensions/ServiceCollectionExtensions.cs create mode 100644 FinalEngine.Editor.Common/Services/Environment/EnvironmentContext.cs create mode 100644 FinalEngine.Editor.Common/Services/Environment/IEnvironmentContext.cs create mode 100644 FinalEngine.Editor.Common/Services/Factories/Factory.cs create mode 100644 FinalEngine.Editor.Common/Services/Factories/IFactory.cs create mode 100644 FinalEngine.Editor.Desktop/Exceptions/Layout/ToolPaneNotFoundException.cs create mode 100644 FinalEngine.Editor.Desktop/Exceptions/Layout/WindowLayoutNotFoundException.cs create mode 100644 FinalEngine.Editor.Desktop/GlobalSuppressions.cs create mode 100644 FinalEngine.Editor.Desktop/Resources/Images/Splashes/splash.png create mode 100644 FinalEngine.Editor.Desktop/Resources/Layouts/default.config create mode 100644 FinalEngine.Editor.Desktop/Selectors/Data/Docking/Panes/PaneTemplateSelector.cs create mode 100644 FinalEngine.Editor.Desktop/Selectors/Styles/Docking/Panes/PaneStyleSelector.cs create mode 100644 FinalEngine.Editor.Desktop/Services/Actions/UserActionRequester.cs create mode 100644 FinalEngine.Editor.Desktop/Services/Factories/Layout/LayoutManagerFactory.cs create mode 100644 FinalEngine.Editor.Desktop/Services/Layout/LayoutManager.cs create mode 100644 FinalEngine.Editor.Desktop/Styles/Controls/ButtonStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Styles/Controls/LabelStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Styles/Controls/ListBoxItemStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Styles/Controls/ListBoxStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Styles/Controls/TextBoxStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Styles/Docking/Panes/PaneStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Styles/Docking/Tools/ToolStyle.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Dialogs/Layout/ManageWindowLayoutsView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Dialogs/Layout/ManageWindowLayoutsView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Dialogs/Layout/SaveWindowLayoutView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Dialogs/Layout/SaveWindowLayoutView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Docking/DockView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Docking/DockView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Inspectors/ConsoleView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Inspectors/ConsoleView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Inspectors/PropertiesView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Inspectors/PropertiesView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Projects/ProjectExplorerView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Projects/ProjectExplorerView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Scenes/EntitySystemsView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Scenes/EntitySystemsView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Scenes/SceneHierarchyView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Scenes/SceneHierarchyView.xaml.cs create mode 100644 FinalEngine.Editor.Desktop/Views/Scenes/SceneView.xaml create mode 100644 FinalEngine.Editor.Desktop/Views/Scenes/SceneView.xaml.cs create mode 100644 FinalEngine.Editor.ViewModels/Dialogs/Layout/IManageWindowLayoutsViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Dialogs/Layout/ISaveWindowLayoutViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Dialogs/Layout/ManageWindowLayoutsViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Dialogs/Layout/SaveWindowLayoutViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/DockViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/IDockViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Panes/IPaneViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Panes/PaneViewModelBase.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Panes/Scenes/ISceneViewPaneViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Panes/Scenes/SceneViewPaneViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/IToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Inspectors/ConsoleToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Inspectors/IConsoleToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Inspectors/IPropertiesToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Inspectors/PropertiesToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Projects/IProjectExplorerToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Projects/ProjectExplorerToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Scenes/EntitySystemsToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Scenes/IEntitySystemsToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Scenes/ISceneHierarchyToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/Scenes/SceneHierarchyToolViewModel.cs create mode 100644 FinalEngine.Editor.ViewModels/Docking/Tools/ToolViewModelBase.cs create mode 100644 FinalEngine.Editor.ViewModels/IViewPresenter.cs create mode 100644 FinalEngine.Editor.ViewModels/Interactions/IViewable.cs create mode 100644 FinalEngine.Editor.ViewModels/Services/Actions/IUserActionRequester.cs create mode 100644 FinalEngine.Editor.ViewModels/Services/Factories/Layout/ILayoutManagerFactory.cs create mode 100644 FinalEngine.Editor.ViewModels/Services/Layout/ILayoutManager.cs create mode 100644 FinalEngine.Editor.ViewModels/Validation/FileNameAttribute.cs create mode 100644 FinalEngine.Editor.ViewModels/ViewPresenter.cs create mode 100644 FinalEngine.Tests/Core/Resources/Mocks/MockDisposableResource.cs create mode 100644 FinalEngine.Tests/Editor/Common/Services/Factories/FactoryTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Dialogs/Layouts/ManageWindowLayoutsViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Dialogs/Layouts/SaveWindowLayoutViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/DockViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Panes/Mocks/MockPaneViewModel.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Panes/PaneViewModelBaseTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Panes/Scenes/SceneViewPaneViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/Inspectors/ConsoleToolViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/Inspectors/PropertiesToolViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/Mocks/MockToolViewModel.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/Projects/ProjectExplorerToolViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/Scenes/EntitySystemsToolViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/Scenes/SceneHierarchyToolViewModelTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Docking/Tools/ToolViewModelBaseTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/Validation/FileNameAttributeTests.cs create mode 100644 FinalEngine.Tests/Editor/ViewModels/ViewPresenterTests.cs diff --git a/FinalEngine.Editor.Common/Extensions/ServiceCollectionExtensions.cs b/FinalEngine.Editor.Common/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..812b8a43 --- /dev/null +++ b/FinalEngine.Editor.Common/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Extensions; + +using System; +using System.Diagnostics.CodeAnalysis; +using FinalEngine.Editor.Common.Services.Factories; +using Microsoft.Extensions.DependencyInjection; + +/// +/// Provides extension methods for an . +/// +[ExcludeFromCodeCoverage(Justification = "Extensions")] +public static class ServiceCollectionExtensions +{ + /// + /// Adds an that can create a to the specified . + /// + /// + /// The type of the service to register. + /// + /// + /// The type of service implementation. + /// + /// + /// The used to register the service. + /// + /// + /// The specified parameter cannot be null. + /// + public static void AddFactory(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddTransient(); + services.AddSingleton>(x => + { + return () => + { + return x.GetRequiredService(); + }; + }); + + services.AddSingleton, Factory>(); + } +} diff --git a/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj b/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj index 0c106d7c..1cbc5447 100644 --- a/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj +++ b/FinalEngine.Editor.Common/FinalEngine.Editor.Common.csproj @@ -23,9 +23,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs b/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs index 1b195bc7..29dc47b5 100644 --- a/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs +++ b/FinalEngine.Editor.Common/Services/Application/ApplicationContext.cs @@ -5,7 +5,9 @@ namespace FinalEngine.Editor.Common.Services.Application; using System; +using System.IO.Abstractions; using System.Reflection; +using FinalEngine.Editor.Common.Services.Environment; /// /// Provides a standard implementation of an . @@ -13,6 +15,53 @@ namespace FinalEngine.Editor.Common.Services.Application; /// public sealed class ApplicationContext : IApplicationContext { + /// + /// The environment service, used when locating the applications local data directory. + /// + private readonly IEnvironmentContext environment; + + /// + /// The file system service, used to potentially create the applications local data directory. + /// + private readonly IFileSystem fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The file system service, used to create the required directories for , if required. + /// + /// + /// The environment, used to locate the application data folder for the local user. + /// + /// + /// The specified or parameter cannot be null. + /// + public ApplicationContext(IFileSystem fileSystem, IEnvironmentContext environment) + { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + this.environment = environment ?? throw new ArgumentNullException(nameof(environment)); + } + + /// + /// + /// Accessing will ensure the applications data directory is created before returning it's location. + /// + public string DataDirectory + { + get + { + string directory = this.fileSystem.Path.Combine(this.environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Final Engine"); + + if (!this.fileSystem.Directory.Exists(directory)) + { + this.fileSystem.Directory.CreateDirectory(directory); + } + + return directory; + } + } + /// public string Title { diff --git a/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs b/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs index 45f8e68e..d3cd6d37 100644 --- a/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs +++ b/FinalEngine.Editor.Common/Services/Application/IApplicationContext.cs @@ -7,15 +7,23 @@ namespace FinalEngine.Editor.Common.Services.Application; using System; /// -/// Defines an interface that represents contextual information related to the current application. +/// Defines an interface that represents contextual information related to the current application and it's associated data. /// public interface IApplicationContext { + /// + /// Gets the directory that serves as a common repository for Final Engine application-specific data for the current local user. + /// + /// + /// The directory that serves as a common repository for application-specific data for the current local user. + /// + string DataDirectory { get; } + /// /// Gets the title of the application. /// /// - /// The title of the application. + /// The title of the application, suffixed by the . /// string Title { get; } @@ -23,7 +31,7 @@ public interface IApplicationContext /// Gets the version of the application. /// /// - /// The version of the application. + /// The assembly version of the application. /// Version Version { get; } } diff --git a/FinalEngine.Editor.Common/Services/Environment/EnvironmentContext.cs b/FinalEngine.Editor.Common/Services/Environment/EnvironmentContext.cs new file mode 100644 index 00000000..b0a5c556 --- /dev/null +++ b/FinalEngine.Editor.Common/Services/Environment/EnvironmentContext.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Services.Environment; + +using System; +using System.Diagnostics.CodeAnalysis; + +/// +/// Provides a standard implementation of an . +/// +/// +[ExcludeFromCodeCoverage] +public sealed class EnvironmentContext : IEnvironmentContext +{ + /// + public string GetFolderPath(Environment.SpecialFolder folder) + { + return Environment.GetFolderPath(folder); + } +} diff --git a/FinalEngine.Editor.Common/Services/Environment/IEnvironmentContext.cs b/FinalEngine.Editor.Common/Services/Environment/IEnvironmentContext.cs new file mode 100644 index 00000000..294df611 --- /dev/null +++ b/FinalEngine.Editor.Common/Services/Environment/IEnvironmentContext.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Services.Environment; + +using System; + +/// +/// Defines an interface that provides information about the current environment and platform. +/// +public interface IEnvironmentContext +{ + /// /> + string GetFolderPath(Environment.SpecialFolder folder); +} diff --git a/FinalEngine.Editor.Common/Services/Factories/Factory.cs b/FinalEngine.Editor.Common/Services/Factories/Factory.cs new file mode 100644 index 00000000..893bb40b --- /dev/null +++ b/FinalEngine.Editor.Common/Services/Factories/Factory.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Services.Factories; + +using System; + +/// +/// Provides a standard implementation of an . +/// +/// +/// The type of instance to create. +/// +/// +public sealed class Factory : IFactory +{ + /// + /// The function required to create the instance. + /// + private readonly Func factory; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The function used when creating the required instance. + /// + /// + /// The specified parameter cannot be null. + /// + public Factory(Func factory) + { + this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + /// + public T Create() + { + return this.factory(); + } +} diff --git a/FinalEngine.Editor.Common/Services/Factories/IFactory.cs b/FinalEngine.Editor.Common/Services/Factories/IFactory.cs new file mode 100644 index 00000000..ffb29797 --- /dev/null +++ b/FinalEngine.Editor.Common/Services/Factories/IFactory.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Common.Services.Factories; + +/// +/// Defines an interface that provides a generic factory method. +/// +/// +/// The type of instance to create. +/// +public interface IFactory +{ + /// + /// Creates an instance of type . + /// + /// + /// The newly created instance. + /// + T Create(); +} diff --git a/FinalEngine.Editor.Desktop/App.xaml b/FinalEngine.Editor.Desktop/App.xaml index 412083d9..072b7ece 100644 --- a/FinalEngine.Editor.Desktop/App.xaml +++ b/FinalEngine.Editor.Desktop/App.xaml @@ -8,6 +8,13 @@ + + + + + + + diff --git a/FinalEngine.Editor.Desktop/App.xaml.cs b/FinalEngine.Editor.Desktop/App.xaml.cs index 40752e62..d609e2ad 100644 --- a/FinalEngine.Editor.Desktop/App.xaml.cs +++ b/FinalEngine.Editor.Desktop/App.xaml.cs @@ -5,10 +5,26 @@ namespace FinalEngine.Editor.Desktop; using System.Diagnostics; +using System.IO.Abstractions; using System.Windows; +using CommunityToolkit.Mvvm.Messaging; +using FinalEngine.Editor.Common.Extensions; using FinalEngine.Editor.Common.Services.Application; +using FinalEngine.Editor.Common.Services.Environment; +using FinalEngine.Editor.Desktop.Services.Actions; +using FinalEngine.Editor.Desktop.Services.Factories.Layout; using FinalEngine.Editor.Desktop.Views; +using FinalEngine.Editor.Desktop.Views.Dialogs.Layout; using FinalEngine.Editor.ViewModels; +using FinalEngine.Editor.ViewModels.Dialogs.Layout; +using FinalEngine.Editor.ViewModels.Docking; +using FinalEngine.Editor.ViewModels.Docking.Panes.Scenes; +using FinalEngine.Editor.ViewModels.Docking.Tools.Inspectors; +using FinalEngine.Editor.ViewModels.Docking.Tools.Projects; +using FinalEngine.Editor.ViewModels.Docking.Tools.Scenes; +using FinalEngine.Editor.ViewModels.Interactions; +using FinalEngine.Editor.ViewModels.Services.Actions; +using FinalEngine.Editor.ViewModels.Services.Factories.Layout; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -86,7 +102,32 @@ private static void ConfigureServices(HostBuilderContext context, IServiceCollec builder.AddConsole().SetMinimumLevel(Debugger.IsAttached ? LogLevel.Debug : LogLevel.Information); }); + services.AddSingleton(); + + services.AddSingleton(WeakReferenceMessenger.Default); services.AddSingleton(); - services.AddTransient(); + services.AddSingleton(); + + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddFactory(); + services.AddSingleton(); + + services.AddTransient, SaveWindowLayoutView>(); + services.AddTransient, ManageWindowLayoutsView>(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(x => + { + return new ViewPresenter(x.GetRequiredService>(), x); + }); } } diff --git a/FinalEngine.Editor.Desktop/Exceptions/Layout/ToolPaneNotFoundException.cs b/FinalEngine.Editor.Desktop/Exceptions/Layout/ToolPaneNotFoundException.cs new file mode 100644 index 00000000..215f1aa7 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Exceptions/Layout/ToolPaneNotFoundException.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Exceptions.Layout; + +using System; +using System.Runtime.Serialization; + +/// +/// Provides an exception when a tool pane cannot be located by it's content identifier. +/// +/// +[Serializable] +public class ToolPaneNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ToolPaneNotFoundException() + : base("The content identifier could not be matched to a tool pane.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The content identifier of the tool pane that could not be located and caused the exception to be thrown. + /// + public ToolPaneNotFoundException(string? contentID) + : base($"The content identifier '{contentID}' could not be matched to a tool pane.") + { + this.ContentID = contentID; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The error message that explains the reason for the exception. + /// + /// + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + /// + public ToolPaneNotFoundException(string? message, Exception? innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object data about the exception being thrown. + /// + /// + /// The that contains contextual information about the source or destination. + /// + protected ToolPaneNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Gets the content identifier of the tool pane that could not be located. + /// + /// + /// The content identifier of the tool pane that could not be located and caused the exception to be thrown; otherwise, null. + /// + public string? ContentID { get; } +} diff --git a/FinalEngine.Editor.Desktop/Exceptions/Layout/WindowLayoutNotFoundException.cs b/FinalEngine.Editor.Desktop/Exceptions/Layout/WindowLayoutNotFoundException.cs new file mode 100644 index 00000000..25a7ddb7 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Exceptions/Layout/WindowLayoutNotFoundException.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Exceptions.Layout; + +using System; +using System.Runtime.Serialization; + +/// +/// Provides an exception that is thrown when a window layout could not be found. +/// +/// +[Serializable] +public class WindowLayoutNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public WindowLayoutNotFoundException() + : base("Failed to locate a window layout.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the layout that could not be found and caused the exception to be thrown. + /// + public WindowLayoutNotFoundException(string layoutName) + : base($"Failed to locate a window layout that matches: '{layoutName}'.") + { + this.LayoutName = layoutName; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The error message that explains the reason for the exception. + /// + /// + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + /// + public WindowLayoutNotFoundException(string? message, Exception? innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object data about the exception being thrown. + /// + /// + /// The that contains contextual information about the source or destination. + /// + protected WindowLayoutNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Gets the name of the layout that could not be found. + /// + /// + /// The name of the layout that could not be found and caused the exception to be thrown; otherwise, null. + /// + public string? LayoutName { get; } +} diff --git a/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj b/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj index e43a46dd..ed1fdbd5 100644 --- a/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj +++ b/FinalEngine.Editor.Desktop/FinalEngine.Editor.Desktop.csproj @@ -1,7 +1,7 @@ - WinExe + Exe net7.0-windows enable true @@ -11,6 +11,10 @@ true x64 + + + + @@ -21,6 +25,9 @@ + + + all @@ -37,4 +44,14 @@ - \ No newline at end of file + + + + PreserveNewest + + + + + + + diff --git a/FinalEngine.Editor.Desktop/GlobalSuppressions.cs b/FinalEngine.Editor.Desktop/GlobalSuppressions.cs new file mode 100644 index 00000000..f52f45a5 --- /dev/null +++ b/FinalEngine.Editor.Desktop/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "KISS")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "KISS")] diff --git a/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs b/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs index 7d52a865..29e1d2e2 100644 --- a/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs +++ b/FinalEngine.Editor.Desktop/Properties/AssemblyInfo.cs @@ -5,8 +5,10 @@ using System; using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Windows; +[assembly: SupportedOSPlatform("windows")] [assembly: CLSCompliant(false)] [assembly: ComVisible(false)] [assembly: AssemblyTitle("FinalEngine.Editor.Desktop")] diff --git a/FinalEngine.Editor.Desktop/Resources/Images/Splashes/splash.png b/FinalEngine.Editor.Desktop/Resources/Images/Splashes/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..bbd0a3f395997188fb97acf9082516676ca98425 GIT binary patch literal 26599 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

@=M)CqPhxHpOqx@<=* z+5cP`xO<{;wa{cStj2pElI_oI#)KQ-lCSZ%X8NmY@Ah#|YKHM<(3mlji%@*c8FF(? zi|=m}Td18H$6bh0dqXQ34Mrc}vsDNJ%U%rN32j&WSvgkFnu6Yc6vQCbSo4sMoegAU0_*{;H)Zce}0r^%g7)8lV2E5^cd*iVr#w$tH|A>L4+H z__1_Bg*(MWcV~qQ%D0 zm*hhfDXO?A__36sNuTfE8H(~|o!1}WKcDJ24&S)aTL4-wYy6&KSi!IkctaAtyOluq zfOv7k0y_+x+X(ttxAC5BJ?+nTSx+p&*_!n1YZSUmu5slmz;SpmRM@4%+UK3E+@7jy zXGMhbF+^has-l`j&-x$1n!SgLry0kkbke*5kwo0q$)@~GgT;M- zUr~{YdyIhWn!IcD1}dzwMBI>b^S8)rwlvHa|+MS$% z-!rg>tX?8I&s(F(7R-;{wK}ZMO@rPF>|iJUL^*F`*!ecAjq@EdYNbnG{;H-C?K3+& zZBjs9BWI!+@h8=d={E8_(ecS9SC4jAc5P+zfOb$ze|a@6%DUf=+?G*8anOWPju8rp z7EIN@YurkA96xH=nu|d<=dYRUYmf|n+N=%mxEOm{Yg`qM*+%TV_zR5Yx`?DVuon&g zk~t%g6OZ#=z$DH^HgKXY!L`oP%v%GjfNp@2Rj%qd*v>}nXlhaRM&8BMco*L6J_XW| zrKFXobQP5ezLQt^=7#%bh)d%rPC--c6}gim4Uj7bW3tWNdQ}%A0_{!Um4sJ8Tif1| zXg|;Hu3^0oV+#@U$bX=gk7#O}zqz^aA=4yy^2dqJrtQg~cTnEBt=-kI(YOragLDSXgMLkZSk5(OBttDV9m=Q&+0iv-1Ov|KJ@u{1wvf<%gK!P8IwYu_48ALw1z zv?E^lp$7f4-?hQltzdcXB~ne>`6p6sLL?mgqBMsl__kO>g9BaP4rSloA*$}J_*xQZ zqQ;8aqh3{2UmdHz)aqLkbDY?ZEhBBbW*JjLeK`t4lj!?gP_tHpEyZGug59+JE2y`R z0OB9uXR(wl4+!-K$&f@Tu=4stxYsOYt> zd}++%*pAPkZ-D|8jX~|HS&<+J61U1Y3aF9Oy)rg?G=_5X=e{>0AE0+kFryAUS_$5K zZxp6P$yeB3BV?6(MX8G|^%Jba*VW5$cCeJdM3jVs4ZR1Om@?>L6l1O8E;OQiHFB4UEtMOIfPGXX!Wcy zTlz<*kzb=HHC{ijVOprCHGT1u+DX=94KrFweB?dB-K6rlaKO$uKJ@B?G2Ma^u>j%o zUJ}OeBa|}z`-X3JluX3e~24WU5HM@Zn}+@E;=d}n}M@jj85s|=q-CkDJo)7g9e@Y3#t78!Q4nUg2X)N#pU@XA2tr2YF&}p|mv%HBm%N!FKFk0hS&}|;QgDVwh?MZ9MU4q#Ep8H9c zhgI?GceTv{l&^3O>@aX#olRlY`BT+hMo1!E7zz@cm2qbCID_l&&DblG( zaa+e0{N2aCWyfFiu9E-p({_&53>IJ1zIaBw-vJBH9+TzHq8gfAq-P!uA}~$>wk9(8 zW-G}0U-92w?dgB$bVr;~-TNHSU=aUTCr={`!$b9AZ_}^D)qJ93b8B<8>N<0EKQGX< zXq+CKr_Ozz+{#^8LW9-RU8yX`z#Q@_D!05`?WLBg&MV&PmDeh-j3w(EJl&o?CBTkABkJBS zHnU&}Pk$vE_ff9+qg!N5G@B>~W?@$s6+W1lK_V_?(zf=)qanze6#(cT8v@K9s$(J5)dr8q>BG%Myg%bWJemKRMaY5i`R&zI~{IB8wfX-46 zw8gT!KV@eLLPUpx0HQcSg$1q9 zB^yC0^|#lK zTPR)ag?+p&JTdU=SsWs0mudPoH8Km7)71_IYsiQ+G>}mQGMoNuiun7PE0KiSz ASO5S3 literal 0 HcmV?d00001 diff --git a/FinalEngine.Editor.Desktop/Resources/Layouts/default.config b/FinalEngine.Editor.Desktop/Resources/Layouts/default.config new file mode 100644 index 00000000..842955b5 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Resources/Layouts/default.config @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FinalEngine.Editor.Desktop/Selectors/Data/Docking/Panes/PaneTemplateSelector.cs b/FinalEngine.Editor.Desktop/Selectors/Data/Docking/Panes/PaneTemplateSelector.cs new file mode 100644 index 00000000..8915685a --- /dev/null +++ b/FinalEngine.Editor.Desktop/Selectors/Data/Docking/Panes/PaneTemplateSelector.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Selectors.Data.Docking.Panes; + +using System.Windows; +using System.Windows.Controls; +using FinalEngine.Editor.ViewModels.Docking.Panes.Scenes; +using FinalEngine.Editor.ViewModels.Docking.Tools.Inspectors; +using FinalEngine.Editor.ViewModels.Docking.Tools.Projects; +using FinalEngine.Editor.ViewModels.Docking.Tools.Scenes; + +///

+/// Provides a data template selectors for panes and tool views. +/// +/// +public sealed class PaneTemplateSelector : DataTemplateSelector +{ + /// + /// Gets or sets the console template. + /// + /// + /// The console template. + /// + public DataTemplate? ConsoleTemplate { get; set; } + + /// + /// Gets or sets the entity systems template. + /// + /// + /// The entity systems template. + /// + public DataTemplate? EntitySystemsTemplate { get; set; } + + /// + /// Gets or sets the project explorer template. + /// + /// + /// The project explorer template. + /// + public DataTemplate? ProjectExplorerTemplate { get; set; } + + /// + /// Gets or sets the properties template. + /// + /// + /// The properties template. + /// + public DataTemplate? PropertiesTemplate { get; set; } + + /// + /// Gets or sets the scene hierarchy template. + /// + /// + /// The scene hierarchy template. + /// + public DataTemplate? SceneHierarchyTemplate { get; set; } + + /// + /// Gets or sets the scene view template. + /// + /// + /// The scene view template. + /// + public DataTemplate? SceneViewTemplate { get; set; } + + /// + /// Selects the template to be used based on the specified . + /// + /// + /// The item, this refers to the view model. + /// + /// + /// The container. + /// + /// + /// The to use, or null if one could not be found. + /// + public override DataTemplate? SelectTemplate(object item, DependencyObject container) + { + return item switch + { + IConsoleToolViewModel => this.ConsoleTemplate, + IEntitySystemsToolViewModel => this.EntitySystemsTemplate, + IProjectExplorerToolViewModel => this.ProjectExplorerTemplate, + IPropertiesToolViewModel => this.PropertiesTemplate, + ISceneHierarchyToolViewModel => this.SceneHierarchyTemplate, + ISceneViewPaneViewModel => this.SceneViewTemplate, + _ => base.SelectTemplate(item, container) + }; + } +} diff --git a/FinalEngine.Editor.Desktop/Selectors/Styles/Docking/Panes/PaneStyleSelector.cs b/FinalEngine.Editor.Desktop/Selectors/Styles/Docking/Panes/PaneStyleSelector.cs new file mode 100644 index 00000000..134e4782 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Selectors/Styles/Docking/Panes/PaneStyleSelector.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Selectors.Styles.Docking.Panes; + +using System.Windows; +using System.Windows.Controls; +using FinalEngine.Editor.ViewModels.Docking.Panes; +using FinalEngine.Editor.ViewModels.Docking.Tools; + +/// +/// Provides a style selector for panes and tool views. +/// +/// +public sealed class PaneStyleSelector : StyleSelector +{ + /// + /// Gets or sets the pane style. + /// + /// + /// The pane style. + /// + public Style? PaneStyle { get; set; } + + /// + /// Gets or sets the tool style. + /// + /// + /// The tool style. + /// + public Style? ToolStyle { get; set; } + + /// + /// Selects the style based on the specified . + /// + /// The item, this refers to the view model. + /// + /// + /// The container. + /// + /// + /// The to use or null if one could not be found. + /// + public override Style? SelectStyle(object item, DependencyObject container) + { + return item switch + { + IToolViewModel => this.ToolStyle, + IPaneViewModel => this.PaneStyle, + _ => base.SelectStyle(item, container) + }; + } +} diff --git a/FinalEngine.Editor.Desktop/Services/Actions/UserActionRequester.cs b/FinalEngine.Editor.Desktop/Services/Actions/UserActionRequester.cs new file mode 100644 index 00000000..fe70f39e --- /dev/null +++ b/FinalEngine.Editor.Desktop/Services/Actions/UserActionRequester.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Services.Actions; + +using System; +using System.Windows; +using FinalEngine.Editor.ViewModels.Services.Actions; +using Microsoft.Extensions.Logging; + +/// +/// Provides a standard implementation of an . +/// +/// +public sealed class UserActionRequester : IUserActionRequester +{ + /// + /// The logger. + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The logger. + /// + /// + /// The specified parameter cannot be null. + /// + public UserActionRequester(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public void RequestOk(string caption, string message) + { + this.logger.LogInformation($"Requesting OK response from user for request: '{message}'."); + MessageBox.Show(message, caption); + } + + /// + public bool RequestYesNo(string caption, string message) + { + this.logger.LogInformation($"Requesting YES/NO response from user for request: '{message}'."); + return MessageBox.Show(message, caption, MessageBoxButton.YesNo) == MessageBoxResult.Yes; + } +} diff --git a/FinalEngine.Editor.Desktop/Services/Factories/Layout/LayoutManagerFactory.cs b/FinalEngine.Editor.Desktop/Services/Factories/Layout/LayoutManagerFactory.cs new file mode 100644 index 00000000..a5628e57 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Services/Factories/Layout/LayoutManagerFactory.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Services.Factories.Layout; + +using System; +using System.IO.Abstractions; +using System.Windows; +using AvalonDock; +using FinalEngine.Editor.Common.Services.Application; +using FinalEngine.Editor.Desktop.Services.Layout; +using FinalEngine.Editor.Desktop.Views.Docking; +using FinalEngine.Editor.ViewModels.Services.Factories.Layout; +using FinalEngine.Editor.ViewModels.Services.Layout; +using MahApps.Metro.Controls; +using Microsoft.Extensions.Logging; + +/// +/// Provides a standard implementation of an . +/// +/// +public sealed class LayoutManagerFactory : ILayoutManagerFactory +{ + /// + /// The cached instanced to the docking manager. + /// + /// + /// The instance is cached to avoid a when unloading the . + /// + private static readonly DockingManager Instance = Application.Current.MainWindow.FindChild().DockManager; + + /// + /// The application context, used to instantiate the layout manager. + /// + private readonly IApplicationContext application; + + /// + /// The file system, used to instantiate the layout manager. + /// + private readonly IFileSystem fileSystem; + + /// + /// The layout manager logger. + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The layout manager logger. + /// + /// + /// The application context, used to instantiate the layout manager. + /// + /// + /// The file system, used to instantiate the layout manager. + /// + /// + /// The specified or parameter cannot be null. + /// + public LayoutManagerFactory(ILogger logger, IApplicationContext application, IFileSystem fileSystem) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.application = application ?? throw new ArgumentNullException(nameof(application)); + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + + /// + public ILayoutManager CreateManager() + { + return new LayoutManager(this.logger, Instance, this.application, this.fileSystem); + } +} diff --git a/FinalEngine.Editor.Desktop/Services/Layout/LayoutManager.cs b/FinalEngine.Editor.Desktop/Services/Layout/LayoutManager.cs new file mode 100644 index 00000000..f5e2ed23 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Services/Layout/LayoutManager.cs @@ -0,0 +1,250 @@ +// +// Copyright (c) Software Antics. All rights reserved. +// + +namespace FinalEngine.Editor.Desktop.Services.Layout; + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using AvalonDock; +using AvalonDock.Layout.Serialization; +using FinalEngine.Editor.Common.Services.Application; +using FinalEngine.Editor.Desktop.Exceptions.Layout; +using FinalEngine.Editor.ViewModels.Docking.Tools; +using FinalEngine.Editor.ViewModels.Services.Layout; +using Microsoft.Extensions.Logging; + +/// +/// Provides a standard implementation of an . +/// +/// +public sealed class LayoutManager : ILayoutManager +{ + /// + /// The application, used to resolve a directory where the window layouts are saved. + /// + private readonly IApplicationContext application; + + /// + /// The docking manager, used to manage the current window layout. + /// + private readonly DockingManager dockManager; + + /// + /// The file system, used to create the if one does not already exist. + /// + private readonly IFileSystem fileSystem; + + /// + /// The logger. + /// + private readonly ILogger logger; + + /// + /// The layout serializer, used to serialize and deserialize the window layouts where required. + /// + private readonly XmlLayoutSerializer serializer; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The logger. + /// + /// + /// The dock manager, used to manage the current window layout. + /// + /// + /// The application context, used to resolve a directory where window layouts are stored. + /// + /// + /// The file system, used to create the if one does not already exist. + /// + /// + /// The specified , or parameter cannot be null. + /// + public LayoutManager( + ILogger logger, + DockingManager dockManager, + IApplicationContext application, + IFileSystem fileSystem) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.dockManager = dockManager ?? throw new ArgumentNullException(nameof(dockManager)); + this.application = application ?? throw new ArgumentNullException(nameof(application)); + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + + this.serializer = new XmlLayoutSerializer(this.dockManager); + } + + /// + /// Gets the layout directory. + /// + /// + /// The layout directory. + /// + /// + /// If the layout directory doesn't exist on the file system, one will be created. The layout directory is stored the applications local data. + /// + private string LayoutDirectory + { + get + { + string directory = this.fileSystem.Path.Combine(this.application.DataDirectory, "Layouts"); + + if (!this.fileSystem.Directory.Exists(directory)) + { + this.fileSystem.Directory.CreateDirectory(directory); + } + + return directory; + } + } + + /// + /// + /// cannot be null or whitespace. + /// + public bool ContainsLayout(string layoutName) + { + if (string.IsNullOrWhiteSpace(layoutName)) + { + throw new ArgumentException($"'{nameof(layoutName)}' cannot be null or whitespace.", nameof(layoutName)); + } + + return this.LoadLayoutNames().Contains(layoutName); + } + + /// + /// + /// cannot be null or whitespace. + /// + /// + /// The specified could not be matched to a save window layout. + /// + public void DeleteLayout(string layoutName) + { + if (string.IsNullOrWhiteSpace(layoutName)) + { + throw new ArgumentException($"'{nameof(layoutName)}' cannot be null or whitespace.", nameof(layoutName)); + } + + if (!this.ContainsLayout(layoutName)) + { + throw new WindowLayoutNotFoundException(layoutName); + } + + this.logger.LogInformation($"Deleting window layout: '{layoutName}'."); + + this.fileSystem.File.Delete(this.GetLayoutPath(layoutName)); + + this.logger.LogInformation($"Layout deleted."); + } + + /// + /// + /// cannot be null or whitespace. + /// + /// + /// The specified could not be matched to a save window layout. + /// + public void LoadLayout(string layoutName) + { + if (string.IsNullOrWhiteSpace(layoutName)) + { + throw new ArgumentException($"'{nameof(layoutName)}' cannot be null or whitespace.", nameof(layoutName)); + } + + if (!this.ContainsLayout(layoutName)) + { + throw new WindowLayoutNotFoundException(layoutName); + } + + this.logger.LogInformation($"Loading window layout: '{layoutName}'."); + + this.serializer.Deserialize(this.GetLayoutPath(layoutName)); + + this.logger.LogInformation("Layout loaded."); + } + + /// + public IEnumerable LoadLayoutNames() + { + var directoryInfo = this.fileSystem.DirectoryInfo.New(this.LayoutDirectory); + var files = directoryInfo.GetFiles("*.config", SearchOption.TopDirectoryOnly); + + return files.Select(x => + { + return this.fileSystem.Path.GetFileNameWithoutExtension(x.Name); + }).ToArray(); + } + + /// + public void ResetLayout() + { + const string defaultLayoutPath = "Resources\\Layouts\\default.config"; + + this.logger.LogInformation("Resting window layout to default layout..."); + this.serializer.Deserialize(defaultLayoutPath); + this.logger.LogInformation("Layout reset."); + } + + /// + /// + /// cannot be null or whitespace. + /// + public void SaveLayout(string layoutName) + { + if (string.IsNullOrWhiteSpace(layoutName)) + { + throw new ArgumentException($"'{nameof(layoutName)}' cannot be null or whitespace.", nameof(layoutName)); + } + + this.logger.LogInformation($"Saving window layout: '{layoutName}'..."); + + this.serializer.Serialize(this.GetLayoutPath(layoutName)); + + this.logger.LogInformation("Layout saved."); + } + + /// + /// + /// cannot be null or whitespace. + /// + /// + /// The parameter could not be matched to a tool pane. + /// + public void ToggleToolWindow(string contentID) + { + if (string.IsNullOrWhiteSpace(contentID)) + { + throw new ArgumentException($"'{nameof(contentID)}' cannot be null or whitespace.", nameof(contentID)); + } + + var tool = this.dockManager.AnchorablesSource.Cast().FirstOrDefault(x => + { + return x.ContentID == contentID; + }) ?? throw new ToolPaneNotFoundException(contentID); + + this.logger.LogInformation($"Toggling tool view visibility for view with ID: '{contentID}'"); + + tool.IsVisible = !tool.IsVisible; + } + + /// + /// Gets the layout file path of the specified . + /// + /// + /// The name of the layout. + /// + /// + /// The file path of window layout that matches the specified . + /// + private string GetLayoutPath(string layoutName) + { + return this.fileSystem.Path.Combine(this.LayoutDirectory, $"{layoutName}.config"); + } +} diff --git a/FinalEngine.Editor.Desktop/Styles/Controls/ButtonStyle.xaml b/FinalEngine.Editor.Desktop/Styles/Controls/ButtonStyle.xaml new file mode 100644 index 00000000..779250f0 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Styles/Controls/ButtonStyle.xaml @@ -0,0 +1,14 @@ + + + diff --git a/FinalEngine.Editor.Desktop/Styles/Controls/LabelStyle.xaml b/FinalEngine.Editor.Desktop/Styles/Controls/LabelStyle.xaml new file mode 100644 index 00000000..98d1c706 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Styles/Controls/LabelStyle.xaml @@ -0,0 +1,9 @@ + + + diff --git a/FinalEngine.Editor.Desktop/Styles/Controls/ListBoxItemStyle.xaml b/FinalEngine.Editor.Desktop/Styles/Controls/ListBoxItemStyle.xaml new file mode 100644 index 00000000..74d4d1c9 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Styles/Controls/ListBoxItemStyle.xaml @@ -0,0 +1,7 @@ + + + diff --git a/FinalEngine.Editor.Desktop/Styles/Controls/TextBoxStyle.xaml b/FinalEngine.Editor.Desktop/Styles/Controls/TextBoxStyle.xaml new file mode 100644 index 00000000..2453aa97 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Styles/Controls/TextBoxStyle.xaml @@ -0,0 +1,20 @@ + + + diff --git a/FinalEngine.Editor.Desktop/Styles/Docking/Panes/PaneStyle.xaml b/FinalEngine.Editor.Desktop/Styles/Docking/Panes/PaneStyle.xaml new file mode 100644 index 00000000..3a064acc --- /dev/null +++ b/FinalEngine.Editor.Desktop/Styles/Docking/Panes/PaneStyle.xaml @@ -0,0 +1,14 @@ + + + diff --git a/FinalEngine.Editor.Desktop/Styles/Docking/Tools/ToolStyle.xaml b/FinalEngine.Editor.Desktop/Styles/Docking/Tools/ToolStyle.xaml new file mode 100644 index 00000000..cb3f0a7d --- /dev/null +++ b/FinalEngine.Editor.Desktop/Styles/Docking/Tools/ToolStyle.xaml @@ -0,0 +1,18 @@ + + + diff --git a/FinalEngine.Editor.Desktop/Views/Dialogs/Layout/ManageWindowLayoutsView.xaml b/FinalEngine.Editor.Desktop/Views/Dialogs/Layout/ManageWindowLayoutsView.xaml new file mode 100644 index 00000000..66f80bd8 --- /dev/null +++ b/FinalEngine.Editor.Desktop/Views/Dialogs/Layout/ManageWindowLayoutsView.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + +