From 810c7053c0583536a84e82b442a4a347920d2aa5 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 19 Mar 2026 09:13:09 +0100 Subject: [PATCH] better web settings --- .../Dialogs/ItemEditorDialog.razor | 118 ++++++++++++++ .../Dialogs/ListEditorDialog.razor | 152 ++++++++++++++++++ .../Dialogs/SettingsDialog.razor | 80 ++++++++- .../wwwroot/css/visionbuilder.css | 23 ++- 4 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 VisionBuilder.UI.Blazor/Dialogs/ItemEditorDialog.razor create mode 100644 VisionBuilder.UI.Blazor/Dialogs/ListEditorDialog.razor diff --git a/VisionBuilder.UI.Blazor/Dialogs/ItemEditorDialog.razor b/VisionBuilder.UI.Blazor/Dialogs/ItemEditorDialog.razor new file mode 100644 index 0000000..0716256 --- /dev/null +++ b/VisionBuilder.UI.Blazor/Dialogs/ItemEditorDialog.razor @@ -0,0 +1,118 @@ +@using MudBlazor +@using System.Reflection + + + + @if (_isSimpleType) + { + @RenderEditor(ItemType, _workingValue, v => _workingValue = v, "Value") + } + else + { + @foreach (var prop in _properties) + { + var p = prop; + var currentVal = p.GetValue(_workingValue); + @RenderEditor(p.PropertyType, currentVal, v => p.SetValue(_workingValue, v), p.Name) + } + } + + + Cancel + OK + + + +@code { + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + [Parameter] public Type ItemType { get; set; } = null!; + [Parameter] public object? InitialValue { get; set; } + + private object? _workingValue; + private bool _isSimpleType; + private PropertyInfo[] _properties = Array.Empty(); + + protected override void OnParametersSet() + { + _isSimpleType = ItemType.IsValueType || ItemType == typeof(string) || ItemType.IsEnum; + _workingValue = InitialValue; + + if (!_isSimpleType && _workingValue != null) + { + _properties = ItemType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite) + .ToArray(); + } + } + + private RenderFragment RenderEditor(Type type, object? value, Action onChanged, string label) => builder => + { + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "settings-field"); + + if (type == typeof(bool)) + { + var boolVal = (bool)(value ?? false); + builder.OpenComponent>(2); + builder.AddAttribute(3, "Value", boolVal); + builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create(this, v => { onChanged(v); StateHasChanged(); })); + builder.AddAttribute(5, "Label", label); + builder.AddAttribute(6, "Color", Color.Primary); + builder.CloseComponent(); + } + else if (type == typeof(int)) + { + var intVal = value is int iv ? iv : 0; + builder.OpenComponent>(2); + builder.AddAttribute(3, "Value", intVal); + builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create(this, v => { onChanged(v); StateHasChanged(); })); + builder.AddAttribute(5, "Label", label); + builder.AddAttribute(6, "Variant", Variant.Outlined); + builder.CloseComponent(); + } + else if (type == typeof(double)) + { + var dblVal = value is double dv ? dv : 0.0; + builder.OpenComponent>(2); + builder.AddAttribute(3, "Value", dblVal); + builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create(this, v => { onChanged(v); StateHasChanged(); })); + builder.AddAttribute(5, "Label", label); + builder.AddAttribute(6, "Variant", Variant.Outlined); + builder.CloseComponent(); + } + else if (type.IsEnum) + { + builder.OpenComponent>(2); + builder.AddAttribute(3, "Value", value); + builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create(this, v => { onChanged(v); StateHasChanged(); })); + builder.AddAttribute(5, "Label", label); + builder.AddAttribute(6, "Variant", Variant.Outlined); + builder.AddAttribute(7, "ChildContent", (RenderFragment)(innerBuilder => + { + foreach (var enumVal in Enum.GetValues(type)) + { + innerBuilder.OpenComponent>(0); + innerBuilder.AddAttribute(1, "Value", enumVal); + innerBuilder.AddAttribute(2, "ChildContent", (RenderFragment)(b => b.AddContent(0, enumVal.ToString()))); + innerBuilder.CloseComponent(); + } + })); + builder.CloseComponent(); + } + else + { + var strVal = value?.ToString() ?? ""; + builder.OpenComponent>(2); + builder.AddAttribute(3, "Value", strVal); + builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create(this, v => { onChanged(v); StateHasChanged(); })); + builder.AddAttribute(5, "Label", label); + builder.AddAttribute(6, "Variant", Variant.Outlined); + builder.CloseComponent(); + } + + builder.CloseElement(); + }; + + private void Ok() => MudDialog.Close(DialogResult.Ok(_workingValue)); + private void Cancel() => MudDialog.Cancel(); +} diff --git a/VisionBuilder.UI.Blazor/Dialogs/ListEditorDialog.razor b/VisionBuilder.UI.Blazor/Dialogs/ListEditorDialog.razor new file mode 100644 index 0000000..157c595 --- /dev/null +++ b/VisionBuilder.UI.Blazor/Dialogs/ListEditorDialog.razor @@ -0,0 +1,152 @@ +@using MudBlazor +@using System.Collections +@using System.Reflection + + + +
+
+ + @for (int i = 0; i < _items.Count; i++) + { + var idx = i; + + } + +
+
+ Add + Edit + Remove +
+
+
+ + Cancel + OK + +
+ +@code { + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + [Parameter] public Type ElementType { get; set; } = null!; + [Parameter] public IList? InitialList { get; set; } + + [Microsoft.AspNetCore.Components.Inject] private IDialogService DialogService { get; set; } = null!; + + private List _items = new(); + private int _selectedIndex = -1; + + protected override void OnParametersSet() + { + _items.Clear(); + if (InitialList != null) + { + foreach (var item in InitialList) + _items.Add(CloneObject(item)); + } + } + + private async Task Add() + { + var defaultValue = GetDefaultValue(ElementType); + var result = await EditItem(defaultValue); + if (result != null) + { + _items.Add(result); + _selectedIndex = _items.Count - 1; + } + } + + private async Task Edit() + { + if (_selectedIndex < 0 || _selectedIndex >= _items.Count) return; + var result = await EditItem(_items[_selectedIndex]); + if (result != null) + { + _items[_selectedIndex] = result; + } + } + + private void Remove() + { + if (_selectedIndex < 0 || _selectedIndex >= _items.Count) return; + _items.RemoveAt(_selectedIndex); + if (_items.Count > 0) + _selectedIndex = Math.Min(_selectedIndex, _items.Count - 1); + else + _selectedIndex = -1; + } + + private async Task EditItem(object? value) + { + if (ElementType.IsValueType || ElementType == typeof(string) || ElementType.IsEnum) + { + // Simple type: inline edit via ItemEditorDialog + var parameters = new DialogParameters + { + { x => x.ItemType, ElementType }, + { x => x.InitialValue, value } + }; + var dialog = await DialogService.ShowAsync($"Edit {ElementType.Name}", parameters); + var result = await dialog.Result; + if (result != null && !result.Canceled) + return result.Data; + return null; + } + else + { + // Complex type: edit properties + var parameters = new DialogParameters + { + { x => x.ItemType, ElementType }, + { x => x.InitialValue, CloneObject(value) } + }; + var dialog = await DialogService.ShowAsync($"Edit {ElementType.Name}", parameters); + var result = await dialog.Result; + if (result != null && !result.Canceled) + return result.Data; + return null; + } + } + + private void Ok() + { + var listType = typeof(List<>).MakeGenericType(ElementType); + var resultList = (IList)Activator.CreateInstance(listType)!; + foreach (var item in _items) + resultList.Add(item); + MudDialog.Close(DialogResult.Ok(resultList)); + } + + private void Cancel() => MudDialog.Cancel(); + + private object? GetDefaultValue(Type type) + { + if (type.IsValueType) return Activator.CreateInstance(type); + if (type == typeof(string)) return ""; + try { return Activator.CreateInstance(type); } + catch { return null; } + } + + private object? CloneObject(object? obj) + { + if (obj == null) return null; + var type = obj.GetType(); + if (type.IsValueType || type == typeof(string)) return obj; + try + { + var clone = Activator.CreateInstance(type); + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.CanRead && prop.CanWrite) + prop.SetValue(clone, prop.GetValue(obj)); + } + return clone; + } + catch { return obj; } + } +} diff --git a/VisionBuilder.UI.Blazor/Dialogs/SettingsDialog.razor b/VisionBuilder.UI.Blazor/Dialogs/SettingsDialog.razor index 0e8880e..d74dff5 100644 --- a/VisionBuilder.UI.Blazor/Dialogs/SettingsDialog.razor +++ b/VisionBuilder.UI.Blazor/Dialogs/SettingsDialog.razor @@ -1,7 +1,9 @@ @using MudBlazor +@using System.Collections @using System.ComponentModel @using System.Reflection @using Inspectron.Settings +@using Inspectron.Settings.Attributes @@ -17,6 +19,11 @@ @foreach (var setting in settings) {
+ @if (!string.IsNullOrEmpty(setting.SettingDescription)) + { + @setting.SettingDescription + } + @if (setting.Property.PropertyType == typeof(bool)) { var boolVal = (bool)(setting.Value ?? false); @@ -49,6 +56,15 @@ } } + else if (IsListType(setting.Property.PropertyType)) + { + var s = setting; + @setting.Description + + Edit + } else { var strVal = setting.Value?.ToString() ?? ""; @@ -76,6 +92,8 @@ [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; [Parameter] public InspectronSettings? Settings { get; set; } + [Microsoft.AspNetCore.Components.Inject] private IDialogService DialogService { get; set; } = null!; + private Dictionary> _categories = new(); private string? _selectedCategory; private List _treeRoot = new(); @@ -95,7 +113,6 @@ if (root != null) Traverse(root, ""); - // Build the UI tree from category paths foreach (var categoryPath in _categories.Keys) { var parts = categoryPath.Split('/'); @@ -137,11 +154,18 @@ { var owner = bpd.Owner; var description = string.IsNullOrEmpty(pd.Description) ? bpd.DisplayName : pd.Description; + + string? settingDesc = null; + var descAttr = bpd.PropertyInfo.GetCustomAttribute(); + if (descAttr != null) + settingDesc = descAttr.Description; + var entry = new SettingEntry { Owner = owner, Property = bpd.PropertyInfo, Description = description, + SettingDescription = settingDesc, Value = bpd.PropertyInfo.GetValue(owner) }; @@ -163,11 +187,9 @@ private List GetSettingsForCategory(string category) { - // Exact match first if (_categories.TryGetValue(category, out var exact)) return exact; - // Otherwise aggregate all sub-categories var prefix = category + "/"; return _categories .Where(kvp => kvp.Key.StartsWith(prefix)) @@ -175,6 +197,39 @@ .ToList(); } + private static bool IsListType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + } + + private static string GetListDisplayText(IList? list) + { + if (list == null || list.Count == 0) + return "(empty list)"; + var items = new List(); + foreach (var item in list) + items.Add(item?.ToString() ?? "(null)"); + return string.Join(Environment.NewLine, items); + } + + private async Task EditList(SettingEntry setting) + { + var elementType = setting.Property.PropertyType.GetGenericArguments()[0]; + var parameters = new DialogParameters + { + { x => x.ElementType, elementType }, + { x => x.InitialList, setting.Value as IList } + }; + var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true }; + var dialog = await DialogService.ShowAsync($"Edit {setting.Description}", parameters, options); + var result = await dialog.Result; + if (result != null && !result.Canceled && result.Data is IList newList) + { + setting.Value = newList; + StateHasChanged(); + } + } + private void Save() { foreach (var list in _categories.Values) @@ -186,7 +241,22 @@ { try { - if (setting.Value != null && setting.Value.GetType() != setting.Property.PropertyType) + if (IsListType(setting.Property.PropertyType) && setting.Value is IList listVal) + { + // For list types, clear and repopulate the original list + var originalList = setting.Property.GetValue(setting.Owner) as IList; + if (originalList != null) + { + originalList.Clear(); + foreach (var item in listVal) + originalList.Add(item); + } + else + { + setting.Property.SetValue(setting.Owner, setting.Value); + } + } + else if (setting.Value != null && setting.Value.GetType() != setting.Property.PropertyType) { var converted = Convert.ChangeType(setting.Value, setting.Property.PropertyType); setting.Property.SetValue(setting.Owner, converted); @@ -198,7 +268,6 @@ } catch { - // Skip properties that fail to convert } } } @@ -214,6 +283,7 @@ public object Owner { get; set; } = null!; public PropertyInfo Property { get; set; } = null!; public string Description { get; set; } = ""; + public string? SettingDescription { get; set; } public object? Value { get; set; } } diff --git a/VisionBuilder.UI.Blazor/wwwroot/css/visionbuilder.css b/VisionBuilder.UI.Blazor/wwwroot/css/visionbuilder.css index dc8b516..7bfae6c 100644 --- a/VisionBuilder.UI.Blazor/wwwroot/css/visionbuilder.css +++ b/VisionBuilder.UI.Blazor/wwwroot/css/visionbuilder.css @@ -289,5 +289,26 @@ } .settings-field { - margin-bottom: 12px; + margin-bottom: 16px; +} + +/* List Editor Dialog */ +.list-editor-layout { + display: flex; + gap: 12px; + min-height: 300px; +} + +.list-editor-list { + flex: 1; + border: 1px solid #e0e0e0; + overflow-y: auto; + max-height: 350px; +} + +.list-editor-buttons { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 80px; }