Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Demo/AutoSettingUI.Ursa.Demo/Models/ApplicationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public string Email
[Description("Your display name (3-20 characters)")]
[Placeholder("Enter username")]
[Validation(Required = true, MinLength = 3, MaxLength = 20, ErrorMessage = "Username must be 3-20 characters")]
[Layout(Width = 200, Height = 28, Margin = "0,2,0,2")]
[Layout(Width = 200, Height = 58, Margin = "0,2,0,2")]
public string Username { get; set; } = "";

// Password: Custom mask character
Expand Down Expand Up @@ -342,6 +342,7 @@ public partial class ExtendedControlsSettings : ObservableObject
[Title("Settings.SelectionTags", UseResourceKey = true)]
[TagInput]
public ObservableCollection<string> ProjectTags { get; set; } = ["Ursa", "Avalonia", "AutoSettingUI"];


[Title("Settings.ReleaseDate", UseResourceKey = true)]
[DatePicker]
Expand Down
78 changes: 77 additions & 1 deletion src/AutoSettingUI.Core/Models/PropertyDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,62 @@ public sealed class PropertyDescriptor
/// </summary>
public int DisplayOrder { get; }

#region Layout Properties

/// <summary>
/// Gets the width for the control. double.NaN means Auto.
/// </summary>
public double LayoutWidth { get; }

/// <summary>
/// Gets the height for the control. double.NaN means Auto.
/// </summary>
public double LayoutHeight { get; }

/// <summary>
/// Gets the minimum width for the control.
/// </summary>
public double LayoutMinWidth { get; }

/// <summary>
/// Gets the minimum height for the control.
/// </summary>
public double LayoutMinHeight { get; }

/// <summary>
/// Gets the maximum width for the control.
/// </summary>
public double LayoutMaxWidth { get; }

/// <summary>
/// Gets the maximum height for the control.
/// </summary>
public double LayoutMaxHeight { get; }

/// <summary>
/// Gets the horizontal alignment for the control.
/// Values: "Left", "Center", "Right", "Stretch"
/// </summary>
public string? LayoutHorizontalAlignment { get; }

/// <summary>
/// Gets the vertical alignment for the control.
/// Values: "Top", "Center", "Bottom", "Stretch"
/// </summary>
public string? LayoutVerticalAlignment { get; }

/// <summary>
/// Gets the margin for the control.
/// </summary>
public string? LayoutMargin { get; }

/// <summary>
/// Gets the padding for the control.
/// </summary>
public string? LayoutPadding { get; }

#endregion

