feat: Add ImageSelector styles and demo.
This commit is contained in:
1 parent
7da3acceb1
commit
cb7e6fbb98
6 files changed
+338
-2
No files matched your search
@@ -64,7 +64,8 @@
|
|||||||
[ "Badge", "BadgeDemo", "Brush.CheckDot", "", "DataDisplay" ],
|
[ "Badge", "BadgeDemo", "Brush.CheckDot", "", "DataDisplay" ],
|
||||||
[ "Shield", "ShieldDemo", "Brush.SplitKeyValuePair", "", "DataDisplay" ],
|
[ "Shield", "ShieldDemo", "Brush.SplitKeyValuePair", "", "DataDisplay" ],
|
||||||
[ "ImageBlock", "ImageBlockDemo", "Brush.ImageStack", "", "DataDisplay" ],
|
[ "ImageBlock", "ImageBlockDemo", "Brush.ImageStack", "", "DataDisplay" ],
|
||||||
[ "AnimationPath", "AnimationPathDemo", "Brush.Path", "", "DataDisplay" ]
|
[ "AnimationPath", "AnimationPathDemo", "Brush.Path", "", "DataDisplay" ],
|
||||||
|
[ "ImageSelector", "ImageSelectorDemo", "Brush.Image", "", "DataEntry" ]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
x:Class="HandyControlDemo.UserControl.ImageSelectorDemo"
|
||||||
|
Background="{DynamicResource RegionBrush}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<hc:UniformSpacingPanel Margin="32"
|
||||||
|
ChildWrapping="Wrap"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="32"
|
||||||
|
HorizontalAlignment="Center">
|
||||||
|
<hc:ImageSelector Width="100" Height="100" />
|
||||||
|
<hc:ImageSelector Width="100" Height="100"
|
||||||
|
hc:BorderElement.CornerRadius="50" />
|
||||||
|
<hc:ImageSelector Width="100" Height="100"
|
||||||
|
hc:BorderElement.CornerRadius="50"
|
||||||
|
StrokeDashArray="10,5" />
|
||||||
|
<hc:ImageSelector Width="100" Height="100"
|
||||||
|
hc:BorderElement.CornerRadius="50"
|
||||||
|
BorderBrush="{DynamicResource SuccessBrush}" />
|
||||||
|
<hc:ImageSelector Width="100" Height="100"
|
||||||
|
hc:BorderElement.CornerRadius="50"
|
||||||
|
StrokeDashArray="10,5,10"
|
||||||
|
BorderBrush="{DynamicResource DangerBrush}" />
|
||||||
|
<hc:ImageSelector Width="100" Height="100"
|
||||||
|
hc:BorderElement.CornerRadius="10"
|
||||||
|
StrokeThickness="2"
|
||||||
|
BorderThickness="2"
|
||||||
|
BorderBrush="{DynamicResource PrimaryBrush}" />
|
||||||
|
</hc:UniformSpacingPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace HandyControlDemo.UserControl;
|
||||||
|
|
||||||
|
public partial class ImageSelectorDemo : Avalonia.Controls.UserControl
|
||||||
|
{
|
||||||
|
public ImageSelectorDemo()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace HandyControl.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A control that allows selecting an image file via a file picker dialog.
|
||||||
|
/// Displays the selected image preview inside a configurable frame.
|
||||||
|
/// </summary>
|
||||||
|
public class ImageSelector : TemplatedControl
|
||||||
|
{
|
||||||
|
public static readonly RoutedEvent<RoutedEventArgs> ImageSelectedEvent =
|
||||||
|
RoutedEvent.Register<ImageSelector, RoutedEventArgs>(nameof(ImageSelected), RoutingStrategies.Bubble);
|
||||||
|
|
||||||
|
public static readonly RoutedEvent<RoutedEventArgs> ImageUnselectedEvent =
|
||||||
|
RoutedEvent.Register<ImageSelector, RoutedEventArgs>(nameof(ImageUnselected), RoutingStrategies.Bubble);
|
||||||
|
|
||||||
|
public event EventHandler<RoutedEventArgs>? ImageSelected
|
||||||
|
{
|
||||||
|
add => AddHandler(ImageSelectedEvent, value);
|
||||||
|
remove => RemoveHandler(ImageSelectedEvent, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<RoutedEventArgs>? ImageUnselected
|
||||||
|
{
|
||||||
|
add => AddHandler(ImageUnselectedEvent, value);
|
||||||
|
remove => RemoveHandler(ImageUnselectedEvent, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ImageSelector()
|
||||||
|
{
|
||||||
|
FocusableProperty.OverrideDefaultValue<ImageSelector>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerPressed(e);
|
||||||
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
{
|
||||||
|
SwitchImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SwitchImage()
|
||||||
|
{
|
||||||
|
if (!HasValue)
|
||||||
|
{
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel == null) return;
|
||||||
|
|
||||||
|
var patterns = ParseFilterPatterns(Filter);
|
||||||
|
|
||||||
|
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter =
|
||||||
|
[
|
||||||
|
new FilePickerFileType("Images") { Patterns = patterns },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.Count > 0)
|
||||||
|
{
|
||||||
|
var file = files[0];
|
||||||
|
var localPath = file.TryGetLocalPath();
|
||||||
|
HasValue = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var stream = await file.OpenReadAsync();
|
||||||
|
var bitmap = new Bitmap(stream);
|
||||||
|
PreviewBrush = new ImageBrush(bitmap) { Stretch = Stretch };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
PreviewBrush = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localPath != null)
|
||||||
|
Uri = new Uri(localPath);
|
||||||
|
ToolTip.SetTip(this, file.Name);
|
||||||
|
RaiseEvent(new RoutedEventArgs(ImageSelectedEvent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HasValue = false;
|
||||||
|
PreviewBrush = null;
|
||||||
|
Uri = null;
|
||||||
|
ToolTip.SetTip(this, null);
|
||||||
|
RaiseEvent(new RoutedEventArgs(ImageUnselectedEvent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Properties ──
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Stretch> StretchProperty =
|
||||||
|
AvaloniaProperty.Register<ImageSelector, Stretch>(nameof(Stretch));
|
||||||
|
|
||||||
|
public Stretch Stretch
|
||||||
|
{
|
||||||
|
get => GetValue(StretchProperty);
|
||||||
|
set => SetValue(StretchProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DirectProperty<ImageSelector, Uri?> UriProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<ImageSelector, Uri?>(nameof(Uri), o => o.Uri);
|
||||||
|
|
||||||
|
private Uri? _uri;
|
||||||
|
|
||||||
|
public Uri? Uri
|
||||||
|
{
|
||||||
|
get => _uri;
|
||||||
|
private set => SetAndRaise(UriProperty, ref _uri, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DirectProperty<ImageSelector, IBrush?> PreviewBrushProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<ImageSelector, IBrush?>(nameof(PreviewBrush), o => o.PreviewBrush);
|
||||||
|
|
||||||
|
private IBrush? _previewBrush;
|
||||||
|
|
||||||
|
public IBrush? PreviewBrush
|
||||||
|
{
|
||||||
|
get => _previewBrush;
|
||||||
|
private set => SetAndRaise(PreviewBrushProperty, ref _previewBrush, value);
|
||||||
|
}
|
||||||
|
public static readonly StyledProperty<double> StrokeThicknessProperty =
|
||||||
|
AvaloniaProperty.Register<ImageSelector, double>(nameof(StrokeThickness), 1.0);
|
||||||
|
|
||||||
|
public double StrokeThickness
|
||||||
|
{
|
||||||
|
get => GetValue(StrokeThicknessProperty);
|
||||||
|
set => SetValue(StrokeThicknessProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Avalonia.Collections.AvaloniaList<double>?> StrokeDashArrayProperty =
|
||||||
|
AvaloniaProperty.Register<ImageSelector, Avalonia.Collections.AvaloniaList<double>?>(nameof(StrokeDashArray));
|
||||||
|
|
||||||
|
public Avalonia.Collections.AvaloniaList<double>? StrokeDashArray
|
||||||
|
{
|
||||||
|
get => GetValue(StrokeDashArrayProperty);
|
||||||
|
set => SetValue(StrokeDashArrayProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<string> DefaultExtProperty =
|
||||||
|
AvaloniaProperty.Register<ImageSelector, string>(nameof(DefaultExt), ".jpg");
|
||||||
|
|
||||||
|
public string DefaultExt
|
||||||
|
{
|
||||||
|
get => GetValue(DefaultExtProperty);
|
||||||
|
set => SetValue(DefaultExtProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<string> FilterProperty =
|
||||||
|
AvaloniaProperty.Register<ImageSelector, string>(nameof(Filter), "(.jpg)|*.jpg|(.png)|*.png");
|
||||||
|
|
||||||
|
public string Filter
|
||||||
|
{
|
||||||
|
get => GetValue(FilterProperty);
|
||||||
|
set => SetValue(FilterProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a WPF-style filter string like "(.jpg)|*.jpg|(.png)|*.png"
|
||||||
|
/// into individual glob patterns for the cross-platform file picker.
|
||||||
|
/// </summary>
|
||||||
|
private static List<string> ParseFilterPatterns(string? filter)
|
||||||
|
{
|
||||||
|
var patterns = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
// Sensible cross-platform defaults with case variations
|
||||||
|
patterns.AddRange(["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"]);
|
||||||
|
patterns.AddRange(["*.JPG", "*.JPEG", "*.PNG", "*.BMP", "*.GIF"]);
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WPF format: "Description|*.ext|Description|*.ext"
|
||||||
|
var parts = filter.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
// Skip display-name segments (e.g. "(.jpg)"); only keep pattern segments
|
||||||
|
if (part.Contains('*') || part.Contains('?'))
|
||||||
|
{
|
||||||
|
// Split multi-pattern entries like "*.jpg;*.png"
|
||||||
|
foreach (var p in part.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
var trimmed = p.Trim();
|
||||||
|
if (!string.IsNullOrEmpty(trimmed) && !patterns.Contains(trimmed))
|
||||||
|
{
|
||||||
|
patterns.Add(trimmed);
|
||||||
|
// Add uppercase variant for case-sensitive platforms (macOS)
|
||||||
|
var upper = trimmed.ToUpperInvariant();
|
||||||
|
if (upper != trimmed && !patterns.Contains(upper))
|
||||||
|
patterns.Add(upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if parsing produced nothing
|
||||||
|
if (patterns.Count == 0)
|
||||||
|
{
|
||||||
|
patterns.AddRange(["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif",
|
||||||
|
"*.JPG", "*.JPEG", "*.PNG", "*.BMP", "*.GIF"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DirectProperty<ImageSelector, bool> HasValueProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<ImageSelector, bool>(nameof(HasValue), o => o.HasValue);
|
||||||
|
|
||||||
|
private bool _hasValue;
|
||||||
|
|
||||||
|
public bool HasValue
|
||||||
|
{
|
||||||
|
get => _hasValue;
|
||||||
|
private set => SetAndRaise(HasValueProperty, ref _hasValue, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:ClassModifier="internal"
|
||||||
|
xmlns:hc="clr-namespace:HandyControl.Controls">
|
||||||
|
|
||||||
|
<ControlTheme x:Key="{x:Type hc:ImageSelector}"
|
||||||
|
TargetType="hc:ImageSelector">
|
||||||
|
<Setter Property="Stretch" Value="UniformToFill" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource RegionBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}" />
|
||||||
|
<Setter Property="StrokeDashArray" Value="2,2" />
|
||||||
|
<Setter Property="hc:BorderElement.CornerRadius"
|
||||||
|
Value="{StaticResource DefaultCornerRadius}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="{Binding $self.(hc:BorderElement.CornerRadius)}">
|
||||||
|
<hc:DashedBorder BorderDashArray="{TemplateBinding StrokeDashArray}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
BorderDashThickness="{TemplateBinding StrokeThickness}"
|
||||||
|
Background="{TemplateBinding PreviewBrush}"
|
||||||
|
CornerRadius="{Binding $self.(hc:BorderElement.CornerRadius)}"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<!-- Add / Remove icon -->
|
||||||
|
<Border Width="30"
|
||||||
|
Height="30"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="15"
|
||||||
|
BorderThickness="0">
|
||||||
|
<Panel>
|
||||||
|
<Path IsVisible="{Binding HasValue, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static BoolConverters.Not}}"
|
||||||
|
Data="{StaticResource AddGeometry}"
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Fill="{DynamicResource PrimaryBrush}" />
|
||||||
|
<Path IsVisible="{Binding HasValue, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
Data="{StaticResource RemoveGeometry}"
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
Fill="{DynamicResource PrimaryBrush}" />
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</hc:DashedBorder>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
|
||||||
|
<Style Selector="^:pointerover">
|
||||||
|
<Setter Property="Background"
|
||||||
|
Value="{DynamicResource SecondaryRegionBrush}" />
|
||||||
|
<Setter Property="BorderBrush"
|
||||||
|
Value="{DynamicResource PrimaryBrush}" />
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -76,8 +76,9 @@
|
|||||||
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Gravatar.axaml" />
|
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Gravatar.axaml" />
|
||||||
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Tag.axaml" />
|
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Tag.axaml" />
|
||||||
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Badge.axaml" />
|
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Badge.axaml" />
|
||||||
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/GotoTop.axaml" />
|
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/GotoTop.axaml" />
|
||||||
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Shield.axaml" />
|
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Shield.axaml" />
|
||||||
|
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ImageSelector.axaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
|
|||||||
Reference in new issue
Block a user