@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<AvaloniaVersion>11.0.0</AvaloniaVersion>
|
||||
<AvaloniaVersion>11.0.9</AvaloniaVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -13,6 +13,7 @@ public static class MenuKeys
|
||||
public const string MenuKeyDrawer = "Drawer";
|
||||
public const string MenuKeyDualBadge = "DualBadge";
|
||||
public const string MenuKeyEnumSelector = "EnumSelector";
|
||||
public const string MenuKeyForm = "Form";
|
||||
public const string MenuKeyImageViewer = "ImageViewer";
|
||||
public const string MenuKeyIpBox = "IPv4Box";
|
||||
public const string MenuKeyIconButton = "IconButton";
|
||||
|
||||
71
demo/Ursa.Demo/Pages/FormDemo.axaml
Normal file
71
demo/Ursa.Demo/Pages/FormDemo.axaml
Normal file
@@ -0,0 +1,71 @@
|
||||
<UserControl
|
||||
x:Class="Ursa.Demo.Pages.FormDemo"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="vm:FormDemoViewModel"
|
||||
mc:Ignorable="d">
|
||||
<ScrollViewer>
|
||||
<StackPanel>
|
||||
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="Auto, Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Label Position" VerticalAlignment="Center"></TextBlock>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Label Alignment" VerticalAlignment="Center"></TextBlock>
|
||||
<u:EnumSelector Grid.Row="0" Grid.Column="1"
|
||||
Name="position"
|
||||
EnumType="common:Position"
|
||||
Value="{x:Static common:Position.Top}" />
|
||||
<u:EnumSelector Grid.Row="1" Grid.Column="1"
|
||||
Name="alignment"
|
||||
EnumType="HorizontalAlignment"
|
||||
Value="{x:Static HorizontalAlignment.Left}" />
|
||||
</Grid>
|
||||
<u:Form
|
||||
DataContext="{Binding Model}"
|
||||
LabelAlignment="{Binding #alignment.Value}"
|
||||
LabelPosition="{Binding #position.Value}"
|
||||
LabelWidth="*">
|
||||
<u:FormGroup Header="Information">
|
||||
<TextBox
|
||||
Width="300"
|
||||
u:FormItem.IsRequired="True"
|
||||
u:FormItem.Label="Name"
|
||||
Text="{Binding Name}" />
|
||||
<TextBox
|
||||
Width="300"
|
||||
u:FormItem.Label="Email"
|
||||
Text="{Binding Email}" />
|
||||
</u:FormGroup>
|
||||
<u:FormItem Label="Please select a Date">
|
||||
<CalendarDatePicker SelectedDate="{Binding Date}" />
|
||||
</u:FormItem>
|
||||
|
||||
<u:FormGroup Header="Education">
|
||||
<TextBox
|
||||
HorizontalAlignment="Stretch"
|
||||
u:FormItem.IsRequired="True"
|
||||
u:FormItem.Label="Name"
|
||||
Text="{Binding Name}" />
|
||||
<TextBox
|
||||
HorizontalAlignment="Stretch"
|
||||
u:FormItem.Label="Email"
|
||||
u:FormItem.IsRequired="True"
|
||||
Text="{Binding Email}" />
|
||||
</u:FormGroup>
|
||||
<u:FormItem Label="Click to Submit">
|
||||
<Button Content="Button With Label" />
|
||||
</u:FormItem>
|
||||
<CheckBox Content="I Agree User Agreement" u:FormItem.NoLabel="True"></CheckBox>
|
||||
<u:FormItem NoLabel="True">
|
||||
<Button HorizontalAlignment="Left" Content="No Label" />
|
||||
</u:FormItem>
|
||||
</u:Form>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
13
demo/Ursa.Demo/Pages/FormDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/FormDemo.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Ursa.Demo.Pages;
|
||||
|
||||
public partial class FormDemo : UserControl
|
||||
{
|
||||
public FormDemo()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
|
||||
<PackageReference Include="Semi.Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Semi.Avalonia" Version="11.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
48
demo/Ursa.Demo/ViewModels/FormDemoViewModel.cs
Normal file
48
demo/Ursa.Demo/ViewModels/FormDemoViewModel.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Ursa.Demo.ViewModels;
|
||||
|
||||
public partial class FormDemoViewModel: ObservableObject
|
||||
{
|
||||
[ObservableProperty] private DataModel _model;
|
||||
|
||||
public FormDemoViewModel()
|
||||
{
|
||||
Model = new DataModel();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DataModel : ObservableObject
|
||||
{
|
||||
private string _name;
|
||||
|
||||
[MinLength(10)]
|
||||
public string Name
|
||||
{
|
||||
get=>_name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
private string _email;
|
||||
|
||||
[EmailAddress]
|
||||
public string Email
|
||||
{
|
||||
get=>_email;
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
private DateTime _date;
|
||||
public DateTime Date
|
||||
{
|
||||
get => _date;
|
||||
set => SetProperty(ref _date, value);
|
||||
}
|
||||
|
||||
public DataModel()
|
||||
{
|
||||
Date = DateTime.Today;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public class MainViewViewModel : ViewModelBase
|
||||
MenuKeys.MenuKeyDrawer => new DrawerDemoViewModel(),
|
||||
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
|
||||
MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(),
|
||||
MenuKeys.MenuKeyForm => new FormDemoViewModel(),
|
||||
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),
|
||||
MenuKeys.MenuKeyIconButton => new IconButtonDemoViewModel(),
|
||||
MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(),
|
||||
|
||||
@@ -22,6 +22,7 @@ public class MenuViewModel: ViewModelBase
|
||||
new() { MenuHeader = "Drawer", Key = MenuKeys.MenuKeyDrawer },
|
||||
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
|
||||
new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector },
|
||||
new() { MenuHeader = "Form", Key = MenuKeys.MenuKeyForm },
|
||||
new() { MenuHeader = "Icon Button", Key = MenuKeys.MenuKeyIconButton },
|
||||
new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer },
|
||||
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
||||
|
||||
97
src/Ursa.Themes.Semi/Controls/Form.axaml
Normal file
97
src/Ursa.Themes.Semi/Controls/Form.axaml
Normal file
@@ -0,0 +1,97 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="https://irihi.tech/ursa">
|
||||
<!-- Add Resources Here -->
|
||||
<ControlTheme x:Key="{x:Type u:Form}" TargetType="u:Form">
|
||||
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:Form">
|
||||
<DataValidationErrors>
|
||||
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||
</DataValidationErrors>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:fixed-width">
|
||||
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type u:FormGroup}" TargetType="u:FormGroup">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:FormGroup">
|
||||
<StackPanel Margin="0 28 0 0">
|
||||
<ContentPresenter Content="{TemplateBinding Header}" FontWeight="Bold" FontSize="18" />
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{DynamicResource SemiColorBorder}" />
|
||||
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type u:FormItem}" TargetType="u:FormItem">
|
||||
<Setter Property="Margin" Value="0 8" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:FormItem">
|
||||
<StackPanel>
|
||||
<StackPanel
|
||||
Name="PART_LabelPanel"
|
||||
Margin="0,0,0,4"
|
||||
HorizontalAlignment="{TemplateBinding LabelAlignment}"
|
||||
Orientation="Horizontal">
|
||||
<ContentPresenter Content="{TemplateBinding Label}" FontWeight="Bold" />
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SemiRed6}"
|
||||
IsVisible="{TemplateBinding IsRequired}"
|
||||
Text="*" />
|
||||
</StackPanel>
|
||||
<ContentPresenter Content="{TemplateBinding Content}" />
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:not(:no-label):horizontal">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:FormItem">
|
||||
<Grid RowDefinitions="*, *">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Width="{TemplateBinding LabelWidth}">
|
||||
<StackPanel
|
||||
Name="PART_LabelPanel"
|
||||
Margin="8,8,8,0"
|
||||
HorizontalAlignment="{TemplateBinding LabelAlignment}"
|
||||
Orientation="Horizontal">
|
||||
<ContentPresenter Content="{TemplateBinding Label}" FontWeight="Bold" />
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SemiRed6}"
|
||||
IsVisible="{TemplateBinding IsRequired}"
|
||||
Text="*" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ContentPresenter
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="^:no-label">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:FormItem">
|
||||
<ContentPresenter Content="{TemplateBinding Content}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -12,6 +12,7 @@
|
||||
<ResourceInclude Source="Drawer.axaml" />
|
||||
<ResourceInclude Source="DualBadge.axaml" />
|
||||
<ResourceInclude Source="EnumSelector.axaml" />
|
||||
<ResourceInclude Source="Form.axaml" />
|
||||
<ResourceInclude Source="IconButton.axaml" />
|
||||
<ResourceInclude Source="ImageViewer.axaml" />
|
||||
<ResourceInclude Source="IPv4Box.axaml" />
|
||||
|
||||
76
src/Ursa/Controls/Form/Form.cs
Normal file
76
src/Ursa/Controls/Form/Form.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Layout;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_FixedWidth)]
|
||||
public class Form: ItemsControl
|
||||
{
|
||||
public const string PC_FixedWidth = ":fixed-width";
|
||||
|
||||
public static readonly StyledProperty<GridLength> LabelWidthProperty = AvaloniaProperty.Register<Form, GridLength>(
|
||||
nameof(LabelWidth));
|
||||
|
||||
/// <summary>
|
||||
/// Behavior:
|
||||
/// <para>Fixed Width: all labels are with fixed length. </para>
|
||||
/// <para>Star: all labels are aligned by max length. </para>
|
||||
/// <para>Auto: labels are not aligned. </para>
|
||||
/// </summary>
|
||||
public GridLength LabelWidth
|
||||
{
|
||||
get => GetValue(LabelWidthProperty);
|
||||
set => SetValue(LabelWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Position> LabelPositionProperty = AvaloniaProperty.Register<Form, Position>(
|
||||
nameof(LabelPosition), defaultValue: Position.Top);
|
||||
|
||||
public Position LabelPosition
|
||||
{
|
||||
get => GetValue(LabelPositionProperty);
|
||||
set => SetValue(LabelPositionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<HorizontalAlignment> LabelAlignmentProperty = AvaloniaProperty.Register<Form, HorizontalAlignment>(
|
||||
nameof(LabelAlignment), defaultValue: HorizontalAlignment.Left);
|
||||
|
||||
public HorizontalAlignment LabelAlignment
|
||||
{
|
||||
get => GetValue(LabelAlignmentProperty);
|
||||
set => SetValue(LabelAlignmentProperty, value);
|
||||
}
|
||||
|
||||
static Form()
|
||||
{
|
||||
LabelWidthProperty.Changed.AddClassHandler<Form, GridLength>((x, args) => x.LabelWidthChanged(args));
|
||||
}
|
||||
|
||||
private void LabelWidthChanged(AvaloniaPropertyChangedEventArgs<GridLength> args)
|
||||
{
|
||||
var newValue = args.NewValue.Value;
|
||||
bool isFixed = newValue.IsStar || newValue.IsAbsolute;
|
||||
PseudoClasses.Set(PC_FixedWidth, isFixed);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
recycleKey = null;
|
||||
return item is not FormItem && item is not FormGroup;
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
if (item is not Control control) return new FormItem();
|
||||
return new FormItem()
|
||||
{
|
||||
Content = control,
|
||||
[!FormItem.LabelProperty] = control[!FormItem.LabelProperty],
|
||||
[!FormItem.IsRequiredProperty] = control[!FormItem.IsRequiredProperty],
|
||||
[!FormItem.NoLabelProperty] = control[!FormItem.NoLabelProperty],
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/Ursa/Controls/Form/FormGroup.cs
Normal file
26
src/Ursa/Controls/Form/FormGroup.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class FormGroup: HeaderedItemsControl
|
||||
{
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
recycleKey = null;
|
||||
return item is not FormItem;
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
if (item is not Control control) return new FormItem();
|
||||
return new FormItem
|
||||
{
|
||||
Content = control,
|
||||
[!FormItem.LabelProperty] = control[!FormItem.LabelProperty],
|
||||
[!FormItem.IsRequiredProperty] = control[!FormItem.IsRequiredProperty],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
93
src/Ursa/Controls/Form/FormItem.cs
Normal file
93
src/Ursa/Controls/Form/FormItem.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.VisualTree;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Horizontal, PC_NoLabel)]
|
||||
public class FormItem: ContentControl
|
||||
{
|
||||
public const string PC_Horizontal = ":horizontal";
|
||||
public const string PC_NoLabel = ":no-label";
|
||||
|
||||
#region Attached Properties
|
||||
public static readonly AttachedProperty<object?> LabelProperty =
|
||||
AvaloniaProperty.RegisterAttached<FormItem, Control, object?>("Label");
|
||||
public static void SetLabel(Control obj, object? value) => obj.SetValue(LabelProperty, value);
|
||||
public static object? GetLabel(Control obj) => obj.GetValue(LabelProperty);
|
||||
|
||||
|
||||
public static readonly AttachedProperty<bool> IsRequiredProperty =
|
||||
AvaloniaProperty.RegisterAttached<FormItem, Control, bool>("IsRequired");
|
||||
public static void SetIsRequired(Control obj, bool value) => obj.SetValue(IsRequiredProperty, value);
|
||||
public static bool GetIsRequired(Control obj) => obj.GetValue(IsRequiredProperty);
|
||||
|
||||
public static readonly AttachedProperty<bool> NoLabelProperty =
|
||||
AvaloniaProperty.RegisterAttached<FormItem, Control, bool>("NoLabel");
|
||||
|
||||
public static void SetNoLabel(Control obj, bool value) => obj.SetValue(NoLabelProperty, value);
|
||||
public static bool GetNoLabel(Control obj) => obj.GetValue(NoLabelProperty);
|
||||
#endregion
|
||||
|
||||
private List<IDisposable> _formSubscriptions = new List<IDisposable>();
|
||||
|
||||
public static readonly StyledProperty<double> LabelWidthProperty = AvaloniaProperty.Register<FormItem, double>(
|
||||
nameof(LabelWidth));
|
||||
|
||||
public double LabelWidth
|
||||
{
|
||||
get => GetValue(LabelWidthProperty);
|
||||
set => SetValue(LabelWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<HorizontalAlignment> LabelAlignmentProperty = AvaloniaProperty.Register<FormItem, HorizontalAlignment>(
|
||||
nameof(LabelAlignment));
|
||||
|
||||
public HorizontalAlignment LabelAlignment
|
||||
{
|
||||
get => GetValue(LabelAlignmentProperty);
|
||||
set => SetValue(LabelAlignmentProperty, value);
|
||||
}
|
||||
|
||||
static FormItem()
|
||||
{
|
||||
PropertyToPseudoClassMixin.Attach<FormItem>(NoLabelProperty, PC_NoLabel);
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
var form = this.GetVisualAncestors().OfType<Form>().FirstOrDefault();
|
||||
if (form is not null)
|
||||
{
|
||||
_formSubscriptions.Clear();
|
||||
var labelSubscription = form
|
||||
.GetObservable(Form.LabelWidthProperty)
|
||||
.Subscribe(new AnonymousObserver<GridLength>(length => { LabelWidth = length.IsAbsolute ? length.Value : double.NaN; }));
|
||||
var positionSubscription = form
|
||||
.GetObservable(Form.LabelPositionProperty)
|
||||
.Subscribe(new AnonymousObserver<Position>(position => { PseudoClasses.Set(PC_Horizontal, position == Position.Left);}));
|
||||
var alignmentSubscription = form
|
||||
.GetObservable(Form.LabelAlignmentProperty)
|
||||
.Subscribe(new AnonymousObserver<HorizontalAlignment>(alignment => { LabelAlignment = alignment; }));
|
||||
_formSubscriptions.Add(labelSubscription);
|
||||
_formSubscriptions.Add(positionSubscription);
|
||||
_formSubscriptions.Add(alignmentSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
foreach (var subscription in _formSubscriptions)
|
||||
{
|
||||
subscription.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user