Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions src/TextForge.Desktop/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,10 +16,12 @@ namespace TextForge.Desktop.ViewModels;

public partial class MainViewModel : ObservableObject
{
public ObservableCollection<ModuleViewModel> ModuleItems { get; } = [];
public IDocumentEngine Engine { get; } = new DocumentEngine();
public DefaultTemplate Template { get; } = new();

[ObservableProperty]

public partial Document CurrentDocument { get; set; }

// --- NEW DRAWER STATE ---
Expand All @@ -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
Expand All @@ -52,14 +111,14 @@ private void CloseDrawer()
{
ActiveTab = string.Empty;
}

public IReadOnlyList<ModuleDefinition> AvailableModules { get; } = ModuleRegistry.GetAvailableModules();
[RelayCommand]
private void ClearAllModules()
{
CurrentDocument.ClearModules();
}

[ObservableProperty]
public partial string ExportStatusMessage { get; set; } = "Status: Ready";

Expand Down Expand Up @@ -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);
}

}
[RelayCommand]
private void SelectRoot()
{
CurrentDocument.SelectRoot();
}
}
128 changes: 128 additions & 0 deletions src/TextForge.Desktop/ViewModels/ModuleViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<ModuleViewModel> 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();
}
}
}
51 changes: 24 additions & 27 deletions src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mod="using:TextForge.Core.Modules"
xmlns:vm="using:TextForge.Desktop.ViewModels"
x:Class="TextForge.Desktop.Views.Components.ModuleEditorView"
x:DataType="vm:MainViewModel">
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">

<UserControl.Styles>
<Style Selector="ListBoxItem">
Expand Down Expand Up @@ -60,7 +59,7 @@

<UserControl.Resources>
<!-- Self-Referencing Recursive Template for Modules & Submodules at any depth -->
<DataTemplate x:Key="RecursiveModuleTemplate" x:DataType="mod:Module">
<DataTemplate x:Key="RecursiveModuleTemplate" x:DataType="vm:ModuleViewModel">
<Border Classes="module-card"
Classes.selected="{Binding IsSelected}"
Tag="{Binding}"
Expand Down Expand Up @@ -100,31 +99,29 @@
<!-- Action Buttons -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="2"
Margin="4,0,0,0">
<Button Content="▲" FontSize="10" Width="22" Height="22" Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="Transparent" Foreground="#6B7280"
ToolTip.Tip="Move Up"
Tag="{Binding}" Click="MoveUpButton_Click" />
<Button Content="▼" FontSize="10" Width="22" Height="22" Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="Transparent" Foreground="#6B7280"
ToolTip.Tip="Move Down"
Tag="{Binding}" Click="MoveDownButton_Click" />
<Button Content="✕" FontSize="11" Width="22" Height="22" Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding DeleteCommand}"
Background="Transparent" Foreground="#EF4444"
ToolTip.Tip="Delete Module"
Tag="{Binding}" Click="DeleteModuleButton_Click" />
Tag="{Binding}" />
<Button Content="⊘" FontSize="14" FontWeight="Bold" Width="22"
Height="22" Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="Transparent" Foreground="#F59E0B"
ToolTip.Tip="Detach from layout"
Tag="{Binding}" Click="DetachModuleButton_Click" />
Height="22" Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding DetachCommand}"
Background="Transparent" Foreground="#F59E0B"
ToolTip.Tip="Detach from layout"
Tag="{Binding}" />
<Button Content="+" FontSize="14" FontWeight="Bold" Width="22"
Height="22" Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding CloneCommand}"
Background="Transparent" Foreground="#2563EB"
ToolTip.Tip="Clone Module"
Tag="{Binding}" />

</StackPanel>
</Grid>
Expand All @@ -138,7 +135,7 @@

<!-- Recursive ItemsControl: uses the exact same template for child
submodules -->
<ItemsControl ItemsSource="{Binding SubModules}"
<ItemsControl ItemsSource="{Binding Children}"
ItemTemplate="{StaticResource RecursiveModuleTemplate}"
Margin="14,2,0,0" />
</StackPanel>
Expand All @@ -158,7 +155,7 @@
<!-- Modules List (Using ItemsControl to prevent VirtualizingStackPanel focus scroll jumps) -->
<ItemsControl Name="ModuleListBox"
Background="Transparent"
ItemsSource="{Binding CurrentDocument.Modules}"
ItemsSource="{Binding ModuleItems}"
ItemTemplate="{StaticResource RecursiveModuleTemplate}" />
</StackPanel>
</ScrollViewer>
Expand Down
12 changes: 3 additions & 9 deletions src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using TextForge.Core;
using TextForge.Core.Documents;
using TextForge.Core.Modules;
using TextForge.Desktop.ViewModels;
Expand Down Expand Up @@ -52,19 +51,17 @@ private void DocumentRootCard_PointerPressed(object? sender, PointerPressedEvent
}
private void ModuleCard_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is Border border && border.DataContext is Module clickedModule)
if (sender is Border border && border.DataContext is ModuleViewModel clickedModule)
{
// 1. Grab MainViewModel from ModuleEditorView (this), NOT the border's DataContext
if (DataContext is MainViewModel vm)
{
vm.CurrentDocument.SelectModule(clickedModule);
vm.CurrentDocument.SelectModule(clickedModule.Model);
}
else if (this.FindAncestorOfType<Window>()?.DataContext is MainViewModel windowVm)
{
windowVm.CurrentDocument.SelectModule(clickedModule);
windowVm.CurrentDocument.SelectModule(clickedModule.Model);
}

// 2. Stop event from bubbling up to DocumentRootCard_PointerPressed
e.Handled = true;
}
}
Expand Down Expand Up @@ -124,8 +121,5 @@ public void SetDocument(Document document)
titleBlock.Text = $"{_document.Metadata.Title} (Root)";
}

// Assign the collection directly ONCE.
// ObservableCollection will handle all subsequent Add/Remove operations automatically.
ModuleListBox.ItemsSource = _document.Modules;
}
}
Loading