diff --git a/projects/Oxygen.Editor.WorldEditor/src/Oxygen.Editor.WorldEditor.csproj b/projects/Oxygen.Editor.WorldEditor/src/Oxygen.Editor.WorldEditor.csproj
index a7125870..564ec55c 100644
--- a/projects/Oxygen.Editor.WorldEditor/src/Oxygen.Editor.WorldEditor.csproj
+++ b/projects/Oxygen.Editor.WorldEditor/src/Oxygen.Editor.WorldEditor.csproj
@@ -8,6 +8,8 @@
10.0.22000.0
x86;x64;arm64
win-x86;win-x64;win-arm64
+ true
+ True
@@ -61,38 +66,4 @@
-
-
- MSBuild:Compile
-
-
-
-
-
- MSBuild:Compile
-
-
-
-
-
- MSBuild:Compile
-
-
-
-
-
- MSBuild:Compile
-
-
-
-
-
-
-
-
-
- MSBuild:Compile
-
-
-
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/EntityAdapter.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/EntityAdapter.cs
new file mode 100644
index 00000000..7d10bb40
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/EntityAdapter.cs
@@ -0,0 +1,26 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using DroidNet.Controls;
+using Oxygen.Editor.Projects;
+
+///
+/// A item adapter for the model class.
+///
+/// The object to wrap as a .
+public partial class EntityAdapter(Entity entity) : TreeItemAdapter, ITreeItem
+{
+ public override bool IsRoot => false;
+
+ public override string Label
+ => this.AttachedObject.Name;
+
+ public Entity AttachedObject => entity;
+
+ protected override int GetChildrenCount() => 0;
+
+ protected override async Task LoadChildren() => await Task.CompletedTask.ConfigureAwait(false);
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectAdapter.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectAdapter.cs
new file mode 100644
index 00000000..64fef383
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectAdapter.cs
@@ -0,0 +1,44 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using DroidNet.Controls;
+using Oxygen.Editor.Projects;
+
+///
+/// A item adapter for the model class.
+///
+/// The object to wrap as a .
+/// The configured project manager service.
+public partial class ProjectAdapter(Project project, IProjectManagerService projectManager)
+ : TreeItemAdapter, ITreeItem
+{
+ public override string Label => project.ProjectInfo.Name;
+
+ public Project AttachedObject => project;
+
+ protected override int GetChildrenCount() => project.Scenes.Count;
+
+ protected override async Task LoadChildren()
+ {
+ this.ClearChildren();
+
+ if (!await projectManager.LoadProjectScenesAsync(project).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ foreach (var scene in project.Scenes)
+ {
+ this.AddChildInternal(
+ new SceneAdapter(scene, projectManager)
+ {
+ IsExpanded = true,
+ });
+ }
+
+ await Task.CompletedTask.ConfigureAwait(true);
+ }
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerView.xaml b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerView.xaml
new file mode 100644
index 00000000..c13cd8d9
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerView.xaml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerView.xaml.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerView.xaml.cs
new file mode 100644
index 00000000..ee894a0a
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerView.xaml.cs
@@ -0,0 +1,62 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using DroidNet.Controls;
+using DroidNet.Mvvm.Generators;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Input;
+using Oxygen.Editor.Projects;
+
+///
+/// A View that shows a hierarchical layout of a project that has scenes, which
+/// in turn can hold multiple entities. It demonstrates the flexibility of the
+/// in representing hierarchical layouts of mixed types which can be loaded dynamically.
+///
+[ViewModel(typeof(ProjectExplorerViewModel))]
+public sealed partial class ProjectExplorerView
+{
+ public ProjectExplorerView()
+ {
+ this.InitializeComponent();
+
+ this.Loaded += this.OnLoaded;
+ }
+
+ private async void OnLoaded(object sender, RoutedEventArgs args)
+ {
+ _ = sender; // unused
+ _ = args; // unused
+
+ if (this.ViewModel is not null)
+ {
+ await this.ViewModel.LoadProjectCommand.ExecuteAsync(parameter: null).ConfigureAwait(true);
+ }
+ }
+
+ private void UndoInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
+ {
+ _ = sender; // unused
+ args.Handled = true;
+
+ this.ViewModel!.UndoCommand.Execute(parameter: null);
+ }
+
+ private void RedoInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
+ {
+ _ = sender; // unused
+ args.Handled = true;
+
+ this.ViewModel!.RedoCommand.Execute(parameter: null);
+ }
+
+ private async void DeleteInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
+ {
+ _ = sender; // unused
+ args.Handled = true;
+
+ await this.ViewModel!.RemoveSelectedItemsCommand.ExecuteAsync(parameter: null).ConfigureAwait(false);
+ }
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerViewModel.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerViewModel.cs
new file mode 100644
index 00000000..0dcd71ab
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ProjectExplorerViewModel.cs
@@ -0,0 +1,262 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using CommunityToolkit.Mvvm.Input;
+using DroidNet.Controls;
+using DroidNet.TimeMachine;
+using DroidNet.TimeMachine.Changes;
+using Oxygen.Editor.Projects;
+using Oxygen.Editor.WorldEditor.Views;
+
+///
+/// The ViewModel for the view.
+///
+public partial class ProjectExplorerViewModel : DynamicTreeViewModel
+{
+ private readonly IProjectManagerService projectManager;
+
+ public ProjectExplorerViewModel(IProjectManagerService projectManager)
+ {
+ this.projectManager = projectManager;
+
+ // TODO: subscribe to CurrentProject property changes
+
+ this.UndoStack = UndoRedo.Default[this].UndoStack;
+ this.RedoStack = UndoRedo.Default[this].RedoStack;
+
+ this.ItemBeingRemoved += this.OnItemBeingRemoved;
+ this.ItemRemoved += this.OnItemRemoved;
+
+ this.ItemBeingAdded += this.OnItemBeingAdded;
+ this.ItemAdded += this.OnItemAdded;
+ }
+
+ public ProjectAdapter? Project { get; private set; }
+
+ public ReadOnlyObservableCollection UndoStack { get; }
+
+ public ReadOnlyObservableCollection RedoStack { get; }
+
+ private bool HasUnlockedSelectedItems { get; set; }
+
+ protected override void OnSelectionModelChanged(SelectionModel? oldValue)
+ {
+ base.OnSelectionModelChanged(oldValue);
+
+ if (oldValue is not null)
+ {
+ oldValue.PropertyChanged -= this.SelectionModel_OnPropertyChanged;
+ }
+
+ if (this.SelectionModel is not null)
+ {
+ this.SelectionModel.PropertyChanged += this.SelectionModel_OnPropertyChanged;
+ }
+ }
+
+ [RelayCommand(CanExecute = nameof(HasUnlockedSelectedItems))]
+ protected override async Task RemoveSelectedItems()
+ {
+ UndoRedo.Default[this].BeginChangeSet($"Remove {this.SelectionModel}");
+ await base.RemoveSelectedItems().ConfigureAwait(false);
+ UndoRedo.Default[this].EndChangeSet();
+ }
+
+ private void OnItemAdded(object? sender, ItemAddedEventArgs args)
+ {
+ _ = sender; // unused
+
+ UndoRedo.Default[this]
+ .AddChange(
+ $"RemoveItem({args.TreeItem.Label})",
+ () => this.RemoveItem(args.TreeItem).GetAwaiter().GetResult());
+ }
+
+ private void OnItemRemoved(object? sender, ItemRemovedEventArgs args)
+ {
+ _ = sender; // unused
+
+ UndoRedo.Default[this]
+ .AddChange(
+ $"Add Item({args.TreeItem.Label})",
+ () => this.AddItem(args.Parent, args.TreeItem).GetAwaiter().GetResult());
+ }
+
+ [RelayCommand]
+ private void Undo() => UndoRedo.Default[this].Undo();
+
+ [RelayCommand]
+ private void Redo() => UndoRedo.Default[this].Redo();
+
+ private void SelectionModel_OnPropertyChanged(object? sender, PropertyChangedEventArgs args)
+ {
+ if (!string.Equals(args.PropertyName, nameof(SelectionModel.IsEmpty), StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ this.AddEntityCommand.NotifyCanExecuteChanged();
+
+ var hasUnlockedSelectedItems = false;
+ switch (this.SelectionModel)
+ {
+ case SingleSelectionModel:
+ hasUnlockedSelectedItems = this.SelectionModel.SelectedItem?.IsLocked == false;
+ break;
+
+ case MultipleSelectionModel multipleSelectionModel:
+ foreach (var selectedIndex in multipleSelectionModel.SelectedIndices)
+ {
+ var item = this.ShownItems[selectedIndex];
+ hasUnlockedSelectedItems = !item.IsLocked;
+ if (hasUnlockedSelectedItems)
+ {
+ break;
+ }
+ }
+
+ break;
+
+ default:
+ // Keep it false
+ break;
+ }
+
+ if (hasUnlockedSelectedItems != this.HasUnlockedSelectedItems)
+ {
+ this.HasUnlockedSelectedItems = hasUnlockedSelectedItems;
+ this.RemoveSelectedItemsCommand.NotifyCanExecuteChanged();
+ }
+ }
+
+ private void OnItemBeingAdded(object? sender, ItemBeingAddedEventArgs args)
+ {
+ _ = sender; // unused
+
+ switch (args.TreeItem)
+ {
+ case SceneAdapter sceneAdapter:
+ {
+ var scene = sceneAdapter.AttachedObject;
+ var parentAdapter = args.Parent as ProjectAdapter;
+ Debug.Assert(parentAdapter is not null, "the parent of a SceneAdpater must be a ProjectAdapter");
+ var project = parentAdapter.AttachedObject;
+ project.Scenes.Add(scene);
+ break;
+ }
+
+ case EntityAdapter entityAdapter:
+ {
+ var entity = entityAdapter.AttachedObject;
+ var parentAdapter = args.Parent as SceneAdapter;
+ Debug.Assert(parentAdapter is not null, "the parent of a EntityAdapter must be a SceneAdapter");
+ var scene = parentAdapter.AttachedObject;
+ scene.Entities.Add(entity);
+ break;
+ }
+
+ default:
+ // Do nothing
+ break;
+ }
+ }
+
+ private void OnItemBeingRemoved(object? sender, ItemBeingRemovedEventArgs args)
+ {
+ _ = sender; // unused
+
+ Debug.Assert(args.TreeItem.Parent is not null, "any item in the tree should have a parent");
+ switch (args.TreeItem)
+ {
+ case SceneAdapter sceneAdapter:
+ {
+ var scene = sceneAdapter.AttachedObject;
+ var parentAdapter = sceneAdapter.Parent as ProjectAdapter;
+ Debug.Assert(parentAdapter is not null, "the parent of a SceneAdpater must be a ProjectAdapter");
+ var project = parentAdapter.AttachedObject;
+ project.Scenes.Remove(scene);
+ break;
+ }
+
+ case EntityAdapter entityAdapter:
+ {
+ var entity = entityAdapter.AttachedObject;
+ var parentAdapter = entityAdapter.Parent as SceneAdapter;
+ Debug.Assert(parentAdapter is not null, "the parent of a EntityAdapter must be a SceneAdapter");
+ var scene = parentAdapter.AttachedObject;
+ scene.Entities.Remove(entity);
+ break;
+ }
+
+ default:
+ // Do nothing
+ break;
+ }
+ }
+
+ [RelayCommand]
+ private async Task LoadProjectAsync()
+ {
+ var currentProject = this.projectManager.CurrentProject;
+ if (currentProject is not Projects.Project project)
+ {
+ this.Project = null;
+ return;
+ }
+
+ this.Project = new ProjectAdapter(project, this.projectManager);
+ await this.InitializeRootAsync(this.Project).ConfigureAwait(false);
+ }
+
+ [RelayCommand]
+ private async Task AddScene()
+ {
+ if (this.Project is null)
+ {
+ return;
+ }
+
+ var newScene = new SceneAdapter(
+ new Scene($"New Scene {this.Project.AttachedObject.Scenes.Count}", this.Project.AttachedObject),
+ this.projectManager);
+
+ await this.AddItem(this.Project, newScene).ConfigureAwait(false);
+ }
+
+ private bool CanAddEntity()
+ => (this.SelectionModel is SingleSelectionModel && this.SelectionModel.SelectedIndex != -1) ||
+ this.SelectionModel is MultipleSelectionModel { SelectedIndices.Count: 1 };
+
+ [RelayCommand(CanExecute = nameof(CanAddEntity))]
+ private async Task AddEntity()
+ {
+ var selectedItem = this.SelectionModel?.SelectedItem;
+ if (selectedItem is null)
+ {
+ return;
+ }
+
+ var scene = selectedItem switch
+ {
+ SceneAdapter item => item,
+ EntityAdapter { Parent: SceneAdapter } entity => (SceneAdapter)entity.Parent,
+
+ // Anything else is not a valid item to which we can add an entity
+ _ => null,
+ };
+
+ if (scene is null)
+ {
+ return;
+ }
+
+ var newEntity = new EntityAdapter(new Entity($"New Entity {scene.AttachedObject.Entities.Count}"));
+ await this.AddItem(scene, newEntity).ConfigureAwait(false);
+ }
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/SceneAdapter.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/SceneAdapter.cs
new file mode 100644
index 00000000..ffec284d
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/SceneAdapter.cs
@@ -0,0 +1,46 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using DroidNet.Controls;
+using Oxygen.Editor.Projects;
+
+///
+/// A item adapter for the model class.
+///
+/// The object to wrap as a .
+/// The configured project manager service.
+public partial class SceneAdapter(Scene scene, IProjectManagerService projectManager)
+ : TreeItemAdapter, ITreeItem
+{
+ public override bool IsRoot => false;
+
+ public override string Label => scene.Name;
+
+ public Scene AttachedObject => scene;
+
+ protected override int GetChildrenCount() => scene.Entities.Count;
+
+ protected override async Task LoadChildren()
+ {
+ this.ClearChildren();
+
+ if (!await projectManager.LoadSceneEntitiesAsync(scene).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ foreach (var entity in this.AttachedObject.Entities)
+ {
+ this.AddChildInternal(
+ new EntityAdapter(entity)
+ {
+ IsExpanded = false,
+ });
+ }
+
+ await Task.CompletedTask.ConfigureAwait(true);
+ }
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/Styles.xaml b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/Styles.xaml
new file mode 100644
index 00000000..105497ef
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/Styles.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/Styles.xaml.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/Styles.xaml.cs
new file mode 100644
index 00000000..5a08d882
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/Styles.xaml.cs
@@ -0,0 +1,17 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using DroidNet.Controls;
+using Microsoft.UI.Xaml;
+
+///
+/// A for the styles used in the demo. Because the
+/// styles use {x:Bind}, they must be backed by a implemented in code behind.
+///
+public partial class Styles
+{
+ public Styles() => this.InitializeComponent();
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ThumbnailGenerator.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ThumbnailGenerator.cs
new file mode 100644
index 00000000..60ee3007
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ThumbnailGenerator.cs
@@ -0,0 +1,59 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using System.Runtime.InteropServices.WindowsRuntime;
+using DroidNet.Controls;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media.Imaging;
+
+/*
+ Pathless casting
+ ----------------
+ The native bind parser doesn't provide a keyword to represent this as a function parameter, but it does support
+ pathless casting(for example, { x: Bind(x: String)}), which can be used as a function parameter.Therefore, {x:Bind
+ MethodName((namespace:TypeOfThis))} is a valid way to perform what is conceptually equivalent to {x:Bind
+ MethodName(this)}.
+
+ The following works fine to use the current template item to call the function:
+
+
+
+
+*/
+
+public static class ThumbnailGenerator
+{
+ public static WriteableBitmap GenerateRandomImage(int width, int height)
+ {
+ var bitmap = new WriteableBitmap(width, height);
+
+ using var stream = bitmap.PixelBuffer.AsStream();
+ var pixels = new byte[width * height * 4]; // RGBA
+ var rand = new Random();
+
+ for (var i = 0; i < pixels.Length; i += 4)
+ {
+ pixels[i] = (byte)rand.Next(256); // R
+ pixels[i + 1] = (byte)rand.Next(256); // G
+ pixels[i + 2] = (byte)rand.Next(256); // B
+ pixels[i + 3] = 255; // A
+ }
+
+ stream.Write(pixels, 0, pixels.Length);
+
+ return bitmap;
+ }
+
+ public static Symbol GetThumbnailForEntity(TreeItemAdapter adapter)
+ {
+ if (adapter is EntityAdapter entityAdapter && entityAdapter.AttachedObject.Name.EndsWith('1'))
+ {
+ return Symbol.Home;
+ }
+
+ return Symbol.Admin;
+ }
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ThumbnailTemplateSelector.cs b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ThumbnailTemplateSelector.cs
new file mode 100644
index 00000000..52e16a9c
--- /dev/null
+++ b/projects/Oxygen.Editor.WorldEditor/src/ProjectExplorer/ThumbnailTemplateSelector.cs
@@ -0,0 +1,48 @@
+// Distributed under the MIT License. See accompanying file LICENSE or copy
+// at https://opensource.org/licenses/MIT.
+// SPDX-License-Identifier: MIT
+
+namespace Oxygen.Editor.WorldEditor.ProjectExplorer;
+
+using DroidNet.Controls;
+using Microsoft.UI.Xaml.Controls;
+using DataTemplate = Microsoft.UI.Xaml.DataTemplate;
+using DependencyObject = Microsoft.UI.Xaml.DependencyObject;
+
+///
+/// A that can map a to a template that can be used to display a
+/// for it.
+///
+public partial class ThumbnailTemplateSelector : DataTemplateSelector
+{
+#if false
+ public ThumbnailTemplateSelector()
+ {
+ const string xaml = """
+
+
+
+ """;
+
+ this.DefaultTemplate = (DataTemplate)XamlReader.Load(xaml);
+ }
+#endif
+
+ public DataTemplate? SceneTemplate { get; set; }
+
+ public DataTemplate? EntityTemplate { get; set; }
+
+ public DataTemplate? DefaultTemplate { get; set; }
+
+ protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
+ => item switch
+ {
+ SceneAdapter => this.SceneTemplate,
+ EntityAdapter => this.EntityTemplate,
+ _ => this.DefaultTemplate,
+ };
+}
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ViewModels/ProjectExplorerViewModel.cs b/projects/Oxygen.Editor.WorldEditor/src/ViewModels/ProjectExplorerViewModel.cs
deleted file mode 100644
index e9f904c4..00000000
--- a/projects/Oxygen.Editor.WorldEditor/src/ViewModels/ProjectExplorerViewModel.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// Distributed under the MIT License. See accompanying file LICENSE or copy
-// at https://opensource.org/licenses/MIT.
-// SPDX-License-Identifier: MIT
-
-namespace Oxygen.Editor.WorldEditor.ViewModels;
-
-using DroidNet.Hosting.Generators;
-using Microsoft.Extensions.DependencyInjection;
-
-[InjectAs(ServiceLifetime.Transient)]
-public class ProjectExplorerViewModel;
diff --git a/projects/Oxygen.Editor.WorldEditor/src/ViewModels/WorkspaceViewModel.cs b/projects/Oxygen.Editor.WorldEditor/src/ViewModels/WorkspaceViewModel.cs
index fd4a841f..41ad2ac5 100644
--- a/projects/Oxygen.Editor.WorldEditor/src/ViewModels/WorkspaceViewModel.cs
+++ b/projects/Oxygen.Editor.WorldEditor/src/ViewModels/WorkspaceViewModel.cs
@@ -12,6 +12,7 @@ namespace Oxygen.Editor.WorldEditor.ViewModels;
using DryIoc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
+using Oxygen.Editor.WorldEditor.ProjectExplorer;
///
/// The view model for the World Editor main view.
diff --git a/projects/Oxygen.Editor.WorldEditor/src/Views/ProjectExplorerView.xaml b/projects/Oxygen.Editor.WorldEditor/src/Views/ProjectExplorerView.xaml
deleted file mode 100644
index 6c7222d3..00000000
--- a/projects/Oxygen.Editor.WorldEditor/src/Views/ProjectExplorerView.xaml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/projects/Oxygen.Editor.WorldEditor/src/Views/ProjectExplorerView.xaml.cs b/projects/Oxygen.Editor.WorldEditor/src/Views/ProjectExplorerView.xaml.cs
deleted file mode 100644
index 96b57aab..00000000
--- a/projects/Oxygen.Editor.WorldEditor/src/Views/ProjectExplorerView.xaml.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Distributed under the MIT License. See accompanying file LICENSE or copy
-// at https://opensource.org/licenses/MIT.
-// SPDX-License-Identifier: MIT
-
-namespace Oxygen.Editor.WorldEditor.Views;
-
-using DroidNet.Hosting.Generators;
-using DroidNet.Mvvm.Generators;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.UI.Xaml.Controls;
-using Oxygen.Editor.WorldEditor.ViewModels;
-
-[ViewModel(typeof(ProjectExplorerViewModel))]
-[InjectAs(ServiceLifetime.Transient)]
-public sealed partial class ProjectExplorerView : UserControl
-{
- public ProjectExplorerView() => this.InitializeComponent();
-}
diff --git a/projects/Oxygen.Editor/src/Program.cs b/projects/Oxygen.Editor/src/Program.cs
index 4de706b2..6a5d2ddd 100644
--- a/projects/Oxygen.Editor/src/Program.cs
+++ b/projects/Oxygen.Editor/src/Program.cs
@@ -33,9 +33,15 @@ namespace Oxygen.Editor;
using Oxygen.Editor.ProjectBrowser.Projects;
using Oxygen.Editor.ProjectBrowser.Templates;
using Oxygen.Editor.ProjectBrowser.ViewModels;
+using Oxygen.Editor.Projects;
+using Oxygen.Editor.Projects.Config;
+using Oxygen.Editor.Projects.Storage;
using Oxygen.Editor.Services;
using Oxygen.Editor.Shell;
+using Oxygen.Editor.Storage;
using Oxygen.Editor.Storage.Native;
+using Oxygen.Editor.WorldEditor.ProjectExplorer;
+using Oxygen.Editor.WorldEditor.Views;
using Serilog;
using Serilog.Events;
using Serilog.Templates;
@@ -178,14 +184,18 @@ private static void RegisterApplicationServices(HostBuilderContext context, DryI
serviceKey: Uri.UriSchemeFile);
sp.Container.Register(Reuse.Singleton);
+ // TODO: use keyed registration and parameter name to key mappings
+ // https://github.com/dadhi/DryIoc/blob/master/docs/DryIoc.Docs/SpecifyDependencyAndPrimitiveValues.md#complete-example-of-matching-the-parameter-name-to-the-service-key
+ sp.Container.Register(Reuse.Singleton);
sp.Container.Register(Reuse.Singleton);
sp.Container.Register(Reuse.Singleton);
sp.Container.Register(Reuse.Singleton);
+ sp.Container.Register(Reuse.Singleton);
// Register the project instance using a delegate that will request the currently open project from the project
// browser service.
sp.Container.RegisterDelegate(
- resolverContext => resolverContext.Resolve().CurrentProject);
+ resolverContext => resolverContext.Resolve().CurrentProject);
/*
* Set up the view model to view converters. We're using the standard converter, and a custom one with fall back
@@ -210,6 +220,8 @@ private static void RegisterApplicationServices(HostBuilderContext context, DryI
// TODO(abdes): refactor into extension method
sp.Container.Register(Reuse.Transient);
sp.Container.Register(Reuse.Transient);
+ sp.Container.Register(Reuse.Transient);
+ sp.Container.Register(Reuse.Transient);
}
private static Routes MakeRoutes() => new(
@@ -274,14 +286,20 @@ private static void AddConfigurationFiles(HostBuilderContext context, IConfigura
var projectBrowserConfigPath = Path.GetFullPath(
$"{Assembly.GetAssembly(typeof(ProjectBrowserSettings))!.GetName().Name}/Config/ProjectBrowser.config.json",
Finder.ProgramData);
+ var categoriesConfigPath = Path.GetFullPath(
+ $"{Assembly.GetAssembly(typeof(ProjectsSettings))!.GetName().Name}/Config/Categories.config.json",
+ Finder.ProgramData);
+
_ = config.AddJsonFile(localSettingsPath, optional: true)
- .AddJsonFile(projectBrowserConfigPath);
+ .AddJsonFile(projectBrowserConfigPath)
+ .AddJsonFile(categoriesConfigPath);
}
private static void ConfigureOptionsPattern(HostBuilderContext context, IServiceCollection sc)
{
_ = sc.Configure(
context.Configuration.GetSection(ProjectBrowserSettings.ConfigSectionName));
+ _ = sc.Configure(context.Configuration.GetSection(ProjectsSettings.ConfigSectionName));
_ = sc.ConfigureWritable(
context.Configuration.GetSection(nameof(ThemeSettings)),
Path.Combine(Finder.LocalAppData, "LocalSettings.json"));