diff --git a/src/TextForge.Desktop/ViewModels/MainViewModel.cs b/src/TextForge.Desktop/ViewModels/MainViewModel.cs index 0d1cba7..74dec7b 100644 --- a/src/TextForge.Desktop/ViewModels/MainViewModel.cs +++ b/src/TextForge.Desktop/ViewModels/MainViewModel.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -13,10 +16,12 @@ namespace TextForge.Desktop.ViewModels; public partial class MainViewModel : ObservableObject { + public ObservableCollection ModuleItems { get; } = []; public IDocumentEngine Engine { get; } = new DocumentEngine(); public DefaultTemplate Template { get; } = new(); [ObservableProperty] + public partial Document CurrentDocument { get; set; } // --- NEW DRAWER STATE --- @@ -33,6 +38,60 @@ public MainViewModel() CurrentDocument = ShowcaseDocumentFactory.Create(); } + partial void OnCurrentDocumentChanged(Document value) + { + value.Modules.CollectionChanged += OnRootModulesChanged; + SyncRootModuleItems(value); + } + + private void SyncRootModuleItems(Document document) + { + ModuleItems.Clear(); + foreach (var module in document.Modules) + { + ModuleItems.Add(new ModuleViewModel(module, document)); + } + } + + private void OnRootModulesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null) + { + var index = e.NewStartingIndex; + foreach (Module module in e.NewItems) + { + var item = new ModuleViewModel(module, CurrentDocument); + if (index >= 0) + { + ModuleItems.Insert(index++, item); + } + else + { + ModuleItems.Add(item); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems is not null) + { + foreach (Module module in e.OldItems) + { + var item = ModuleItems.FirstOrDefault(existing => existing.Model == module); + if (item is not null) + { + ModuleItems.Remove(item); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Move) + { + ModuleItems.Move(e.OldStartingIndex, e.NewStartingIndex); + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + ModuleItems.Clear(); + } + } + partial void OnActiveTabChanged(string value) { // Notify XAML of derived visibility changes when ActiveTab mutates @@ -52,14 +111,14 @@ private void CloseDrawer() { ActiveTab = string.Empty; } - + public IReadOnlyList AvailableModules { get; } = ModuleRegistry.GetAvailableModules(); [RelayCommand] private void ClearAllModules() { CurrentDocument.ClearModules(); } - + [ObservableProperty] public partial string ExportStatusMessage { get; set; } = "Status: Ready"; @@ -95,8 +154,12 @@ private void AddModule(ModuleDefinition definition) { var newModule = definition.Create(); var targetParent = CurrentDocument.IsRootSelected ? null : CurrentDocument.SelectedModule; - + CurrentDocument.AddModule(newModule, parent: targetParent); } - -} \ No newline at end of file + [RelayCommand] + private void SelectRoot() + { + CurrentDocument.SelectRoot(); + } +} diff --git a/src/TextForge.Desktop/ViewModels/ModuleViewModel.cs b/src/TextForge.Desktop/ViewModels/ModuleViewModel.cs new file mode 100644 index 0000000..c388a76 --- /dev/null +++ b/src/TextForge.Desktop/ViewModels/ModuleViewModel.cs @@ -0,0 +1,128 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using TextForge.Core.Documents; +using TextForge.Core.Modules; + +namespace TextForge.Desktop.ViewModels; + +public partial class ModuleViewModel : ObservableObject +{ + public Module Model { get; } + public ObservableCollection Children { get; } = []; + + private readonly Document _document; + + public ModuleViewModel(Module model, Document document) + { + Model = model; + _document = document; + + Model.PropertyChanged += OnModelPropertyChanged; + Model.SubModules.CollectionChanged += OnSubModulesChanged; + + foreach (var child in Model.SubModules) + { + Children.Add(new ModuleViewModel(child, document)); + } + } + + public string Name => Model.Name; + public string? Content + { + get => Model.Content; + set => Model.Content = value ?? string.Empty; + } + + public string TruncatedPreviewContent => Model.TruncatedPreviewContent; + public bool IsSelected => Model.IsSelected; + + public bool IsExpanded + { + get => Model.IsExpanded; + set => Model.IsExpanded = value; + } + + [RelayCommand] + private void Delete() + { + _document.RemoveModule(Model); + } + + [RelayCommand] + private void MoveUp() + { + _document.MoveModuleUp(Model); + } + + [RelayCommand] + private void MoveDown() + { + _document.MoveModuleDown(Model); + } + + [RelayCommand] + private void Detach() + { + _document.DetachModule(Model); + } + + [RelayCommand] + private void Clone() + { + _document.DuplicateModule(Model); + } + + private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(Module.Content) + or nameof(Module.TruncatedPreviewContent) + or nameof(Module.IsSelected) + or nameof(Module.IsExpanded)) + { + OnPropertyChanged(e.PropertyName); + } + } + + private void OnSubModulesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null) + { + var index = e.NewStartingIndex; + foreach (Module child in e.NewItems) + { + var childViewModel = new ModuleViewModel(child, _document); + if (index >= 0) + { + Children.Insert(index++, childViewModel); + } + else + { + Children.Add(childViewModel); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems is not null) + { + foreach (Module child in e.OldItems) + { + var childViewModel = Children.FirstOrDefault(item => item.Model == child); + if (childViewModel is not null) + { + Children.Remove(childViewModel); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Move) + { + Children.Move(e.OldStartingIndex, e.NewStartingIndex); + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + Children.Clear(); + } + } +} \ No newline at end of file diff --git a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml index 9a7e54b..0a57eaa 100644 --- a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml +++ b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml @@ -1,9 +1,8 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:vm="clr-namespace:TextForge.Desktop.ViewModels" + x:Class="TextForge.Desktop.Views.Components.ModuleEditorView" + x:DataType="vm:MainViewModel">