Browse Source

Add PropertyGrid and Dialog

pull/333/head
Ionite 12 months ago
parent
commit
0233a4f46d
No known key found for this signature in database
  1. 70
      StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs
  2. 35
      StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs
  3. 36
      StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs
  4. 220
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  5. 44
      StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs
  6. 2
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  7. 31
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs
  8. 22
      StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml
  9. 54
      StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs
  10. 101
      StabilityMatrix.Core/Extensions/ObjectExtensions.cs

70
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;
/// <inheritdoc />
[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<string> excludedCategories)
{
// Get internal property `ViewModel` of internal type `PropertyGridViewModel`
var gridVm = this.GetProtectedProperty("ViewModel")!;
// Get public property `CategoryFilter`
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("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<string> includeCategories)
{
// Get internal property `ViewModel` of internal type `PropertyGridViewModel`
var gridVm = this.GetProtectedProperty("ViewModel")!;
// Get public property `CategoryFilter`
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("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();
}
}

35
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
{
/// <inheritdoc />
public bool Reload() => false;
/// <inheritdoc />
public CultureInfo Culture => Cultures.Current ?? Cultures.Default;
/// <inheritdoc />
public Uri Path => new("");
/// <inheritdoc />
public string this[string key]
{
get
{
if (Resources.ResourceManager.GetString(key) is { } result)
{
return result;
}
return key;
}
}
/// <inheritdoc />
public bool IsLoaded => true;
}

36
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;
/// <summary>
/// Implements <see cref="ILocalizationService"/> using static <see cref="Cultures"/>.
/// </summary>
internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService
{
/// <inheritdoc />
public ICultureData CultureData { get; } = new PropertyGridCultureData();
/// <inheritdoc />
public string this[string key] => CultureData[key];
/// <inheritdoc />
public event EventHandler? OnCultureChanged;
/// <inheritdoc />
public ILocalizationService[] GetExtraServices() => Array.Empty<ILocalizationService>();
/// <inheritdoc />
public void AddExtraService(ILocalizationService service) { }
/// <inheritdoc />
public void RemoveExtraService(ILocalizationService service) { }
/// <inheritdoc />
public ICultureData[] GetCultures() => new[] { CultureData };
/// <inheritdoc />
public void SelectCulture(string cultureName) { }
}

220
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<CheckpointBrowserCardViewModel>
CheckpointBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(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<CheckpointBrowserCardViewModel>(vm =>
Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro",
Username = "creator-1"
}
};
}),
dialogFactory.Get<CheckpointBrowserCardViewModel>(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<CivitImage>
{
Name = "v1.2.2-Inpainting",
Images = new List<CivitImage>
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<CheckpointFile>
{
@ -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>();
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<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>();
public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService<LaunchPageViewModel>();
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<NewCheckpointsPageViewModel>();
public static SettingsViewModel SettingsViewModel =>
Services.GetRequiredService<SettingsViewModel>();
public static SettingsViewModel SettingsViewModel => Services.GetRequiredService<SettingsViewModel>();
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService<InferenceSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel =>
Services.GetRequiredService<MainSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService<MainSettingsViewModel>();
public static AccountSettingsViewModel AccountSettingsViewModel =>
Services.GetRequiredService<AccountSettingsViewModel>();
@ -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<string> { "ABCD" },
sampleCivitVersions[0]
);
var sampleViewModel = new ModelVersionViewModel(new HashSet<string> { "ABCD" }, sampleCivitVersions[0]);
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> { sampleViewModel };
@ -578,8 +556,7 @@ The gallery images are often inpainted, but you will get something very similar
public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>();
public static InferenceViewModel InferenceViewModel =>
Services.GetRequiredService<InferenceViewModel>();
public static InferenceViewModel InferenceViewModel => Services.GetRequiredService<InferenceViewModel>();
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
Services.GetRequiredService<SelectDataDirectoryViewModel>();
@ -617,14 +594,10 @@ The gallery images are often inpainted, but you will get something very similar
public static PythonPackagesViewModel PythonPackagesViewModel =>
DialogFactory.Get<PythonPackagesViewModel>(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<LykosLoginViewModel>();
public static LykosLoginViewModel LykosLoginViewModel => DialogFactory.Get<LykosLoginViewModel>();
public static OAuthConnectViewModel OAuthConnectViewModel =>
DialogFactory.Get<OAuthConnectViewModel>(vm =>
@ -647,15 +620,20 @@ The gallery images are often inpainted, but you will get something very similar
public static InferenceImageToImageViewModel InferenceImageToImageViewModel =>
DialogFactory.Get<InferenceImageToImageViewModel>();
public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel =>
DialogFactory.Get<InferenceImageUpscaleViewModel>();
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success };
public static RefreshBadgeViewModel RefreshBadgeViewModel =>
new() { State = ProgressState.Success };
public static PropertyGridViewModel PropertyGridViewModel =>
DialogFactory.Get<PropertyGridViewModel>(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<ImageFolderCardViewModel>();
public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get<ImageFolderCardViewModel>();
public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get<FreeUCardViewModel>();
@ -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<UpscalerCardViewModel>();
public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get<UpscalerCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel =>
DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption =>
DialogFactory.Get<BatchSizeCardViewModel>(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<SharpenCardViewModel>();
public static SharpenCardViewModel SharpenCardViewModel => DialogFactory.Get<SharpenCardViewModel>();
public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel =>
DialogFactory.Get<InferenceConnectionHelpViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel =>
DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel_WithImage =>
DialogFactory.Get<SelectImageCardViewModel>(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<ControlNetCardViewModel>();
public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get<ControlNetCardViewModel>();
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);

44
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;
}

2
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -32,6 +32,8 @@
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.5" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.5" />
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.0.5.1" />
<PackageReference Include="bodong.PropertyModels" Version="11.0.5.1" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />

31
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<INotifyPropertyChanged, IEnumerable<INotifyPropertyChanged>>? selectedObject;
public object? SelectedObjectSource => SelectedObject?.Value;
[ObservableProperty]
private PropertyGridShowStyle showStyle = PropertyGridShowStyle.Alphabetic;
[ObservableProperty]
private IReadOnlyList<string>? excludeCategories;
[ObservableProperty]
private IReadOnlyList<string>? includeCategories;
}

22
StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml

@ -0,0 +1,22 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:vmDialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:pg="clr-namespace:Avalonia.PropertyGrid.Controls;assembly=Avalonia.PropertyGrid"
d:DataContext="{x:Static mocks:DesignData.PropertyGridViewModel}"
x:DataType="vmDialogs:PropertyGridViewModel"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PropertyGridDialog">
<Grid>
<controls:BetterPropertyGrid
x:Name="MainPropertyGrid"
SelectedObject="{Binding SelectedObjectSource, Mode=OneTime}"
ShowStyle="{Binding ShowStyle}"
AllowQuickFilter="False"
AllowFilter="False"
ShowTitle="False" />
</Grid>
</controls:UserControlBase>

54
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();
}
/// <inheritdoc />
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
{
/// <inheritdoc />
public IFilterPattern? FilterPattern => null;
/// <inheritdoc />
public ICheckedMaskModel? FastFilterPattern => null;
/// <inheritdoc />
public PropertyVisibility PropagateVisibility(
IPropertyGridCellInfo info,
FilterCategory category = FilterCategory.Default
)
{
return PropertyVisibility.AlwaysVisible;
}
}
}

101
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
/// <summary>
/// Cache of Types to named field getters
/// </summary>
private static readonly Dictionary<
Type,
Dictionary<string, Func<object, object>>
> FieldGetterTypeCache = new();
private static readonly Dictionary<Type, Dictionary<string, Func<object, object>>> FieldGetterTypeCache = new();
/// <summary>
/// Cache of Types to named field setters
/// </summary>
private static readonly Dictionary<
Type,
Dictionary<string, Action<object, object>>
> FieldSetterTypeCache = new();
private static readonly Dictionary<Type, Dictionary<string, Action<object, object>>> FieldSetterTypeCache = new();
/// <summary>
/// Cache of Types to named property getters
/// </summary>
private static readonly Dictionary<Type, Dictionary<string, Func<object, object>>> PropertyGetterTypeCache = new();
/// <summary>
/// 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);
}
/// <summary>
/// Get the value of a protected property from an object
/// </summary>
/// <remarks>
/// The property must be defined by the runtime type of <see cref="obj"/> or its first base type.
/// </remarks>
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);
}
/// <summary>
/// Get the value of a protected property from an object
/// </summary>
/// <remarks>
/// The property must be defined by the runtime type of <see cref="obj"/> or its first base type.
/// </remarks>
public static T? GetProtectedProperty<T>(this object obj, [LocalizationRequired(false)] string propertyName)
where T : class
{
return (T?)GetProtectedProperty(obj, propertyName);
}
/// <summary>
/// Get the value of a named private field from an object
/// </summary>
@ -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

Loading…
Cancel
Save