344 lines
12 KiB
C#
344 lines
12 KiB
C#
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;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// A PropertyGrid control for editing object properties with Xceed-like functionality
|
||
|
|
/// </summary>
|
||
|
|
public class PropertyGrid : TemplatedControl
|
||
|
|
{
|
||
|
|
private object? _selectedObject;
|
||
|
|
private string _searchText = string.Empty;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Defines the SelectedObject property.
|
||
|
|
/// </summary>
|
||
|
|
public static readonly StyledProperty<object?> SelectedObjectProperty =
|
||
|
|
AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Defines the SearchText property.
|
||
|
|
/// </summary>
|
||
|
|
public static readonly StyledProperty<string> SearchTextProperty =
|
||
|
|
AvaloniaProperty.Register<PropertyGrid, string>(nameof(SearchText), string.Empty);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Defines the ShowCategories property.
|
||
|
|
/// </summary>
|
||
|
|
public static readonly StyledProperty<bool> ShowCategoriesProperty =
|
||
|
|
AvaloniaProperty.Register<PropertyGrid, bool>(nameof(ShowCategories), true);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Defines the ShowDescriptions property.
|
||
|
|
/// </summary>
|
||
|
|
public static readonly StyledProperty<bool> ShowDescriptionsProperty =
|
||
|
|
AvaloniaProperty.Register<PropertyGrid, bool>(nameof(ShowDescriptions), true);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Defines the FilteredProperties property.
|
||
|
|
/// </summary>
|
||
|
|
public static readonly StyledProperty<ObservableCollection<PropertyItem>> FilteredPropertiesProperty =
|
||
|
|
AvaloniaProperty.Register<PropertyGrid, ObservableCollection<PropertyItem>>(
|
||
|
|
nameof(FilteredProperties),
|
||
|
|
new ObservableCollection<PropertyItem>());
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Defines the CategoryGroups property.
|
||
|
|
/// </summary>
|
||
|
|
public static readonly StyledProperty<ObservableCollection<CategoryGroup>> CategoryGroupsProperty =
|
||
|
|
AvaloniaProperty.Register<PropertyGrid, ObservableCollection<CategoryGroup>>(
|
||
|
|
nameof(CategoryGroups),
|
||
|
|
new ObservableCollection<CategoryGroup>());
|
||
|
|
|
||
|
|
static PropertyGrid()
|
||
|
|
{
|
||
|
|
SelectedObjectProperty.Changed.AddClassHandler<PropertyGrid>((grid, args) =>
|
||
|
|
grid.OnSelectedObjectChanged(args));
|
||
|
|
|
||
|
|
SearchTextProperty.Changed.AddClassHandler<PropertyGrid>((grid, args) =>
|
||
|
|
grid.OnSearchTextChanged(args));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Initializes a new instance of the PropertyGrid class.
|
||
|
|
/// </summary>
|
||
|
|
public PropertyGrid()
|
||
|
|
{
|
||
|
|
FilteredProperties = new ObservableCollection<PropertyItem>();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The object whose properties are being edited
|
||
|
|
/// </summary>
|
||
|
|
public object? SelectedObject
|
||
|
|
{
|
||
|
|
get => GetValue(SelectedObjectProperty);
|
||
|
|
set => SetValue(SelectedObjectProperty, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The search/filter text
|
||
|
|
/// </summary>
|
||
|
|
public string SearchText
|
||
|
|
{
|
||
|
|
get => GetValue(SearchTextProperty);
|
||
|
|
set => SetValue(SearchTextProperty, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Whether to show properties grouped by categories
|
||
|
|
/// </summary>
|
||
|
|
public bool ShowCategories
|
||
|
|
{
|
||
|
|
get => GetValue(ShowCategoriesProperty);
|
||
|
|
set => SetValue(ShowCategoriesProperty, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Whether to show property descriptions
|
||
|
|
/// </summary>
|
||
|
|
public bool ShowDescriptions
|
||
|
|
{
|
||
|
|
get => GetValue(ShowDescriptionsProperty);
|
||
|
|
set => SetValue(ShowDescriptionsProperty, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The filtered list of properties to display
|
||
|
|
/// </summary>
|
||
|
|
public ObservableCollection<PropertyItem> FilteredProperties
|
||
|
|
{
|
||
|
|
get => GetValue(FilteredPropertiesProperty);
|
||
|
|
set => SetValue(FilteredPropertiesProperty, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The category groups to display
|
||
|
|
/// </summary>
|
||
|
|
public ObservableCollection<CategoryGroup> CategoryGroups
|
||
|
|
{
|
||
|
|
get => GetValue(CategoryGroupsProperty);
|
||
|
|
set => SetValue(CategoryGroupsProperty, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
private List<PropertyItem> _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();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Refreshes the property list from the selected object
|
||
|
|
/// </summary>
|
||
|
|
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))
|
||
|
|
{
|
||
|
|
var searchLower = _searchText.ToLowerInvariant();
|
||
|
|
filtered = filtered.Where(p =>
|
||
|
|
p.DisplayName.ToLowerInvariant().Contains(searchLower) ||
|
||
|
|
(p.Category?.ToLowerInvariant().Contains(searchLower) ?? false) ||
|
||
|
|
(p.Description?.ToLowerInvariant().Contains(searchLower) ?? 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}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Refreshes all property values from the source object
|
||
|
|
/// </summary>
|
||
|
|
public void RefreshValues()
|
||
|
|
{
|
||
|
|
foreach (var item in _allProperties)
|
||
|
|
{
|
||
|
|
item.RefreshValue();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|