using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.PropertyGrid.Models; using Avalonia.PropertyGrid.Services; namespace Avalonia.PropertyGrid.Controls; /// /// A PropertyGrid control for editing object properties with Xceed-like functionality /// public class PropertyGrid : TemplatedControl { private object? _selectedObject; private string _searchText = string.Empty; /// /// Defines the SelectedObject property. /// public static readonly StyledProperty SelectedObjectProperty = AvaloniaProperty.Register(nameof(SelectedObject)); /// /// Defines the SearchText property. /// public static readonly StyledProperty SearchTextProperty = AvaloniaProperty.Register(nameof(SearchText), string.Empty); /// /// Defines the ShowCategories property. /// public static readonly StyledProperty ShowCategoriesProperty = AvaloniaProperty.Register(nameof(ShowCategories), true); /// /// Defines the ShowDescriptions property. /// public static readonly StyledProperty ShowDescriptionsProperty = AvaloniaProperty.Register(nameof(ShowDescriptions), true); /// /// Defines the FilteredProperties property. /// public static readonly StyledProperty> FilteredPropertiesProperty = AvaloniaProperty.Register>( nameof(FilteredProperties), new ObservableCollection()); /// /// Defines the CategoryGroups property. /// public static readonly StyledProperty> CategoryGroupsProperty = AvaloniaProperty.Register>( nameof(CategoryGroups), new ObservableCollection()); static PropertyGrid() { SelectedObjectProperty.Changed.AddClassHandler((grid, args) => grid.OnSelectedObjectChanged(args)); SearchTextProperty.Changed.AddClassHandler((grid, args) => grid.OnSearchTextChanged(args)); } /// /// Initializes a new instance of the PropertyGrid class. /// public PropertyGrid() { FilteredProperties = new ObservableCollection(); } /// /// The object whose properties are being edited /// public object? SelectedObject { get => GetValue(SelectedObjectProperty); set => SetValue(SelectedObjectProperty, value); } /// /// The search/filter text /// public string SearchText { get => GetValue(SearchTextProperty); set => SetValue(SearchTextProperty, value); } /// /// Whether to show properties grouped by categories /// public bool ShowCategories { get => GetValue(ShowCategoriesProperty); set => SetValue(ShowCategoriesProperty, value); } /// /// Whether to show property descriptions /// public bool ShowDescriptions { get => GetValue(ShowDescriptionsProperty); set => SetValue(ShowDescriptionsProperty, value); } /// /// The filtered list of properties to display /// public ObservableCollection FilteredProperties { get => GetValue(FilteredPropertiesProperty); set => SetValue(FilteredPropertiesProperty, value); } /// /// The category groups to display /// public ObservableCollection CategoryGroups { get => GetValue(CategoryGroupsProperty); set => SetValue(CategoryGroupsProperty, value); } private List _allProperties = new(); private void OnSelectedObjectChanged(AvaloniaPropertyChangedEventArgs args) { _selectedObject = args.NewValue; System.Diagnostics.Debug.WriteLine($"PropertyGrid: SelectedObject changed to {_selectedObject?.GetType().Name ?? "null"}"); RefreshProperties(); } private void OnSearchTextChanged(AvaloniaPropertyChangedEventArgs args) { _searchText = args.NewValue as string ?? string.Empty; FilterProperties(); } /// /// Refreshes the property list from the selected object /// public void RefreshProperties() { _allProperties.Clear(); FilteredProperties.Clear(); if (_selectedObject == null) { System.Diagnostics.Debug.WriteLine("PropertyGrid: RefreshProperties - SelectedObject is null"); return; } _allProperties = PropertyItemFactory.CreatePropertyItems(_selectedObject); System.Diagnostics.Debug.WriteLine($"PropertyGrid: Created {_allProperties.Count} property items"); // Subscribe to property changes foreach (var item in _allProperties) { item.PropertyChanged += OnPropertyItemChanged; } FilterProperties(); System.Diagnostics.Debug.WriteLine($"PropertyGrid: FilteredProperties count = {FilteredProperties.Count}"); } private void OnPropertyItemChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PropertyItem.IsExpanded) && sender is PropertyItem item) { HandlePropertyExpansion(item); } } private void HandlePropertyExpansion(PropertyItem item) { System.Diagnostics.Debug.WriteLine($"HandlePropertyExpansion: {item.DisplayName}, IsExpanded={item.IsExpanded}, Children.Count={item.Children.Count}"); if (item.IsExpanded && item.Value != null) { // Load child properties if not already loaded if (item.Children.Count == 0) { item.Children = PropertyItemFactory.CreatePropertyItems(item.Value, item.IndentLevel + 1); System.Diagnostics.Debug.WriteLine($" Loaded {item.Children.Count} children for {item.DisplayName}"); // Subscribe to property changes for children foreach (var child in item.Children) { child.PropertyChanged += OnPropertyItemChanged; } } // Insert children after the parent in the filtered list var parentIndex = FilteredProperties.IndexOf(item); if (parentIndex >= 0) { int insertedCount = 0; for (int i = 0; i < item.Children.Count; i++) { // Only insert if not already in the list if (!FilteredProperties.Contains(item.Children[i])) { FilteredProperties.Insert(parentIndex + i + 1, item.Children[i]); insertedCount++; } } System.Diagnostics.Debug.WriteLine($" Inserted {insertedCount} children into FilteredProperties at index {parentIndex + 1}"); } // Also insert into CategoryGroups if ShowCategories is enabled if (ShowCategories) { foreach (var categoryGroup in CategoryGroups) { var itemIndexInCategory = categoryGroup.Properties.IndexOf(item); if (itemIndexInCategory >= 0) { // Insert children after the parent in the category for (int i = 0; i < item.Children.Count; i++) { if (!categoryGroup.Properties.Contains(item.Children[i])) { categoryGroup.Properties.Insert(itemIndexInCategory + i + 1, item.Children[i]); } } System.Diagnostics.Debug.WriteLine($" Inserted {item.Children.Count} children into category '{categoryGroup.CategoryName}'"); break; } } } } else if (!item.IsExpanded) { // Remove children from the filtered list (but keep them in item.Children for re-expansion) int removedCount = 0; foreach (var child in item.Children) { if (FilteredProperties.Remove(child)) { removedCount++; } // If child is also expanded, recursively collapse it if (child.IsExpanded) { child.IsExpanded = false; } } System.Diagnostics.Debug.WriteLine($" Removed {removedCount} children from FilteredProperties"); // Also remove from CategoryGroups if ShowCategories is enabled if (ShowCategories) { foreach (var categoryGroup in CategoryGroups) { if (categoryGroup.Properties.Contains(item)) { int categoryRemovedCount = 0; foreach (var child in item.Children) { if (categoryGroup.Properties.Remove(child)) { categoryRemovedCount++; } } System.Diagnostics.Debug.WriteLine($" Removed {categoryRemovedCount} children from category '{categoryGroup.CategoryName}'"); break; } } } } } private void FilterProperties() { FilteredProperties.Clear(); CategoryGroups.Clear(); var filtered = _allProperties.AsEnumerable(); // Apply search filter if (!string.IsNullOrWhiteSpace(_searchText)) { filtered = filtered.Where(p => p.DisplayName.Contains(_searchText, StringComparison.OrdinalIgnoreCase) || (p.Category?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false) || (p.Description?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false)); } // Populate flat list foreach (var item in filtered) { FilteredProperties.Add(item); } System.Diagnostics.Debug.WriteLine($"PropertyGrid: FilterProperties - ShowCategories={ShowCategories}, filtered count={filtered.Count()}"); // Group by category for category view if (ShowCategories) { var groupedByCategory = filtered .GroupBy(p => p.Category ?? "Misc") .OrderBy(g => g.Key); foreach (var group in groupedByCategory) { var categoryGroup = new CategoryGroup { CategoryName = group.Key }; foreach (var item in group.OrderBy(p => p.Order).ThenBy(p => p.DisplayName)) { categoryGroup.Properties.Add(item); } CategoryGroups.Add(categoryGroup); System.Diagnostics.Debug.WriteLine($"PropertyGrid: Added category '{categoryGroup.CategoryName}' with {categoryGroup.PropertyCount} properties"); } System.Diagnostics.Debug.WriteLine($"PropertyGrid: Total CategoryGroups={CategoryGroups.Count}"); } } /// /// Refreshes all property values from the source object /// public void RefreshValues() { foreach (var item in _allProperties) { item.RefreshValue(); } } }