From 0233a4f46d6236a472ba7e86ed1ca02807037e4e Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 4 Dec 2023 14:35:38 -0500 Subject: [PATCH] Add PropertyGrid and Dialog --- .../PropertyGrid/BetterPropertyGrid.cs | 70 ++++++ .../PropertyGrid/PropertyGridCultureData.cs | 35 +++ .../PropertyGridLocalizationService.cs | 36 +++ .../DesignData/DesignData.cs | 220 ++++++++---------- .../DesignData/MockPropertyGridObject.cs | 44 ++++ .../StabilityMatrix.Avalonia.csproj | 2 + .../Dialogs/PropertyGridViewModel.cs | 31 +++ .../Views/Dialogs/PropertyGridDialog.axaml | 22 ++ .../Views/Dialogs/PropertyGridDialog.axaml.cs | 54 +++++ .../Extensions/ObjectExtensions.cs | 101 +++++--- 10 files changed, 454 insertions(+), 161 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs create mode 100644 StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs create mode 100644 StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs create mode 100644 StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs new file mode 100644 index 00000000..07da92a5 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.PropertyGrid.Services; +using JetBrains.Annotations; +using PropertyModels.ComponentModel; +using StabilityMatrix.Core.Extensions; + +namespace StabilityMatrix.Avalonia.Controls; + +/// +[PublicAPI] +public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid +{ + protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid); + + static BetterPropertyGrid() + { + // Initialize localization and name resolver + LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService()); + } + + public void FilterExcludeCategories(IEnumerable excludedCategories) + { + // Get internal property `ViewModel` of internal type `PropertyGridViewModel` + var gridVm = this.GetProtectedProperty("ViewModel")!; + // Get public property `CategoryFilter` + var categoryFilter = gridVm.GetProtectedProperty("CategoryFilter")!; + + categoryFilter.BeginUpdate(); + + // Uncheck All, then check all except All + categoryFilter.UnCheck(categoryFilter.All); + + foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All)) + { + categoryFilter.Check(mask); + } + + // Uncheck excluded categories + foreach (var mask in excludedCategories) + { + categoryFilter.UnCheck(mask); + } + + categoryFilter.EndUpdate(); + } + + public void FilterIncludeCategories(IEnumerable includeCategories) + { + // Get internal property `ViewModel` of internal type `PropertyGridViewModel` + var gridVm = this.GetProtectedProperty("ViewModel")!; + // Get public property `CategoryFilter` + var categoryFilter = gridVm.GetProtectedProperty("CategoryFilter")!; + + categoryFilter.BeginUpdate(); + + // Uncheck All and check Misc by default + categoryFilter.UnCheck(categoryFilter.All); + categoryFilter.Check("Misc"); + + // Check included categories + foreach (var mask in includeCategories) + { + categoryFilter.Check(mask); + } + + categoryFilter.EndUpdate(); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs new file mode 100644 index 00000000..d074dd06 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; +using PropertyModels.Localilzation; +using StabilityMatrix.Avalonia.Languages; + +namespace StabilityMatrix.Avalonia.Controls; + +internal class PropertyGridCultureData : ICultureData +{ + /// + public bool Reload() => false; + + /// + public CultureInfo Culture => Cultures.Current ?? Cultures.Default; + + /// + public Uri Path => new(""); + + /// + public string this[string key] + { + get + { + if (Resources.ResourceManager.GetString(key) is { } result) + { + return result; + } + + return key; + } + } + + /// + public bool IsLoaded => true; +} diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs new file mode 100644 index 00000000..dfc27ade --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs @@ -0,0 +1,36 @@ +using System; +using PropertyModels.ComponentModel; +using PropertyModels.Localilzation; +using StabilityMatrix.Avalonia.Languages; + +namespace StabilityMatrix.Avalonia.Controls; + +/// +/// Implements using static . +/// +internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService +{ + /// + public ICultureData CultureData { get; } = new PropertyGridCultureData(); + + /// + public string this[string key] => CultureData[key]; + + /// + public event EventHandler? OnCultureChanged; + + /// + public ILocalizationService[] GetExtraServices() => Array.Empty(); + + /// + public void AddExtraService(ILocalizationService service) { } + + /// + public void RemoveExtraService(ILocalizationService service) { } + + /// + public ICultureData[] GetCultures() => new[] { CultureData }; + + /// + public void SelectCulture(string cultureName) { } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index ef8a6027..8edb6ca8 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -21,10 +21,10 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Dialogs; -using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Avalonia.ViewModels.OutputsPage; +using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; @@ -76,10 +76,7 @@ public static class DesignData Id = activePackageId, DisplayName = "My Installed Package", PackageName = "stable-diffusion-webui", - Version = new InstalledPackageVersion - { - InstalledReleaseVersion = "v1.0.0" - }, + Version = new InstalledPackageVersion { InstalledReleaseVersion = "v1.0.0" }, LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LastUpdateCheck = DateTimeOffset.Now }, @@ -91,8 +88,7 @@ public static class DesignData Version = new InstalledPackageVersion { InstalledBranch = "master", - InstalledCommitSha = - "abc12uwu345568972abaedf7g7e679a98879e879f87ga8" + InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8" }, LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LastUpdateCheck = DateTimeOffset.Now @@ -211,11 +207,7 @@ public static class DesignData TrainedWords = ["aurora", "lightning"] } }, - new() - { - FilePath = "~/Models/Lora/model.safetensors", - Title = "Some model" - }, + new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }, }, }, new(settingsManager, downloadService, modelFinder, notificationService) @@ -299,56 +291,55 @@ public static class DesignData ); }*/ - CheckpointBrowserViewModel.ModelCards = - new ObservableCollection + CheckpointBrowserViewModel.ModelCards = new ObservableCollection + { + dialogFactory.Get(vm => { - dialogFactory.Get(vm => + vm.CivitModel = new CivitModel { - vm.CivitModel = new CivitModel + Name = "BB95 Furry Mix", + Description = "A furry mix of BB95", + Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 }, + ModelVersions = [new() { Name = "v1.2.2-Inpainting" }], + Creator = new CivitCreator { - Name = "BB95 Furry Mix", - Description = "A furry mix of BB95", - Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 }, - ModelVersions = [ - new() { Name = "v1.2.2-Inpainting" } - ], - Creator = new CivitCreator - { - Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro", - Username = "creator-1" - } - }; - }), - dialogFactory.Get(vm => + Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro", + Username = "creator-1" + } + }; + }), + dialogFactory.Get(vm => + { + vm.CivitModel = new CivitModel { - vm.CivitModel = new CivitModel - { - Name = "Another Model", - Description = "A mix of example", - Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 }, - ModelVersions = [ - new() + Name = "Another Model", + Description = "A mix of example", + Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 }, + ModelVersions = + [ + new() + { + Name = "v1.2.2-Inpainting", + Images = new List { - Name = "v1.2.2-Inpainting", - Images = new List + new() { - new() - { - Nsfw = "None", - Url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" - + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" - } + Nsfw = "None", + Url = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" } - } - ], - Creator = new CivitCreator - { - Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", - Username = "creator-2" + } } - }; - }) - }; + ], + Creator = new CivitCreator + { + Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", + Username = "creator-2" + } + }; + }) + }; NewCheckpointsPageViewModel.AllCheckpoints = new ObservableCollection { @@ -376,33 +367,30 @@ public static class DesignData new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" } }; - ProgressManagerViewModel.ProgressItems.AddRange( - new ProgressItemViewModelBase[] - { - new ProgressItemViewModel( - new ProgressItem( - Guid.NewGuid(), - "Test File.exe", - new ProgressReport(0.5f, "Downloading...") + ProgressManagerViewModel + .ProgressItems + .AddRange( + new ProgressItemViewModelBase[] + { + new ProgressItemViewModel( + new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading...")) + ), + new MockDownloadProgressItemViewModel( + "Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe" + ), + new PackageInstallProgressItemViewModel( + new PackageModificationRunner + { + CurrentProgress = new ProgressReport(0.5f, "Installing package...") + } ) - ), - new MockDownloadProgressItemViewModel( - "Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe" - ), - new PackageInstallProgressItemViewModel( - new PackageModificationRunner - { - CurrentProgress = new ProgressReport(0.5f, "Installing package...") - } - ) - } - ); + } + ); UpdateViewModel = Services.GetRequiredService(); UpdateViewModel.CurrentVersionText = "v2.0.0"; UpdateViewModel.NewVersionText = "v2.0.1"; - UpdateViewModel.ReleaseNotes = - "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; + UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; isInitialized = true; } @@ -419,14 +407,12 @@ public static class DesignData public static ServiceManager DialogFactory => Services.GetRequiredService>(); - public static MainWindowViewModel MainWindowViewModel => - Services.GetRequiredService(); + public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService(); public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel => Services.GetRequiredService(); - public static LaunchPageViewModel LaunchPageViewModel => - Services.GetRequiredService(); + public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService(); public static OutputsPageViewModel OutputsPageViewModel { @@ -457,10 +443,7 @@ public static class DesignData vm.SetPackages(settings.Settings.InstalledPackages); vm.SetUnknownPackages( - new InstalledPackage[] - { - UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), - } + new InstalledPackage[] { UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), } ); vm.PackageCards[0].IsUpdateAvailable = true; @@ -475,14 +458,12 @@ public static class DesignData public static NewCheckpointsPageViewModel NewCheckpointsPageViewModel => Services.GetRequiredService(); - public static SettingsViewModel SettingsViewModel => - Services.GetRequiredService(); + public static SettingsViewModel SettingsViewModel => Services.GetRequiredService(); public static InferenceSettingsViewModel InferenceSettingsViewModel => Services.GetRequiredService(); - public static MainSettingsViewModel MainSettingsViewModel => - Services.GetRequiredService(); + public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService(); public static AccountSettingsViewModel AccountSettingsViewModel => Services.GetRequiredService(); @@ -502,7 +483,7 @@ public static class DesignData HashBlake3 = "", Signature = "", }; - + vm.UpdateStatus = new UpdateStatusChangedEventArgs { LatestUpdate = update, @@ -563,10 +544,7 @@ The gallery images are often inpainted, but you will get something very similar } } }; - var sampleViewModel = new ModelVersionViewModel( - new HashSet { "ABCD" }, - sampleCivitVersions[0] - ); + var sampleViewModel = new ModelVersionViewModel(new HashSet { "ABCD" }, sampleCivitVersions[0]); // Sample data for dialogs vm.Versions = new List { sampleViewModel }; @@ -578,8 +556,7 @@ The gallery images are often inpainted, but you will get something very similar public static OneClickInstallViewModel OneClickInstallViewModel => Services.GetRequiredService(); - public static InferenceViewModel InferenceViewModel => - Services.GetRequiredService(); + public static InferenceViewModel InferenceViewModel => Services.GetRequiredService(); public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel => Services.GetRequiredService(); @@ -617,14 +594,10 @@ The gallery images are often inpainted, but you will get something very similar public static PythonPackagesViewModel PythonPackagesViewModel => DialogFactory.Get(vm => { - vm.AddPackages( - new PipPackageInfo("pip", "1.0.0"), - new PipPackageInfo("torch", "2.1.0+cu121") - ); + vm.AddPackages(new PipPackageInfo("pip", "1.0.0"), new PipPackageInfo("torch", "2.1.0+cu121")); }); - public static LykosLoginViewModel LykosLoginViewModel => - DialogFactory.Get(); + public static LykosLoginViewModel LykosLoginViewModel => DialogFactory.Get(); public static OAuthConnectViewModel OAuthConnectViewModel => DialogFactory.Get(vm => @@ -647,15 +620,20 @@ The gallery images are often inpainted, but you will get something very similar public static InferenceImageToImageViewModel InferenceImageToImageViewModel => DialogFactory.Get(); - + public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel => DialogFactory.Get(); - public static PackageImportViewModel PackageImportViewModel => - DialogFactory.Get(); + public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get(); + + public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success }; - public static RefreshBadgeViewModel RefreshBadgeViewModel => - new() { State = ProgressState.Success }; + public static PropertyGridViewModel PropertyGridViewModel => + DialogFactory.Get(vm => + { + vm.SelectedObject = new MockPropertyGridObject(); + vm.ExcludeCategories = ["Excluded Category"]; + }); public static SeedCardViewModel SeedCardViewModel => new(); @@ -712,8 +690,7 @@ The gallery images are often inpainted, but you will get something very similar ); }); - public static ImageFolderCardViewModel ImageFolderCardViewModel => - DialogFactory.Get(); + public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get(); public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get(); @@ -755,11 +732,9 @@ The gallery images are often inpainted, but you will get something very similar vm.OnContainerIndexChanged(0); }); - public static UpscalerCardViewModel UpscalerCardViewModel => - DialogFactory.Get(); + public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get(); - public static BatchSizeCardViewModel BatchSizeCardViewModel => - DialogFactory.Get(); + public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get(); public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption => DialogFactory.Get(vm => @@ -811,14 +786,12 @@ The gallery images are often inpainted, but you will get something very similar vm.Resource = ComfyUpscaler.DefaultDownloadableModels[0].DownloadableResource!.Value; }); - public static SharpenCardViewModel SharpenCardViewModel => - DialogFactory.Get(); + public static SharpenCardViewModel SharpenCardViewModel => DialogFactory.Get(); public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel => DialogFactory.Get(); - public static SelectImageCardViewModel SelectImageCardViewModel => - DialogFactory.Get(); + public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get(); public static SelectImageCardViewModel SelectImageCardViewModel_WithImage => DialogFactory.Get(vm => @@ -831,18 +804,13 @@ The gallery images are often inpainted, but you will get something very similar }); public static ImageSource SampleImageSource => - new( - new Uri( - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500" - ) - ) + new(new Uri("https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500")) { Label = "Test Image" }; - public static ControlNetCardViewModel ControlNetCardViewModel => - DialogFactory.Get(); - + public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get(); + public static Indexer Types { get; } = new(); public class Indexer @@ -851,9 +819,7 @@ The gallery images are often inpainted, but you will get something very similar { get { - var type = - Type.GetType(typeName) - ?? throw new ArgumentException($"Type {typeName} not found"); + var type = Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found"); try { return Services.GetService(type); diff --git a/StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs b/StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs new file mode 100644 index 00000000..82969486 --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs @@ -0,0 +1,44 @@ +using System.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; +using PropertyModels.ComponentModel; +using StabilityMatrix.Avalonia.Languages; + +#pragma warning disable CS0657 // Not a valid attribute location for this declaration + +namespace StabilityMatrix.Avalonia.DesignData; + +public partial class MockPropertyGridObject : ObservableObject +{ + [ObservableProperty] + private string? stringProperty; + + [ObservableProperty] + private int intProperty; + + [ObservableProperty] + [property: Trackable(0, 50, Increment = 1, FormatString = "{0:0}")] + private int intRange = 10; + + [ObservableProperty] + [property: Trackable(0d, 1d, Increment = 0.01, FormatString = "{0:P0}")] + private double floatPercentRange = 0.25; + + [ObservableProperty] + [property: DisplayName("Int Custom Name")] + private int intCustomNameProperty = 42; + + [ObservableProperty] + [property: DisplayName(nameof(Resources.Label_Language))] + private int? intLocalizedNameProperty; + + [ObservableProperty] + private bool boolProperty; + + [ObservableProperty] + [property: Category("Included Category")] + private string? stringIncludedCategoryProperty; + + [ObservableProperty] + [property: Category("Excluded Category")] + private string? stringExcludedCategoryProperty; +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 37aafc2a..8d26ce3d 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -32,6 +32,8 @@ + + diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs new file mode 100644 index 00000000..0a1eb21b --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.ComponentModel; +using Avalonia.PropertyGrid.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; +using OneOf; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; + +[View(typeof(PropertyGridDialog))] +[ManagedService] +[Transient] +public partial class PropertyGridViewModel : TaskDialogViewModelBase +{ + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SelectedObjectSource))] + private OneOf>? selectedObject; + + public object? SelectedObjectSource => SelectedObject?.Value; + + [ObservableProperty] + private PropertyGridShowStyle showStyle = PropertyGridShowStyle.Alphabetic; + + [ObservableProperty] + private IReadOnlyList? excludeCategories; + + [ObservableProperty] + private IReadOnlyList? includeCategories; +} diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml new file mode 100644 index 00000000..5a09bb05 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml @@ -0,0 +1,22 @@ + + + + + diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs new file mode 100644 index 00000000..77b4317e --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs @@ -0,0 +1,54 @@ +using Avalonia.Controls.Primitives; +using Avalonia.PropertyGrid.Controls; +using Avalonia.PropertyGrid.ViewModels; +using PropertyModels.ComponentModel; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Views.Dialogs; + +[Transient] +public partial class PropertyGridDialog : UserControlBase +{ + public PropertyGridDialog() + { + InitializeComponent(); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + var vm = DataContext as PropertyGridViewModel; + + if (vm?.ExcludeCategories is { } excludeCategories) + { + MainPropertyGrid.FilterExcludeCategories(excludeCategories); + } + + if (vm?.IncludeCategories is { } includeCategories) + { + MainPropertyGrid.FilterIncludeCategories(includeCategories); + } + } + + internal class CustomFilter : IPropertyGridFilterContext + { + /// + public IFilterPattern? FilterPattern => null; + + /// + public ICheckedMaskModel? FastFilterPattern => null; + + /// + public PropertyVisibility PropagateVisibility( + IPropertyGridCellInfo info, + FilterCategory category = FilterCategory.Default + ) + { + return PropertyVisibility.AlwaysVisible; + } + } +} diff --git a/StabilityMatrix.Core/Extensions/ObjectExtensions.cs b/StabilityMatrix.Core/Extensions/ObjectExtensions.cs index 16743710..fa54caee 100644 --- a/StabilityMatrix.Core/Extensions/ObjectExtensions.cs +++ b/StabilityMatrix.Core/Extensions/ObjectExtensions.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using JetBrains.Annotations; using RockLib.Reflection.Optimized; @@ -10,18 +11,17 @@ public static class ObjectExtensions /// /// Cache of Types to named field getters /// - private static readonly Dictionary< - Type, - Dictionary> - > FieldGetterTypeCache = new(); + private static readonly Dictionary>> FieldGetterTypeCache = new(); /// /// Cache of Types to named field setters /// - private static readonly Dictionary< - Type, - Dictionary> - > FieldSetterTypeCache = new(); + private static readonly Dictionary>> FieldSetterTypeCache = new(); + + /// + /// Cache of Types to named property getters + /// + private static readonly Dictionary>> PropertyGetterTypeCache = new(); /// /// Get the value of a named private field from an object @@ -38,17 +38,13 @@ public static class ObjectExtensions if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) { // Get the field - var field = obj.GetType() - .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType() - .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { - throw new ArgumentException( - $"Field {fieldName} not found on type {obj.GetType().Name}" - ); + throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a getter for the field @@ -61,6 +57,54 @@ public static class ObjectExtensions return (T?)fieldGetter(obj); } + /// + /// Get the value of a protected property from an object + /// + /// + /// The property must be defined by the runtime type of or its first base type. + /// + public static object? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName) + { + // Check cache + var fieldGetterCache = PropertyGetterTypeCache.GetOrAdd(obj.GetType()); + + if (!fieldGetterCache.TryGetValue(propertyName, out var propertyGetter)) + { + // Get the field + var propertyInfo = obj.GetType() + .GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + // Try get from parent + propertyInfo ??= obj.GetType() + .BaseType + ?.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + if (propertyInfo is null) + { + throw new ArgumentException($"Property {propertyName} not found on type {obj.GetType().Name}"); + } + + // Create a getter for the field + propertyGetter = o => propertyInfo.GetValue(o)!; + + // Add to cache + fieldGetterCache.Add(propertyName, propertyGetter); + } + + return (object?)propertyGetter(obj); + } + + /// + /// Get the value of a protected property from an object + /// + /// + /// The property must be defined by the runtime type of or its first base type. + /// + public static T? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName) + where T : class + { + return (T?)GetProtectedProperty(obj, propertyName); + } + /// /// Get the value of a named private field from an object /// @@ -75,10 +119,7 @@ public static class ObjectExtensions if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) { // Get the field - var field = typeof(TObject).GetField( - fieldName, - BindingFlags.Instance | BindingFlags.NonPublic - ); + var field = typeof(TObject).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { @@ -108,17 +149,13 @@ public static class ObjectExtensions if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field - var field = obj.GetType() - .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType() - .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { - throw new ArgumentException( - $"Field {fieldName} not found on type {obj.GetType().Name}" - ); + throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a setter for the field @@ -142,17 +179,13 @@ public static class ObjectExtensions if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field - var field = obj.GetType() - .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType() - .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { - throw new ArgumentException( - $"Field {fieldName} not found on type {obj.GetType().Name}" - ); + throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a setter for the field