/// <summary>
/// Initializes a new instance of the <see cref="PropertyDescriptor"/> class.
/// </summary>
Expand Down Expand Up @@ -262,7 +318,17 @@ public PropertyDescriptor(
int displayOrder = 0,
string? displayNameKey = null,
string? placeholderKey = null,
string? descriptionKey = null)
string? descriptionKey = null,
double layoutWidth = double.NaN,
double layoutHeight = double.NaN,
double layoutMinWidth = double.NaN,
double layoutMinHeight = double.NaN,
double layoutMaxWidth = double.NaN,
double layoutMaxHeight = double.NaN,
string? layoutHorizontalAlignment = null,
string? layoutVerticalAlignment = null,
string? layoutMargin = null,
string? layoutPadding = null)
{
PropertyName = propertyName;
DisplayName = displayName;
Expand Down Expand Up @@ -302,6 +368,16 @@ public PropertyDescriptor(
NumericMaximum = numericMaximum;
NumericIncrement = numericIncrement;
DisplayOrder = displayOrder;
LayoutWidth = layoutWidth;
LayoutHeight = layoutHeight;
LayoutMinWidth = layoutMinWidth;
LayoutMinHeight = layoutMinHeight;
LayoutMaxWidth = layoutMaxWidth;
LayoutMaxHeight = layoutMaxHeight;
LayoutHorizontalAlignment = layoutHorizontalAlignment;
LayoutVerticalAlignment = layoutVerticalAlignment;
LayoutMargin = layoutMargin;
LayoutPadding = layoutPadding;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public bool HasDescriptor(string typeName)
var descriptionAttr = prop.GetCustomAttribute<DescriptionAttribute>();
var passwordAttr = prop.GetCustomAttribute<PasswordAttribute>();
var displayOrderAttr = prop.GetCustomAttribute<DisplayOrderAttribute>();
var layoutAttr = prop.GetCustomAttribute<LayoutAttribute>();

// Pre-compute enum values to avoid runtime Enum.GetValues in non-AOT path
string[]? enumValues = null;
Expand Down Expand Up @@ -191,7 +192,17 @@ public bool HasDescriptor(string typeName)
displayOrderAttr?.Order ?? 0,
titleAttr?.UseResourceKey == true ? titleAttr.Name : null,
placeholderAttr?.UseResourceKey == true ? placeholderAttr.Text : null,
descriptionAttr?.UseResourceKey == true ? descriptionAttr.Text : (titleAttr?.UseDescriptionKey == true ? titleAttr.Description : null)
descriptionAttr?.UseResourceKey == true ? descriptionAttr.Text : (titleAttr?.UseDescriptionKey == true ? titleAttr.Description : null),
layoutAttr?.Width ?? double.NaN,
layoutAttr?.Height ?? double.NaN,
layoutAttr?.MinWidth ?? double.NaN,
layoutAttr?.MinHeight ?? double.NaN,
layoutAttr?.MaxWidth ?? double.NaN,
layoutAttr?.MaxHeight ?? double.NaN,
layoutAttr?.HorizontalAlignment,
layoutAttr?.VerticalAlignment,
layoutAttr?.Margin,
layoutAttr?.Padding
);

if (currentSubProps is not null)
Expand Down
47 changes: 46 additions & 1 deletion src/AutoSettingUI.Generator/Incremental/AutoSettingGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class AutoSettingGenerator : IIncrementalGenerator
private const string NumericUpDownAttributeName = "NumericUpDownAttribute"; // Special handling needed - type depends on property type
private const string ControlBindingDefaultsAttributeName = "ControlBindingDefaultsAttribute";
private const string DisplayOrderAttributeName = "DisplayOrderAttribute";
private const string LayoutAttributeName = "LayoutAttribute";

// Diagnostic descriptors
private static readonly DiagnosticDescriptor NonPublicClassWarning = new(
Expand Down Expand Up @@ -546,6 +547,32 @@ private static void AppendRegisterMethod(StringBuilder sb, INamedTypeSymbol cls,
displayOrder = orderVal;
}

// Layout
var layoutAttr = GetAttr(prop, LayoutAttributeName);
double layoutWidth = double.NaN;
double layoutHeight = double.NaN;
double layoutMinWidth = double.NaN;
double layoutMinHeight = double.NaN;
double layoutMaxWidth = double.NaN;
double layoutMaxHeight = double.NaN;
string? layoutHAlign = null;
string? layoutVAlign = null;
string? layoutMargin = null;
string? layoutPadding = null;
if (layoutAttr != null)
{
layoutWidth = TryGetNamedArgDouble(layoutAttr, "Width") ?? double.NaN;
layoutHeight = TryGetNamedArgDouble(layoutAttr, "Height") ?? double.NaN;
layoutMinWidth = TryGetNamedArgDouble(layoutAttr, "MinWidth") ?? double.NaN;
layoutMinHeight = TryGetNamedArgDouble(layoutAttr, "MinHeight") ?? double.NaN;
layoutMaxWidth = TryGetNamedArgDouble(layoutAttr, "MaxWidth") ?? double.NaN;
layoutMaxHeight = TryGetNamedArgDouble(layoutAttr, "MaxHeight") ?? double.NaN;
layoutHAlign = GetNamedArgString(layoutAttr, "HorizontalAlignment");
layoutVAlign = GetNamedArgString(layoutAttr, "VerticalAlignment");
layoutMargin = GetNamedArgString(layoutAttr, "Margin");
layoutPadding = GetNamedArgString(layoutAttr, "Padding");
}

// CollectionEditor
var collEditorAttr = GetAttr(prop, CollectionEditorAttributeName);
string? collEditorTypeName = null;
Expand Down Expand Up @@ -651,7 +678,17 @@ private static void AppendRegisterMethod(StringBuilder sb, INamedTypeSymbol cls,
sb.AppendLine($" {displayOrder},");
sb.AppendLine($" {displayNameKey},");
sb.AppendLine($" {placeholderKey},");
sb.AppendLine($" {descriptionKey});");
sb.AppendLine($" {descriptionKey},");
sb.AppendLine($" {FormatDouble(layoutWidth)},");
sb.AppendLine($" {FormatDouble(layoutHeight)},");
sb.AppendLine($" {FormatDouble(layoutMinWidth)},");
sb.AppendLine($" {FormatDouble(layoutMinHeight)},");
sb.AppendLine($" {FormatDouble(layoutMaxWidth)},");
sb.AppendLine($" {FormatDouble(layoutMaxHeight)},");
sb.AppendLine($" {layoutHAlign ?? "null"},");
sb.AppendLine($" {layoutVAlign ?? "null"},");
sb.AppendLine($" {layoutMargin ?? "null"},");
sb.AppendLine($" {layoutPadding ?? "null"});");

var varName = $"pd_{safeName}_{EscapeName(prop.Name)}";
sb.AppendLine($" if (currentSub != null) currentSub.Add({varName}); else directProps.Add({varName});");
Expand Down Expand Up @@ -976,6 +1013,14 @@ private static int GetDisplayOrder(PropertyInfo prop)
return null;
}

private static string FormatDouble(double value)
{
if (double.IsNaN(value)) return "double.NaN";
if (double.IsPositiveInfinity(value)) return "double.PositiveInfinity";
if (double.IsNegativeInfinity(value)) return "double.NegativeInfinity";
return value.ToString(System.Globalization.CultureInfo.InvariantCulture);
}

/// <summary>Gets a named argument value as a Type symbol.</summary>
private static INamedTypeSymbol? GetNamedArgType(AttributeData? attr, string key)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -993,34 +993,92 @@ private bool CanExecuteDelegate(Core.Models.PropertyDescriptor prop, object targ
/// </summary>
private void ApplyControlAttributes(global::Avalonia.Controls.Control control, Core.Models.PropertyDescriptor prop, object target)
{
var propertyInfo = GetPropertyInfo(target, prop.PropertyName);
if (propertyInfo != null)
{
// Apply layout attributes
control.ApplyLayout(propertyInfo);
// Apply layout from pre-generated PropertyDescriptor (AOT-safe)
ApplyLayoutFromDescriptor(control, prop);

// Apply placeholder to TextBox
if (control is TextBox textBox)
{
textBox.ApplyPlaceholder(propertyInfo);
}

// Apply description (tooltip)
control.ApplyDescription(propertyInfo);
}

// AOT-safe fallback using generated metadata
if (control is TextBox tb && !string.IsNullOrEmpty(prop.PlaceholderText))
// Apply placeholder to TextBox
if (control is TextBox textBox && !string.IsNullOrEmpty(prop.PlaceholderText))
{
tb.Watermark = prop.PlaceholderText;
textBox.Watermark = prop.PlaceholderText;
}

// Apply description (tooltip)
if (!string.IsNullOrEmpty(prop.DescriptionText))
{
ToolTip.SetTip(control, prop.DescriptionText);
}
}

/// <summary>
/// Applies layout properties from PropertyDescriptor (AOT-safe, no reflection).
/// </summary>
private static void ApplyLayoutFromDescriptor(global::Avalonia.Controls.Control control, Core.Models.PropertyDescriptor prop)
{
if (!double.IsNaN(prop.LayoutWidth)) control.Width = prop.LayoutWidth;
if (!double.IsNaN(prop.LayoutHeight)) control.Height = prop.LayoutHeight;
if (!double.IsNaN(prop.LayoutMinWidth)) control.MinWidth = prop.LayoutMinWidth;
if (!double.IsNaN(prop.LayoutMinHeight)) control.MinHeight = prop.LayoutMinHeight;
if (!double.IsNaN(prop.LayoutMaxWidth)) control.MaxWidth = prop.LayoutMaxWidth;
if (!double.IsNaN(prop.LayoutMaxHeight)) control.MaxHeight = prop.LayoutMaxHeight;

if (!string.IsNullOrEmpty(prop.LayoutHorizontalAlignment))
control.HorizontalAlignment = ParseHorizontalAlignment(prop.LayoutHorizontalAlignment);

if (!string.IsNullOrEmpty(prop.LayoutVerticalAlignment))
control.VerticalAlignment = ParseVerticalAlignment(prop.LayoutVerticalAlignment);

if (!string.IsNullOrEmpty(prop.LayoutMargin))
control.Margin = ParseThickness(prop.LayoutMargin);

if (!string.IsNullOrEmpty(prop.LayoutPadding))
{
var padding = ParseThickness(prop.LayoutPadding);
if (control is ContentControl contentControl)
contentControl.Padding = padding;
else if (control is Decorator decorator)
decorator.Padding = padding;
}
}

private static HorizontalAlignment ParseHorizontalAlignment(string? value)
=> value?.ToLowerInvariant() switch
{
"left" => HorizontalAlignment.Left,
"center" => HorizontalAlignment.Center,
"right" => HorizontalAlignment.Right,
"stretch" => HorizontalAlignment.Stretch,
_ => HorizontalAlignment.Stretch
};

private static VerticalAlignment ParseVerticalAlignment(string? value)
=> value?.ToLowerInvariant() switch
{
"top" => VerticalAlignment.Top,
"center" => VerticalAlignment.Center,
"bottom" => VerticalAlignment.Bottom,
"stretch" => VerticalAlignment.Stretch,
_ => VerticalAlignment.Stretch
};

private static Thickness ParseThickness(string? value)
{
if (string.IsNullOrEmpty(value)) return new Thickness(0);

var parts = value.Split(',').Select(p => p.Trim()).ToArray();

if (parts.Length == 1 && double.TryParse(parts[0], out var uniform))
return new Thickness(uniform);

if (parts.Length == 4 &&
double.TryParse(parts[0], out var left) &&
double.TryParse(parts[1], out var top) &&
double.TryParse(parts[2], out var right) &&
double.TryParse(parts[3], out var bottom))
return new Thickness(left, top, right, bottom);

return new Thickness(0);
}

/// <summary>
/// Creates a TextBox with password support and validation.
/// </summary>
Expand Down
Loading