Merge branch 'refs/heads/main' into ElasticWrapPanel
# Conflicts: # demo/Ursa.Demo/Models/MenuKeys.cs # demo/Ursa.Demo/ViewModels/MainViewViewModel.cs # demo/Ursa.Demo/ViewModels/MenuViewModel.cs # src/Ursa.Themes.Semi/Controls/_index.axaml
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
@@ -10,18 +9,14 @@ using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_ContentPresenter, typeof(ContentPresenter))]
|
||||
[TemplatePart(PART_BadgeContainer, typeof(Border))]
|
||||
[TemplatePart(PART_BadgeContentPresenter, typeof(ContentPresenter))]
|
||||
public class Badge: ContentControl
|
||||
public class Badge: HeaderedContentControl
|
||||
{
|
||||
public const string PART_ContentPresenter = "PART_ContentPresenter";
|
||||
public const string PART_BadgeContainer = "PART_BadgeContainer";
|
||||
public const string PART_BadgeContentPresenter = "PART_BadgeContentPresenter";
|
||||
|
||||
private ContentPresenter? _content;
|
||||
public const string PART_HeaderPresenter = "PART_HeaderPresenter";
|
||||
|
||||
private Border? _badgeContainer;
|
||||
private ContentPresenter? _badgeContent;
|
||||
|
||||
public static readonly StyledProperty<ControlTheme> BadgeThemeProperty = AvaloniaProperty.Register<Badge, ControlTheme>(
|
||||
nameof(BadgeTheme));
|
||||
@@ -39,14 +34,6 @@ public class Badge: ContentControl
|
||||
set => SetValue(DotProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> BadgeContentProperty = AvaloniaProperty.Register<Badge, object?>(
|
||||
nameof(BadgeContent));
|
||||
public object? BadgeContent
|
||||
{
|
||||
get => GetValue(BadgeContentProperty);
|
||||
set => SetValue(BadgeContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<CornerPosition> CornerPositionProperty = AvaloniaProperty.Register<Badge, CornerPosition>(
|
||||
nameof(CornerPosition));
|
||||
public CornerPosition CornerPosition
|
||||
@@ -63,17 +50,24 @@ public class Badge: ContentControl
|
||||
set => SetValue(OverflowCountProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> BadgeFontSizeProperty = AvaloniaProperty.Register<Badge, double>(
|
||||
nameof(BadgeFontSize));
|
||||
|
||||
public double BadgeFontSize
|
||||
{
|
||||
get => GetValue(BadgeFontSizeProperty);
|
||||
set => SetValue(BadgeFontSizeProperty, value);
|
||||
}
|
||||
|
||||
static Badge()
|
||||
{
|
||||
BadgeContentProperty.Changed.AddClassHandler<Badge>((badge, args) => badge.UpdateBadgePosition());
|
||||
HeaderProperty.Changed.AddClassHandler<Badge>((badge, args) => badge.UpdateBadgePosition());
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_content = e.NameScope.Find<ContentPresenter>(PART_ContentPresenter);
|
||||
_badgeContainer = e.NameScope.Find<Border>(PART_BadgeContainer);
|
||||
_badgeContent = e.NameScope.Find<ContentPresenter>(PART_BadgeContentPresenter);
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
@@ -92,7 +86,7 @@ public class Badge: ContentControl
|
||||
{
|
||||
var vertical = CornerPosition is CornerPosition.BottomLeft or CornerPosition.BottomRight ? 1 : -1;
|
||||
var horizontal = CornerPosition is CornerPosition.TopRight or CornerPosition.BottomRight ? 1 : -1;
|
||||
if (_badgeContainer is not null && _content?.Child is not null)
|
||||
if (_badgeContainer is not null && base.Presenter?.Child is not null)
|
||||
{
|
||||
_badgeContainer.RenderTransform = new TransformGroup()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Metadata;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
@@ -57,16 +58,9 @@ public class Banner: HeaderedContentControl
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.Click -= OnCloseClick;
|
||||
}
|
||||
Button.ClickEvent.RemoveHandler(OnCloseClick, _closeButton);
|
||||
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.Click += OnCloseClick;
|
||||
}
|
||||
|
||||
Button.ClickEvent.AddHandler(OnCloseClick, _closeButton);
|
||||
}
|
||||
|
||||
private void OnCloseClick(object sender, RoutedEventArgs args)
|
||||
128
src/Ursa/Controls/Breadcrumb/Breadcrumb.cs
Normal file
128
src/Ursa/Controls/Breadcrumb/Breadcrumb.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class Breadcrumb: ItemsControl
|
||||
{
|
||||
private static readonly ITemplate<Panel?> _defaultPanel =
|
||||
new FuncTemplate<Panel?>(() => new StackPanel() { Orientation = Orientation.Horizontal });
|
||||
|
||||
|
||||
public static readonly StyledProperty<IBinding?> IconBindingProperty = AvaloniaProperty.Register<Breadcrumb, IBinding?>(
|
||||
nameof(IconBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? IconBinding
|
||||
{
|
||||
get => GetValue(IconBindingProperty);
|
||||
set => SetValue(IconBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> CommandBindingProperty = AvaloniaProperty.Register<Breadcrumb, IBinding?>(
|
||||
nameof(CommandBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? CommandBinding
|
||||
{
|
||||
get => GetValue(CommandBindingProperty);
|
||||
set => SetValue(CommandBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> SeparatorProperty = AvaloniaProperty.Register<Breadcrumb, object?>(
|
||||
nameof(Separator));
|
||||
|
||||
/// <summary>
|
||||
/// Separator between items.
|
||||
/// Usage: Separator can only be raw string or ITemplate<Control>.
|
||||
/// </summary>
|
||||
public object? Separator
|
||||
{
|
||||
get => GetValue(SeparatorProperty);
|
||||
set => SetValue(SeparatorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<Breadcrumb, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
static Breadcrumb()
|
||||
{
|
||||
ItemsPanelProperty.OverrideDefaultValue<Breadcrumb>(_defaultPanel);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<BreadcrumbItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new BreadcrumbItem();
|
||||
}
|
||||
|
||||
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||
{
|
||||
// base.PrepareContainerForItemOverride(container, item, index);
|
||||
if (container is not BreadcrumbItem breadcrumbItem) return;
|
||||
if (!breadcrumbItem.IsSet(BreadcrumbItem.SeparatorProperty))
|
||||
{
|
||||
if (GetSeparatorInstance(Separator) is { } a)
|
||||
{
|
||||
breadcrumbItem.Separator = a;
|
||||
}
|
||||
SeparatorProperty.Changed.AddClassHandler<Breadcrumb, object?>((_, args) =>
|
||||
{
|
||||
if (GetSeparatorInstance(args.NewValue.Value) is { } b)
|
||||
breadcrumbItem.Separator = b;
|
||||
});
|
||||
}
|
||||
|
||||
PseudolassesExtensions.Set(container.Classes, BreadcrumbItem.PC_Last, index == ItemCount - 1);
|
||||
|
||||
if (container == item) return;
|
||||
if(!breadcrumbItem.IsSet(ContentControl.ContentProperty))
|
||||
{
|
||||
breadcrumbItem.SetCurrentValue(ContentControl.ContentProperty, item);
|
||||
if (DisplayMemberBinding is not null)
|
||||
{
|
||||
breadcrumbItem[!ContentControl.ContentProperty] = DisplayMemberBinding;
|
||||
}
|
||||
}
|
||||
if (!breadcrumbItem.IsSet(ContentControl.ContentTemplateProperty) && this.ItemTemplate != null)
|
||||
{
|
||||
breadcrumbItem.SetCurrentValue(ContentControl.ContentTemplateProperty, this.ItemTemplate);
|
||||
}
|
||||
if (!breadcrumbItem.IsSet(BreadcrumbItem.IconProperty) && IconBinding != null)
|
||||
{
|
||||
breadcrumbItem[!BreadcrumbItem.IconProperty] = IconBinding;
|
||||
}
|
||||
if (!breadcrumbItem.IsSet(BreadcrumbItem.CommandProperty) && CommandBinding != null)
|
||||
{
|
||||
breadcrumbItem[!BreadcrumbItem.CommandProperty] = CommandBinding;
|
||||
}
|
||||
if (!breadcrumbItem.IsSet(BreadcrumbItem.IconTemplateProperty) && IconTemplate != null)
|
||||
{
|
||||
breadcrumbItem.IconTemplate = IconTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
private static object? GetSeparatorInstance(object? separator) => separator switch
|
||||
{
|
||||
null => null,
|
||||
string s => s,
|
||||
ITemplate<Control?> t => t.Build(),
|
||||
_ => separator.ToString()
|
||||
};
|
||||
}
|
||||
69
src/Ursa/Controls/Breadcrumb/BreadcrumbItem.cs
Normal file
69
src/Ursa/Controls/Breadcrumb/BreadcrumbItem.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.LogicalTree;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Last)]
|
||||
public class BreadcrumbItem: ContentControl
|
||||
{
|
||||
public const string PC_Last = ":last";
|
||||
public static readonly StyledProperty<object?> SeparatorProperty =
|
||||
AvaloniaProperty.Register<BreadcrumbItem, object?>(
|
||||
nameof(Separator));
|
||||
|
||||
public object? Separator
|
||||
{
|
||||
get => GetValue(SeparatorProperty);
|
||||
set => SetValue(SeparatorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<BreadcrumbItem, object?>(
|
||||
nameof(Icon));
|
||||
|
||||
public object? Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ICommand?> CommandProperty = AvaloniaProperty.Register<BreadcrumbItem, ICommand?>(
|
||||
nameof(Command));
|
||||
|
||||
public ICommand? Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<BreadcrumbItem, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsReadOnlyProperty = AvaloniaProperty.Register<BreadcrumbItem, bool>(
|
||||
nameof(IsReadOnly));
|
||||
|
||||
public bool IsReadOnly
|
||||
{
|
||||
get => GetValue(IsReadOnlyProperty);
|
||||
set => SetValue(IsReadOnlyProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
Command?.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ButtonGroup: ItemsControl
|
||||
{
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
recycleKey = null;
|
||||
return item is not Button;
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new Button();
|
||||
}
|
||||
}
|
||||
71
src/Ursa/Controls/Buttons/ButtonGroup.cs
Normal file
71
src/Ursa/Controls/Buttons/ButtonGroup.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ButtonGroup: ItemsControl
|
||||
{
|
||||
public static readonly StyledProperty<IBinding?> CommandBindingProperty = AvaloniaProperty.Register<ButtonGroup, IBinding?>(
|
||||
nameof(CommandBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? CommandBinding
|
||||
{
|
||||
get => GetValue(CommandBindingProperty);
|
||||
set => SetValue(CommandBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> CommandParameterBindingProperty = AvaloniaProperty.Register<ButtonGroup, IBinding?>(
|
||||
nameof(CommandParameterBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? CommandParameterBinding
|
||||
{
|
||||
get => GetValue(CommandParameterBindingProperty);
|
||||
set => SetValue(CommandParameterBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> ContentBindingProperty = AvaloniaProperty.Register<ButtonGroup, IBinding?>(
|
||||
nameof(ContentBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? ContentBinding
|
||||
{
|
||||
get => GetValue(ContentBindingProperty);
|
||||
set => SetValue(ContentBindingProperty, value);
|
||||
}
|
||||
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
recycleKey = null;
|
||||
return item is not Button;
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new Button();
|
||||
}
|
||||
|
||||
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||
{
|
||||
base.PrepareContainerForItemOverride(container, item, index);
|
||||
if(container is Button button)
|
||||
{
|
||||
button.TryBind(Button.CommandProperty, CommandBinding);
|
||||
button.TryBind(Button.CommandParameterProperty, CommandParameterBinding);
|
||||
button.TryBind(ContentControl.ContentProperty, ContentBinding);
|
||||
button.TryBind(ContentControl.ContentTemplateProperty, this[!ItemTemplateProperty]);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/Ursa/Controls/Buttons/IconButton.cs
Normal file
91
src/Ursa/Controls/Buttons/IconButton.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Layout;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Right, PC_Left, PC_Top, PC_Bottom, PC_Empty)]
|
||||
public class IconButton: Button
|
||||
{
|
||||
public const string PC_Right = ":right";
|
||||
public const string PC_Left = ":left";
|
||||
public const string PC_Top = ":top";
|
||||
public const string PC_Bottom = ":bottom";
|
||||
public const string PC_Empty = ":empty";
|
||||
|
||||
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<IconButton, object?>(
|
||||
nameof(Icon));
|
||||
|
||||
public object? Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<IconButton, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsLoadingProperty = AvaloniaProperty.Register<IconButton, bool>(
|
||||
nameof(IsLoading));
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => GetValue(IsLoadingProperty);
|
||||
set => SetValue(IsLoadingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Position> IconPlacementProperty = AvaloniaProperty.Register<IconButton, Position>(
|
||||
nameof(IconPlacement), defaultValue: Position.Left);
|
||||
|
||||
public Position IconPlacement
|
||||
{
|
||||
get => GetValue(IconPlacementProperty);
|
||||
set => SetValue(IconPlacementProperty, value);
|
||||
}
|
||||
|
||||
static IconButton()
|
||||
{
|
||||
IconPlacementProperty.Changed.AddClassHandler<IconButton, Position>((o, e) =>
|
||||
{
|
||||
o.SetPlacement(e.NewValue.Value, o.Icon);
|
||||
});
|
||||
IconProperty.Changed.AddClassHandler<IconButton, object?>((o, e) =>
|
||||
{
|
||||
o.SetPlacement(o.IconPlacement, e.NewValue.Value);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
SetPlacement(IconPlacement, Icon);
|
||||
}
|
||||
|
||||
private void SetPlacement(Position placement, object? icon)
|
||||
{
|
||||
if (icon is null)
|
||||
{
|
||||
PseudoClasses.Set(PC_Empty, true);
|
||||
PseudoClasses.Set(PC_Left, false);
|
||||
PseudoClasses.Set(PC_Right, false);
|
||||
PseudoClasses.Set(PC_Top, false);
|
||||
PseudoClasses.Set(PC_Bottom, false);
|
||||
return;
|
||||
}
|
||||
PseudoClasses.Set(PC_Empty, false);
|
||||
PseudoClasses.Set(PC_Left, placement == Position.Left);
|
||||
PseudoClasses.Set(PC_Right, placement == Position.Right);
|
||||
PseudoClasses.Set(PC_Top, placement == Position.Top);
|
||||
PseudoClasses.Set(PC_Bottom, placement == Position.Bottom);
|
||||
}
|
||||
}
|
||||
141
src/Ursa/Controls/Clock/Clock.cs
Normal file
141
src/Ursa/Controls/Clock/Clock.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_ClockTicks, typeof(ClockTicks))]
|
||||
public class Clock: TemplatedControl
|
||||
{
|
||||
public const string PART_ClockTicks = "PART_ClockTicks";
|
||||
|
||||
public static readonly StyledProperty<DateTime> TimeProperty = AvaloniaProperty.Register<Clock, DateTime>(
|
||||
nameof(Time), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public DateTime Time
|
||||
{
|
||||
get => GetValue(TimeProperty);
|
||||
set => SetValue(TimeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowHourTicksProperty =
|
||||
ClockTicks.ShowHourTicksProperty.AddOwner<Clock>();
|
||||
|
||||
public bool ShowHourTicks
|
||||
{
|
||||
get => GetValue(ShowHourTicksProperty);
|
||||
set => SetValue(ShowHourTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowMinuteTicksProperty =
|
||||
ClockTicks.ShowMinuteTicksProperty.AddOwner<Clock>();
|
||||
|
||||
public bool ShowMinuteTicks
|
||||
{
|
||||
get => GetValue(ShowMinuteTicksProperty);
|
||||
set => SetValue(ShowMinuteTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> HandBrushProperty = AvaloniaProperty.Register<Clock, IBrush?>(
|
||||
nameof(HandBrush));
|
||||
|
||||
public IBrush? HandBrush
|
||||
{
|
||||
get => GetValue(HandBrushProperty);
|
||||
set => SetValue(HandBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowHourHandProperty = AvaloniaProperty.Register<Clock, bool>(
|
||||
nameof(ShowHourHand), defaultValue: true);
|
||||
|
||||
public bool ShowHourHand
|
||||
{
|
||||
get => GetValue(ShowHourHandProperty);
|
||||
set => SetValue(ShowHourHandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowMinuteHandProperty = AvaloniaProperty.Register<Clock, bool>(
|
||||
nameof(ShowMinuteHand), defaultValue: true);
|
||||
|
||||
public bool ShowMinuteHand
|
||||
{
|
||||
get => GetValue(ShowMinuteHandProperty);
|
||||
set => SetValue(ShowMinuteHandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowSecondHandProperty = AvaloniaProperty.Register<Clock, bool>(
|
||||
nameof(ShowSecondHand), defaultValue: true);
|
||||
|
||||
public bool ShowSecondHand
|
||||
{
|
||||
get => GetValue(ShowSecondHandProperty);
|
||||
set => SetValue(ShowSecondHandProperty, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static readonly DirectProperty<Clock, double> HourAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
|
||||
nameof(HourAngle), o => o.HourAngle);
|
||||
private double _hourAngle;
|
||||
public double HourAngle
|
||||
{
|
||||
get => _hourAngle;
|
||||
private set => SetAndRaise(HourAngleProperty, ref _hourAngle, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<Clock, double> MinuteAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
|
||||
nameof(MinuteAngle), o => o.MinuteAngle);
|
||||
private double _minuteAngle;
|
||||
public double MinuteAngle
|
||||
{
|
||||
get => _minuteAngle;
|
||||
private set => SetAndRaise(MinuteAngleProperty, ref _minuteAngle, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<Clock, double> SecondAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
|
||||
nameof(SecondAngle), o => o.SecondAngle);
|
||||
|
||||
private double _secondAngle;
|
||||
public double SecondAngle
|
||||
{
|
||||
get => _secondAngle;
|
||||
private set => SetAndRaise(SecondAngleProperty, ref _secondAngle, value);
|
||||
}
|
||||
|
||||
static Clock()
|
||||
{
|
||||
TimeProperty.Changed.AddClassHandler<Clock, DateTime>((clock, args)=>clock.OnTimeChanged(args));
|
||||
}
|
||||
|
||||
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs<DateTime> args)
|
||||
{
|
||||
DateTime time = args.NewValue.Value;
|
||||
var hour = time.Hour;
|
||||
var minute = time.Minute;
|
||||
var second = time.Second;
|
||||
var hourAngle = 360.0 / 12 * hour + 360.0 / 12 / 60 * minute;
|
||||
var minuteAngle = 360.0 / 60 * minute + 360.0 / 60 / 60 * second;
|
||||
var secondAngle = 360.0 / 60 * second;
|
||||
HourAngle = hourAngle;
|
||||
MinuteAngle = minuteAngle;
|
||||
SecondAngle = secondAngle;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
double min = Math.Min(availableSize.Height, availableSize.Width);
|
||||
var newSize = new Size(min, min);
|
||||
var size = base.MeasureOverride(newSize);
|
||||
return size;
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
double min = Math.Min(finalSize.Height, finalSize.Width);
|
||||
var newSize = new Size(min, min);
|
||||
var size = base.ArrangeOverride(newSize);
|
||||
return size;
|
||||
}
|
||||
}
|
||||
139
src/Ursa/Controls/Clock/ClockTicks.cs
Normal file
139
src/Ursa/Controls/Clock/ClockTicks.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ClockTicks: Control
|
||||
{
|
||||
private Matrix _hourRotationMatrix = Matrix.CreateRotation(Math.PI / 6);
|
||||
private Matrix _minuteRotationMatrix = Matrix.CreateRotation(Math.PI / 30);
|
||||
|
||||
public static readonly StyledProperty<bool> ShowHourTicksProperty = AvaloniaProperty.Register<ClockTicks, bool>(
|
||||
nameof(ShowHourTicks), true);
|
||||
|
||||
public bool ShowHourTicks
|
||||
{
|
||||
get => GetValue(ShowHourTicksProperty);
|
||||
set => SetValue(ShowHourTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowMinuteTicksProperty = AvaloniaProperty.Register<ClockTicks, bool>(
|
||||
nameof(ShowMinuteTicks), true);
|
||||
|
||||
public bool ShowMinuteTicks
|
||||
{
|
||||
get => GetValue(ShowMinuteTicksProperty);
|
||||
set => SetValue(ShowMinuteTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> HourTickForegroundProperty = AvaloniaProperty.Register<ClockTicks, IBrush?>(
|
||||
nameof(HourTickForeground));
|
||||
|
||||
public IBrush? HourTickForeground
|
||||
{
|
||||
get => GetValue(HourTickForegroundProperty);
|
||||
set => SetValue(HourTickForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> MinuteTickForegroundProperty = AvaloniaProperty.Register<ClockTicks, IBrush?>(
|
||||
nameof(MinuteTickForeground));
|
||||
|
||||
public IBrush? MinuteTickForeground
|
||||
{
|
||||
get => GetValue(MinuteTickForegroundProperty);
|
||||
set => SetValue(MinuteTickForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> HourTickLengthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(HourTickLength), 10);
|
||||
|
||||
public double HourTickLength
|
||||
{
|
||||
get => GetValue(HourTickLengthProperty);
|
||||
set => SetValue(HourTickLengthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MinuteTickLengthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(MinuteTickLength), 5);
|
||||
|
||||
public double MinuteTickLength
|
||||
{
|
||||
get => GetValue(MinuteTickLengthProperty);
|
||||
set => SetValue(MinuteTickLengthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> HourTickWidthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(HourTickWidth), 2);
|
||||
|
||||
public double HourTickWidth
|
||||
{
|
||||
get => GetValue(HourTickWidthProperty);
|
||||
set => SetValue(HourTickWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MinuteTickWidthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(MinuteTickWidth), 1);
|
||||
|
||||
public double MinuteTickWidth
|
||||
{
|
||||
get => GetValue(MinuteTickWidthProperty);
|
||||
set => SetValue(MinuteTickWidthProperty, value);
|
||||
}
|
||||
|
||||
static ClockTicks()
|
||||
{
|
||||
AffectsRender<ClockTicks>(ShowHourTicksProperty);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
double minSize= Math.Min(availableSize.Width, availableSize.Height);
|
||||
return new Size(minSize, minSize);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var minSize = Math.Min(finalSize.Width, finalSize.Height);
|
||||
return new Size(minSize, minSize);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
base.Render(context);
|
||||
var size = Math.Min(Bounds.Width, Bounds.Height);
|
||||
var center = size / 2;
|
||||
IPen hourTickPen = new Pen(HourTickForeground, HourTickWidth);
|
||||
IPen minuteTickPen = new Pen(MinuteTickForeground, MinuteTickWidth);
|
||||
double hourTickLength = Math.Min(center, HourTickLength);
|
||||
double minuteTickLength = Math.Min(center, MinuteTickLength);
|
||||
context.PushTransform(Matrix.CreateTranslation(center, center));
|
||||
if (ShowHourTicks)
|
||||
{
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
DrawTick(context, hourTickPen, center, hourTickLength);
|
||||
context.PushTransform(_hourRotationMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
if (ShowMinuteTicks)
|
||||
{
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
if (i % 5 != 0)
|
||||
{
|
||||
DrawTick(context, minuteTickPen, center, minuteTickLength);
|
||||
}
|
||||
context.PushTransform(_minuteRotationMatrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTick(DrawingContext context, IPen pen, double center, double length)
|
||||
{
|
||||
var start = new Point(0, -center);
|
||||
var end = new Point(0, length-center);
|
||||
context.DrawLine(pen, start, end);
|
||||
}
|
||||
}
|
||||
199
src/Ursa/Controls/ComboBox/MultiComboBox.cs
Normal file
199
src/Ursa/Controls/ComboBox/MultiComboBox.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_BackgroundBorder, typeof(Border))]
|
||||
[PseudoClasses(PC_DropDownOpen, PC_Empty)]
|
||||
public class MultiComboBox: SelectingItemsControl, IInnerContentControl
|
||||
{
|
||||
public const string PART_BackgroundBorder = "PART_BackgroundBorder";
|
||||
public const string PC_DropDownOpen = ":dropdownopen";
|
||||
public const string PC_Empty = ":selection-empty";
|
||||
|
||||
private Border? _rootBorder;
|
||||
|
||||
private static ITemplate<Panel?> _defaultPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel());
|
||||
|
||||
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
|
||||
ComboBox.IsDropDownOpenProperty.AddOwner<MultiComboBox>();
|
||||
|
||||
public bool IsDropDownOpen
|
||||
{
|
||||
get => GetValue(IsDropDownOpenProperty);
|
||||
set => SetValue(IsDropDownOpenProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MaxDropdownHeightProperty = AvaloniaProperty.Register<MultiComboBox, double>(
|
||||
nameof(MaxDropdownHeight));
|
||||
|
||||
public double MaxDropdownHeight
|
||||
{
|
||||
get => GetValue(MaxDropdownHeightProperty);
|
||||
set => SetValue(MaxDropdownHeightProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MaxSelectionBoxHeightProperty = AvaloniaProperty.Register<MultiComboBox, double>(
|
||||
nameof(MaxSelectionBoxHeight));
|
||||
|
||||
public double MaxSelectionBoxHeight
|
||||
{
|
||||
get => GetValue(MaxSelectionBoxHeightProperty);
|
||||
set => SetValue(MaxSelectionBoxHeightProperty, value);
|
||||
}
|
||||
|
||||
public new static readonly StyledProperty<IList?> SelectedItemsProperty = AvaloniaProperty.Register<MultiComboBox, IList?>(
|
||||
nameof(SelectedItems), new AvaloniaList<object>());
|
||||
|
||||
public new IList? SelectedItems
|
||||
{
|
||||
get => GetValue(SelectedItemsProperty);
|
||||
set => SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerLeftContentProperty = AvaloniaProperty.Register<MultiComboBox, object?>(
|
||||
nameof(InnerLeftContent));
|
||||
|
||||
public object? InnerLeftContent
|
||||
{
|
||||
get => GetValue(InnerLeftContentProperty);
|
||||
set => SetValue(InnerLeftContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerRightContentProperty = AvaloniaProperty.Register<MultiComboBox, object?>(
|
||||
nameof(InnerRightContent));
|
||||
|
||||
public object? InnerRightContent
|
||||
{
|
||||
get => GetValue(InnerRightContentProperty);
|
||||
set => SetValue(InnerRightContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> SelectedItemTemplateProperty = AvaloniaProperty.Register<MultiComboBox, IDataTemplate?>(
|
||||
nameof(SelectedItemTemplate));
|
||||
|
||||
public IDataTemplate? SelectedItemTemplate
|
||||
{
|
||||
get => GetValue(SelectedItemTemplateProperty);
|
||||
set => SetValue(SelectedItemTemplateProperty, value);
|
||||
}
|
||||
|
||||
static MultiComboBox()
|
||||
{
|
||||
FocusableProperty.OverrideDefaultValue<MultiComboBox>(true);
|
||||
ItemsPanelProperty.OverrideDefaultValue<MultiComboBox>(_defaultPanel);
|
||||
IsDropDownOpenProperty.AffectsPseudoClass<MultiComboBox>(PC_DropDownOpen);
|
||||
SelectedItemsProperty.Changed.AddClassHandler<MultiComboBox, IList?>((box, args) => box.OnSelectedItemsChanged(args));
|
||||
}
|
||||
|
||||
public MultiComboBox()
|
||||
{
|
||||
if (SelectedItems is INotifyCollectionChanged c)
|
||||
{
|
||||
c.CollectionChanged+=OnSelectedItemsCollectionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectedItemsChanged(AvaloniaPropertyChangedEventArgs<IList?> args)
|
||||
{
|
||||
if (args.OldValue.Value is INotifyCollectionChanged old)
|
||||
{
|
||||
old.CollectionChanged-=OnSelectedItemsCollectionChanged;
|
||||
}
|
||||
if (args.NewValue.Value is INotifyCollectionChanged @new)
|
||||
{
|
||||
@new.CollectionChanged += OnSelectedItemsCollectionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
PseudoClasses.Set(PC_Empty, SelectedItems?.Count == 0);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
recycleKey = item;
|
||||
return item is not MultiComboBoxItem;
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new MultiComboBoxItem();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
PointerPressedEvent.RemoveHandler(OnBackgroundPointerPressed, _rootBorder);
|
||||
_rootBorder = e.NameScope.Find<Border>(PART_BackgroundBorder);
|
||||
PointerPressedEvent.AddHandler(OnBackgroundPointerPressed, _rootBorder);
|
||||
PseudoClasses.Set(PC_Empty, SelectedItems?.Count == 0);
|
||||
}
|
||||
|
||||
private void OnBackgroundPointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
|
||||
}
|
||||
|
||||
internal void ItemFocused(MultiComboBoxItem dropDownItem)
|
||||
{
|
||||
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
|
||||
{
|
||||
dropDownItem.BringIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(object? o)
|
||||
{
|
||||
if (o is StyledElement s)
|
||||
{
|
||||
var data = s.DataContext;
|
||||
this.SelectedItems?.Remove(data);
|
||||
var item = this.Items.FirstOrDefault(a => ReferenceEquals(a, data));
|
||||
if (item is not null)
|
||||
{
|
||||
var container = ContainerFromItem(item);
|
||||
if (container is MultiComboBoxItem t)
|
||||
{
|
||||
t.IsSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
// this.SelectedItems?.Clear();
|
||||
var containers = Presenter?.Panel?.Children;
|
||||
if(containers is null) return;
|
||||
foreach (var container in containers)
|
||||
{
|
||||
if (container is MultiComboBoxItem t)
|
||||
{
|
||||
t.IsSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUnloaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnUnloaded(e);
|
||||
if (SelectedItems is INotifyCollectionChanged c)
|
||||
{
|
||||
c.CollectionChanged-=OnSelectedItemsCollectionChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs
Normal file
97
src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.LogicalTree;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class MultiComboBoxItem: ContentControl
|
||||
{
|
||||
private MultiComboBox? _parent;
|
||||
private static readonly Point s_invalidPoint = new (double.NaN, double.NaN);
|
||||
private Point _pointerDownPoint = s_invalidPoint;
|
||||
|
||||
public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<MultiComboBoxItem, bool>(
|
||||
nameof(IsSelected));
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => GetValue(IsSelectedProperty);
|
||||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
static MultiComboBoxItem()
|
||||
{
|
||||
IsSelectedProperty.AffectsPseudoClass<MultiComboBoxItem>(":selected");
|
||||
PressedMixin.Attach<MultiComboBoxItem>();
|
||||
FocusableProperty.OverrideDefaultValue<MultiComboBoxItem>(true);
|
||||
IsSelectedProperty.Changed.AddClassHandler<MultiComboBoxItem, bool>((item, args) =>
|
||||
item.OnSelectionChanged(args));
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
var parent = this.FindLogicalAncestorOfType<MultiComboBox>();
|
||||
if (args.NewValue.Value)
|
||||
{
|
||||
parent?.SelectedItems?.Add(this.DataContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent?.SelectedItems?.Remove(this.DataContext);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToLogicalTree(e);
|
||||
_parent = this.FindLogicalAncestorOfType<MultiComboBox>();
|
||||
if(this.IsSelected)
|
||||
_parent?.SelectedItems?.Add(this.DataContext);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
_pointerDownPoint = e.GetPosition(this);
|
||||
if (e.Handled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
var p = e.GetCurrentPoint(this);
|
||||
if (p.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed
|
||||
or PointerUpdateKind.RightButtonPressed)
|
||||
{
|
||||
if (p.Pointer.Type == PointerType.Mouse)
|
||||
{
|
||||
this.IsSelected = !this.IsSelected;
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pointerDownPoint = p.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
if (!e.Handled && !double.IsNaN(_pointerDownPoint.X) &&
|
||||
e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right)
|
||||
{
|
||||
var point = e.GetCurrentPoint(this);
|
||||
if (new Rect(Bounds.Size).ContainsExclusive(point.Position) && e.Pointer.Type == PointerType.Touch)
|
||||
{
|
||||
this.IsSelected = !this.IsSelected;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Ursa/Controls/ComboBox/MultiComboBoxSelectedItemList.cs
Normal file
36
src/Ursa/Controls/ComboBox/MultiComboBoxSelectedItemList.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class MultiComboBoxSelectedItemList: ItemsControl
|
||||
{
|
||||
public static readonly StyledProperty<ICommand?> RemoveCommandProperty = AvaloniaProperty.Register<MultiComboBoxSelectedItemList, ICommand?>(
|
||||
nameof(RemoveCommand));
|
||||
|
||||
public ICommand? RemoveCommand
|
||||
{
|
||||
get => GetValue(RemoveCommandProperty);
|
||||
set => SetValue(RemoveCommandProperty, value);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<ClosableTag>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new ClosableTag();
|
||||
}
|
||||
|
||||
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||
{
|
||||
base.PrepareContainerForItemOverride(container, item, index);
|
||||
if (container is ClosableTag tag)
|
||||
{
|
||||
tag.Command = RemoveCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
339
src/Ursa/Controls/ComboBox/TreeComboBox.cs
Normal file
339
src/Ursa/Controls/ComboBox/TreeComboBox.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
using Avalonia.VisualTree;
|
||||
using Irihi.Avalonia.Shared.Common;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Size = Avalonia.Size;
|
||||
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PartNames.PART_Popup, typeof(Popup))]
|
||||
[PseudoClasses(PC_DropdownOpen)]
|
||||
public class TreeComboBox: ItemsControl, IClearControl, IInnerContentControl, IPopupInnerContent
|
||||
{
|
||||
public const string PC_DropdownOpen = ":dropdownopen";
|
||||
|
||||
private Popup? _popup;
|
||||
|
||||
private static readonly FuncTemplate<Panel?> DefaultPanel =
|
||||
new FuncTemplate<Panel?>(() => new VirtualizingStackPanel());
|
||||
|
||||
public static readonly StyledProperty<double> MaxDropDownHeightProperty =
|
||||
ComboBox.MaxDropDownHeightProperty.AddOwner<TreeComboBox>();
|
||||
|
||||
public double MaxDropDownHeight
|
||||
{
|
||||
get => GetValue(MaxDropDownHeightProperty);
|
||||
set => SetValue(MaxDropDownHeightProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> WatermarkProperty =
|
||||
TextBox.WatermarkProperty.AddOwner<TreeComboBox>();
|
||||
|
||||
public string? Watermark
|
||||
{
|
||||
get => GetValue(WatermarkProperty);
|
||||
set => SetValue(WatermarkProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
|
||||
ComboBox.IsDropDownOpenProperty.AddOwner<TreeComboBox>();
|
||||
|
||||
public bool IsDropDownOpen
|
||||
{
|
||||
get => GetValue(IsDropDownOpenProperty);
|
||||
set => SetValue(IsDropDownOpenProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
|
||||
ContentControl.HorizontalContentAlignmentProperty.AddOwner<TreeComboBox>();
|
||||
|
||||
public HorizontalAlignment HorizontalContentAlignment
|
||||
{
|
||||
get => GetValue(HorizontalContentAlignmentProperty);
|
||||
set => SetValue(HorizontalContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
|
||||
ContentControl.VerticalContentAlignmentProperty.AddOwner<TreeComboBox>();
|
||||
|
||||
public VerticalAlignment VerticalContentAlignment
|
||||
{
|
||||
get => GetValue(VerticalContentAlignmentProperty);
|
||||
set => SetValue(VerticalContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> SelectedItemTemplateProperty =
|
||||
AvaloniaProperty.Register<TreeComboBox, IDataTemplate?>(nameof(SelectedItemTemplate));
|
||||
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IDataTemplate? SelectedItemTemplate
|
||||
{
|
||||
get => GetValue(SelectedItemTemplateProperty);
|
||||
set => SetValue(SelectedItemTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<TreeComboBox, object?> SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect<TreeComboBox, object?>(
|
||||
nameof(SelectionBoxItem), o => o.SelectionBoxItem);
|
||||
private object? _selectionBoxItem;
|
||||
public object? SelectionBoxItem
|
||||
{
|
||||
get => _selectionBoxItem;
|
||||
protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
|
||||
}
|
||||
|
||||
private object? _selectedItem;
|
||||
|
||||
public static readonly DirectProperty<TreeComboBox, object?> SelectedItemProperty =
|
||||
AvaloniaProperty.RegisterDirect<TreeComboBox, object?>(
|
||||
nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v,
|
||||
defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => _selectedItem;
|
||||
set => SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerLeftContentProperty = AvaloniaProperty.Register<TreeComboBox, object?>(
|
||||
nameof(InnerLeftContent));
|
||||
|
||||
public object? InnerLeftContent
|
||||
{
|
||||
get => GetValue(InnerLeftContentProperty);
|
||||
set => SetValue(InnerLeftContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerRightContentProperty = AvaloniaProperty.Register<TreeComboBox, object?>(
|
||||
nameof(InnerRightContent));
|
||||
|
||||
public object? InnerRightContent
|
||||
{
|
||||
get => GetValue(InnerRightContentProperty);
|
||||
set => SetValue(InnerRightContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> PopupInnerTopContentProperty = AvaloniaProperty.Register<TreeComboBox, object?>(
|
||||
nameof(PopupInnerTopContent));
|
||||
|
||||
public object? PopupInnerTopContent
|
||||
{
|
||||
get => GetValue(PopupInnerTopContentProperty);
|
||||
set => SetValue(PopupInnerTopContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> PopupInnerBottomContentProperty = AvaloniaProperty.Register<TreeComboBox, object?>(
|
||||
nameof(PopupInnerBottomContent));
|
||||
|
||||
public object? PopupInnerBottomContent
|
||||
{
|
||||
get => GetValue(PopupInnerBottomContentProperty);
|
||||
set => SetValue(PopupInnerBottomContentProperty, value);
|
||||
}
|
||||
|
||||
static TreeComboBox()
|
||||
{
|
||||
ItemsPanelProperty.OverrideDefaultValue<TreeComboBox>(DefaultPanel);
|
||||
FocusableProperty.OverrideDefaultValue<TreeComboBox>(true);
|
||||
SelectedItemProperty.Changed.AddClassHandler<TreeComboBox, object?>((box, args) => box.OnSelectedItemChanged(args));
|
||||
IsDropDownOpenProperty.AffectsPseudoClass<TreeComboBox>(PC_DropdownOpen);
|
||||
PressedMixin.Attach<TreeComboBox>();
|
||||
}
|
||||
|
||||
private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
MarkContainerSelection(args.OldValue.Value, false);
|
||||
MarkContainerSelection(args.NewValue.Value, true);
|
||||
UpdateSelectionBoxItem(args.NewValue.Value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_popup = e.NameScope.Find<Popup>(PartNames.PART_Popup);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<TreeComboBoxItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
internal bool NeedsContainerInternal(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainerOverride(item, index, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new TreeComboBoxItem();
|
||||
}
|
||||
|
||||
internal Control CreateContainerForItemInternal(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return CreateContainerForItemOverride(item, index, recycleKey);
|
||||
}
|
||||
|
||||
internal void ContainerForItemPreparedInternal(Control container, object? item, int index)
|
||||
{
|
||||
ContainerForItemPreparedOverride(container, item, index);
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
if (e is { InitialPressMouseButton: MouseButton.Left, Source: Visual source })
|
||||
{
|
||||
if (_popup is not null && _popup.IsOpen && _popup.IsInsidePopup(source))
|
||||
{
|
||||
var container = GetContainerFromEventSource(source);
|
||||
if (container is null) return;
|
||||
var item = TreeItemFromContainer(container);
|
||||
if (item is null) return;
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
var selectedContainer = TreeContainerFromItem(SelectedItem);
|
||||
if(selectedContainer is TreeComboBoxItem selectedTreeComboBoxItem)
|
||||
{
|
||||
selectedTreeComboBoxItem.IsSelected = false;
|
||||
}
|
||||
}
|
||||
this.SelectedItem = item;
|
||||
container.IsSelected = true;
|
||||
IsDropDownOpen = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsDropDownOpen = !IsDropDownOpen;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelectionBoxItem(object? item)
|
||||
{
|
||||
if(item is null) SelectionBoxItem = null;
|
||||
if (item is ContentControl contentControl)
|
||||
{
|
||||
item = contentControl.Content;
|
||||
}
|
||||
else if(item is HeaderedItemsControl headeredItemsControl)
|
||||
{
|
||||
item = headeredItemsControl.Header;
|
||||
}
|
||||
|
||||
if (item is Control control)
|
||||
{
|
||||
if (VisualRoot == null) return;
|
||||
control.Measure(Size.Infinity);
|
||||
SelectionBoxItem = new Rectangle
|
||||
{
|
||||
Width = control.DesiredSize.Width,
|
||||
Height = control.DesiredSize.Height,
|
||||
Fill = new VisualBrush
|
||||
{
|
||||
Visual = control,
|
||||
Stretch = Stretch.None,
|
||||
AlignmentX = AlignmentX.Left,
|
||||
}
|
||||
};
|
||||
// TODO: Implement flow direction udpate
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ItemTemplate is null && DisplayMemberBinding is { } binding)
|
||||
{
|
||||
var template = new FuncDataTemplate<object?>((a,_) => new TextBlock
|
||||
{
|
||||
[DataContextProperty] = a,
|
||||
[!TextBlock.TextProperty] = binding,
|
||||
});
|
||||
var textBlock = template.Build(item);
|
||||
SelectionBoxItem = textBlock;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectionBoxItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TreeComboBoxItem? GetContainerFromEventSource(object eventSource)
|
||||
{
|
||||
if (eventSource is Visual visual)
|
||||
{
|
||||
var item = visual.GetSelfAndVisualAncestors().OfType<TreeComboBoxItem>().FirstOrDefault();
|
||||
return item?.Owner == this ? item : null!;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private object? TreeItemFromContainer(Control container)
|
||||
{
|
||||
return TreeItemFromContainer(this, container);
|
||||
}
|
||||
|
||||
private Control? TreeContainerFromItem(object item)
|
||||
{
|
||||
return TreeContainerFromItem(this, item);
|
||||
}
|
||||
|
||||
private static Control? TreeContainerFromItem(ItemsControl itemsControl, object item)
|
||||
{
|
||||
if (itemsControl.ContainerFromItem(item) is { } container)
|
||||
{
|
||||
return container;
|
||||
}
|
||||
foreach (var child in itemsControl.GetRealizedContainers())
|
||||
{
|
||||
if(child is ItemsControl childItemsControl && TreeContainerFromItem(childItemsControl, item) is { } childContainer)
|
||||
{
|
||||
return childContainer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? TreeItemFromContainer(ItemsControl itemsControl, Control container)
|
||||
{
|
||||
if (itemsControl.ItemFromContainer(container) is { } item)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
foreach (var child in itemsControl.GetRealizedContainers())
|
||||
{
|
||||
if(child is ItemsControl childItemsControl && TreeItemFromContainer(childItemsControl, container) is { } childItem)
|
||||
{
|
||||
return childItem;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void MarkContainerSelection(object? item, bool selected)
|
||||
{
|
||||
if (item is null) return;
|
||||
var container = TreeContainerFromItem(item);
|
||||
if (container is TreeComboBoxItem treeComboBoxItem)
|
||||
{
|
||||
treeComboBoxItem.IsSelected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
}
|
||||
123
src/Ursa/Controls/ComboBox/TreeComboBoxItem.cs
Normal file
123
src/Ursa/Controls/ComboBox/TreeComboBoxItem.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using Irihi.Avalonia.Shared.Common;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PartNames.PART_Header, typeof(Control))]
|
||||
public class TreeComboBoxItem: HeaderedItemsControl, ISelectable
|
||||
{
|
||||
private Control? _header;
|
||||
private TreeComboBox? _treeComboBox;
|
||||
public TreeComboBox? Owner => _treeComboBox;
|
||||
|
||||
public static readonly StyledProperty<bool> IsSelectedProperty = TreeViewItem.IsSelectedProperty.AddOwner<TreeComboBoxItem>();
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => GetValue(IsSelectedProperty);
|
||||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsExpandedProperty = TreeViewItem.IsExpandedProperty.AddOwner<TreeComboBoxItem>();
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => GetValue(IsExpandedProperty);
|
||||
set => SetValue(IsExpandedProperty, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static readonly DirectProperty<TreeComboBoxItem, int> LevelProperty = AvaloniaProperty.RegisterDirect<TreeComboBoxItem, int>(
|
||||
nameof(Level), o => o.Level, (o, v) => o.Level = v);
|
||||
private int _level;
|
||||
public int Level
|
||||
{
|
||||
get => _level;
|
||||
protected set => SetAndRaise(LevelProperty, ref _level, value);
|
||||
}
|
||||
|
||||
static TreeComboBoxItem()
|
||||
{
|
||||
IsSelectedProperty.AffectsPseudoClass<TreeComboBoxItem>(PseudoClassName.PC_Selected,
|
||||
SelectingItemsControl.IsSelectedChangedEvent);
|
||||
PressedMixin.Attach<TreeComboBoxItem>();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
DoubleTappedEvent.RemoveHandler(OnDoubleTapped, this);
|
||||
_header = e.NameScope.Find<Control>(PartNames.PART_Header);
|
||||
DoubleTappedEvent.AddHandler(OnDoubleTapped, RoutingStrategies.Tunnel, true, this);
|
||||
}
|
||||
|
||||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToLogicalTree(e);
|
||||
_treeComboBox = this.FindLogicalAncestorOfType<TreeComboBox>();
|
||||
Level = CalculateDistanceFromLogicalParent<TreeComboBox>(this) - 1;
|
||||
if (this.ItemTemplate is null && this._treeComboBox?.ItemTemplate is not null)
|
||||
{
|
||||
SetCurrentValue(ItemTemplateProperty, this._treeComboBox.ItemTemplate);
|
||||
}
|
||||
if(this.ItemContainerTheme is null && this._treeComboBox?.ItemContainerTheme is not null)
|
||||
{
|
||||
SetCurrentValue(ItemContainerThemeProperty, this._treeComboBox.ItemContainerTheme);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDoubleTapped(object sender, TappedEventArgs e)
|
||||
{
|
||||
if (this.ItemCount <= 0) return;
|
||||
this.SetCurrentValue(IsExpandedProperty, !IsExpanded);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
TreeViewItem t = new TreeViewItem();
|
||||
ComboBox c = new ComboBox();
|
||||
return EnsureParent().NeedsContainerInternal(item, index, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return EnsureParent().CreateContainerForItemInternal(item, index, recycleKey);
|
||||
}
|
||||
|
||||
protected override void ContainerForItemPreparedOverride(Control container, object? item, int index)
|
||||
{
|
||||
EnsureParent().ContainerForItemPreparedInternal(container, item, index);
|
||||
}
|
||||
|
||||
// TODO replace with helper method from shared library.
|
||||
private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T: ILogical
|
||||
{
|
||||
int distance = 0;
|
||||
ILogical? parent = logical;
|
||||
while (parent is not null)
|
||||
{
|
||||
if (parent is T) return distance;
|
||||
parent = parent.LogicalParent;
|
||||
distance++;
|
||||
}
|
||||
return @default;
|
||||
}
|
||||
|
||||
private TreeComboBox EnsureParent()
|
||||
{
|
||||
return this._treeComboBox ??
|
||||
throw new InvalidOperationException("TreeComboBoxItem must be a part of TreeComboBox");
|
||||
}
|
||||
}
|
||||
177
src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs
Normal file
177
src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ControlClassesInput: TemplatedControl
|
||||
{
|
||||
LinkedList<List<string>> _history = new();
|
||||
LinkedList<List<string>> _undoHistory = new();
|
||||
private bool _disableHistory = false;
|
||||
|
||||
public int CountOfHistoricalRecord { get; set; } = 10;
|
||||
|
||||
public static readonly StyledProperty<Control?> TargetProperty = AvaloniaProperty.Register<ControlClassesInput, Control?>(
|
||||
nameof(Target));
|
||||
|
||||
public Control? Target
|
||||
{
|
||||
get => GetValue(TargetProperty);
|
||||
set => SetValue(TargetProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string> SeparatorProperty =
|
||||
TagInput.SeparatorProperty.AddOwner<ControlClassesInput>();
|
||||
|
||||
public string Separator
|
||||
{
|
||||
get => GetValue(SeparatorProperty);
|
||||
set => SetValue(SeparatorProperty, value);
|
||||
}
|
||||
|
||||
|
||||
private ObservableCollection<string> _targetClasses;
|
||||
|
||||
internal static readonly DirectProperty<ControlClassesInput, ObservableCollection<string>> TargetClassesProperty = AvaloniaProperty.RegisterDirect<ControlClassesInput, ObservableCollection<string>>(
|
||||
nameof(TargetClasses), o => o.TargetClasses, (o, v) => o.TargetClasses = v);
|
||||
|
||||
internal ObservableCollection<string> TargetClasses
|
||||
{
|
||||
get => _targetClasses;
|
||||
set => SetAndRaise(TargetClassesProperty, ref _targetClasses, value);
|
||||
}
|
||||
|
||||
public static readonly AttachedProperty<ControlClassesInput?> SourceProperty =
|
||||
AvaloniaProperty.RegisterAttached<ControlClassesInput, StyledElement, ControlClassesInput?>("Source");
|
||||
|
||||
public static void SetSource(StyledElement obj, ControlClassesInput value) => obj.SetValue(SourceProperty, value);
|
||||
public static ControlClassesInput? GetSource(StyledElement obj) => obj.GetValue(SourceProperty);
|
||||
|
||||
static ControlClassesInput()
|
||||
{
|
||||
TargetClassesProperty.Changed.AddClassHandler<ControlClassesInput, ObservableCollection<string>>((o,e)=>o.OnClassesChanged(e));
|
||||
SourceProperty.Changed.AddClassHandler<StyledElement, ControlClassesInput?>(HandleSourceChange);
|
||||
}
|
||||
|
||||
public ControlClassesInput()
|
||||
{
|
||||
TargetClasses = new ObservableCollection<string>();
|
||||
//TargetClasses.CollectionChanged += InccOnCollectionChanged;
|
||||
}
|
||||
|
||||
private List<StyledElement> _targets = new();
|
||||
|
||||
private static void HandleSourceChange(StyledElement arg1, AvaloniaPropertyChangedEventArgs<ControlClassesInput?> arg2)
|
||||
{
|
||||
var newControl = arg2.NewValue.Value;
|
||||
if (newControl is null) return;
|
||||
newControl._targets.Add(arg1);
|
||||
var oldControl = arg2.OldValue.Value;
|
||||
if (oldControl is not null)
|
||||
{
|
||||
newControl._targets.Remove(oldControl);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClassesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<string>?> args)
|
||||
{
|
||||
var newValue = args.NewValue.Value;
|
||||
if (newValue is null)
|
||||
{
|
||||
SaveHistory(new List<string>(), true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var classes = newValue.Where(a => !string.IsNullOrWhiteSpace(a)).Distinct().ToList();
|
||||
SaveHistory(classes, true);
|
||||
if (newValue is INotifyCollectionChanged incc)
|
||||
{
|
||||
incc.CollectionChanged+=InccOnCollectionChanged;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void InccOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if(_disableHistory) return;
|
||||
SaveHistory(TargetClasses?.ToList() ?? new List<string>(), true);
|
||||
}
|
||||
|
||||
private void SaveHistory(List<string> strings, bool fromInput)
|
||||
{
|
||||
_history.AddLast(strings);
|
||||
_undoHistory.Clear();
|
||||
if (_history.Count > CountOfHistoricalRecord)
|
||||
{
|
||||
_history.RemoveFirst();
|
||||
}
|
||||
SetClassesToTarget(fromInput);
|
||||
}
|
||||
|
||||
private void SetClassesToTarget(bool fromInput)
|
||||
{
|
||||
List<string> strings;
|
||||
if (_history.Count == 0)
|
||||
{
|
||||
strings = new List<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
strings = _history.Last.Value;
|
||||
}
|
||||
|
||||
if (!fromInput)
|
||||
{
|
||||
SetCurrentValue(TargetClassesProperty, new ObservableCollection<string>(strings));
|
||||
}
|
||||
if (Target is not null)
|
||||
{
|
||||
Target.Classes.Replace(strings);
|
||||
}
|
||||
foreach (var target in _targets)
|
||||
{
|
||||
target.Classes.Replace(strings);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnDo()
|
||||
{
|
||||
var node = _history.Last;
|
||||
_history.RemoveLast();
|
||||
_undoHistory.AddFirst(node);
|
||||
_disableHistory = true;
|
||||
TargetClasses.Clear();
|
||||
foreach (var value in _history.Last.Value)
|
||||
{
|
||||
TargetClasses.Add(value);
|
||||
}
|
||||
_disableHistory = false;
|
||||
SetClassesToTarget(false);
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
var node = _undoHistory.First;
|
||||
_undoHistory.RemoveFirst();
|
||||
_history.AddLast(node);
|
||||
_disableHistory = true;
|
||||
TargetClasses.Clear();
|
||||
foreach (var value in _history.Last.Value)
|
||||
{
|
||||
TargetClasses.Add(value);
|
||||
}
|
||||
_disableHistory = false;
|
||||
SetClassesToTarget(false);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SaveHistory(new List<string>(), false);
|
||||
}
|
||||
}
|
||||
187
src/Ursa/Controls/DateTimePicker/TimePicker.cs
Normal file
187
src/Ursa/Controls/DateTimePicker/TimePicker.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Common;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_TextBox, typeof(TextBox))]
|
||||
[TemplatePart(PartNames.PART_Popup, typeof(Popup))]
|
||||
[TemplatePart(PART_Presenter, typeof(TimePickerPresenter))]
|
||||
[TemplatePart(PART_Button, typeof(Button))]
|
||||
public class TimePicker : TimePickerBase, IClearControl
|
||||
{
|
||||
public const string PART_TextBox = "PART_TextBox";
|
||||
public const string PART_Presenter = "PART_Presenter";
|
||||
public const string PART_Button = "PART_Button";
|
||||
|
||||
public static readonly StyledProperty<TimeSpan?> SelectedTimeProperty =
|
||||
AvaloniaProperty.Register<TimePicker, TimeSpan?>(
|
||||
nameof(SelectedTime), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<TimePicker, string?>(
|
||||
nameof(Watermark));
|
||||
|
||||
private Button? _button;
|
||||
private Popup? _popup;
|
||||
private TimePickerPresenter? _presenter;
|
||||
private TextBox? _textBox;
|
||||
|
||||
|
||||
static TimePicker()
|
||||
{
|
||||
SelectedTimeProperty.Changed.AddClassHandler<TimePicker, TimeSpan?>((picker, args) =>
|
||||
picker.OnSelectionChanged(args));
|
||||
DisplayFormatProperty.Changed.AddClassHandler<TimePicker, string?>((picker, args) => picker.OnDisplayFormatChanged(args));
|
||||
}
|
||||
|
||||
private void OnDisplayFormatChanged(AvaloniaPropertyChangedEventArgs<string?> args)
|
||||
{
|
||||
if (_textBox is null) return;
|
||||
var time = SelectedTime;
|
||||
var date = new DateTime( 1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds);
|
||||
var text = date.ToString(DisplayFormat);
|
||||
_textBox.Text = text;
|
||||
}
|
||||
|
||||
public string? Watermark
|
||||
{
|
||||
get => GetValue(WatermarkProperty);
|
||||
set => SetValue(WatermarkProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan? SelectedTime
|
||||
{
|
||||
get => GetValue(SelectedTimeProperty);
|
||||
set => SetValue(SelectedTimeProperty, value);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Focus(NavigationMethod.Pointer);
|
||||
_presenter?.SetValue(TimePickerPresenter.TimeProperty, null);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _textBox);
|
||||
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox);
|
||||
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _textBox);
|
||||
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
|
||||
|
||||
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
|
||||
_popup = e.NameScope.Find<Popup>(PartNames.PART_Popup);
|
||||
_presenter = e.NameScope.Find<TimePickerPresenter>(PART_Presenter);
|
||||
_button = e.NameScope.Find<Button>(PART_Button);
|
||||
|
||||
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
|
||||
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
|
||||
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
|
||||
Button.ClickEvent.AddHandler(OnButtonClick, _button);
|
||||
|
||||
SetCurrentValue(SelectedTimeProperty, DateTime.Now.TimeOfDay);
|
||||
}
|
||||
|
||||
private void OnButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Focus(NavigationMethod.Pointer);
|
||||
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
|
||||
}
|
||||
|
||||
private void OnTextBoxPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, true);
|
||||
}
|
||||
|
||||
private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, true);
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Down)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, true);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Tab)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
|
||||
private void OnTextChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_textBox?.Text))
|
||||
{
|
||||
TimePickerPresenter.TimeProperty.SetValue(null, _presenter);
|
||||
}
|
||||
else if (DisplayFormat is null || DisplayFormat.Length == 0)
|
||||
{
|
||||
if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime))
|
||||
TimePickerPresenter.TimeProperty.SetValue(defaultTime, _presenter);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
|
||||
out var time)) TimePickerPresenter.TimeProperty.SetValue(time.TimeOfDay, _presenter);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args)
|
||||
{
|
||||
if (_textBox is null) return;
|
||||
var time = args.NewValue.Value;
|
||||
if (time is null)
|
||||
{
|
||||
_textBox.Text = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var date = new DateTime(1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds);
|
||||
var text = date.ToString(DisplayFormat);
|
||||
_textBox.Text = text;
|
||||
}
|
||||
|
||||
public void Confirm()
|
||||
{
|
||||
_presenter?.Confirm();
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
Focus();
|
||||
}
|
||||
|
||||
public void Dismiss()
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
Focus();
|
||||
}
|
||||
|
||||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
|
||||
{
|
||||
base.UpdateDataValidation(property, state, error);
|
||||
if (property == SelectedTimeProperty) DataValidationErrors.SetError(this, error);
|
||||
}
|
||||
}
|
||||
96
src/Ursa/Controls/DateTimePicker/TimePickerBase.cs
Normal file
96
src/Ursa/Controls/DateTimePicker/TimePickerBase.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public abstract class TimePickerBase : TemplatedControl, IInnerContentControl, IPopupInnerContent
|
||||
{
|
||||
public static readonly StyledProperty<string?> DisplayFormatProperty =
|
||||
AvaloniaProperty.Register<TimePicker, string?>(
|
||||
nameof(DisplayFormat), "HH:mm:ss");
|
||||
|
||||
public static readonly StyledProperty<string> PanelFormatProperty = AvaloniaProperty.Register<TimePicker, string>(
|
||||
nameof(PanelFormat), "HH mm ss");
|
||||
|
||||
public static readonly StyledProperty<bool> NeedConfirmationProperty = AvaloniaProperty.Register<TimePicker, bool>(
|
||||
nameof(NeedConfirmation));
|
||||
|
||||
public static readonly StyledProperty<object?> InnerLeftContentProperty =
|
||||
AvaloniaProperty.Register<TimePicker, object?>(
|
||||
nameof(InnerLeftContent));
|
||||
|
||||
public static readonly StyledProperty<object?> InnerRightContentProperty =
|
||||
AvaloniaProperty.Register<TimePicker, object?>(
|
||||
nameof(InnerRightContent));
|
||||
|
||||
|
||||
public static readonly StyledProperty<object?> PopupInnerTopContentProperty =
|
||||
AvaloniaProperty.Register<TimePicker, object?>(
|
||||
nameof(PopupInnerTopContent));
|
||||
|
||||
public static readonly StyledProperty<object?> PopupInnerBottomContentProperty =
|
||||
AvaloniaProperty.Register<TimePicker, object?>(
|
||||
nameof(PopupInnerBottomContent));
|
||||
|
||||
public static readonly StyledProperty<bool> IsDropdownOpenProperty = AvaloniaProperty.Register<TimePicker, bool>(
|
||||
nameof(IsDropdownOpen), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<bool> IsReadonlyProperty = AvaloniaProperty.Register<TimePicker, bool>(
|
||||
nameof(IsReadonly));
|
||||
|
||||
public bool IsReadonly
|
||||
{
|
||||
get => GetValue(IsReadonlyProperty);
|
||||
set => SetValue(IsReadonlyProperty, value);
|
||||
}
|
||||
|
||||
public bool IsDropdownOpen
|
||||
{
|
||||
get => GetValue(IsDropdownOpenProperty);
|
||||
set => SetValue(IsDropdownOpenProperty, value);
|
||||
}
|
||||
|
||||
public string? DisplayFormat
|
||||
{
|
||||
get => GetValue(DisplayFormatProperty);
|
||||
set => SetValue(DisplayFormatProperty, value);
|
||||
}
|
||||
|
||||
public string PanelFormat
|
||||
{
|
||||
get => GetValue(PanelFormatProperty);
|
||||
set => SetValue(PanelFormatProperty, value);
|
||||
}
|
||||
|
||||
public bool NeedConfirmation
|
||||
{
|
||||
get => GetValue(NeedConfirmationProperty);
|
||||
set => SetValue(NeedConfirmationProperty, value);
|
||||
}
|
||||
|
||||
public object? InnerLeftContent
|
||||
{
|
||||
get => GetValue(InnerLeftContentProperty);
|
||||
set => SetValue(InnerLeftContentProperty, value);
|
||||
}
|
||||
|
||||
public object? InnerRightContent
|
||||
{
|
||||
get => GetValue(InnerRightContentProperty);
|
||||
set => SetValue(InnerRightContentProperty, value);
|
||||
}
|
||||
|
||||
public object? PopupInnerTopContent
|
||||
{
|
||||
get => GetValue(PopupInnerTopContentProperty);
|
||||
set => SetValue(PopupInnerTopContentProperty, value);
|
||||
}
|
||||
|
||||
public object? PopupInnerBottomContent
|
||||
{
|
||||
get => GetValue(PopupInnerBottomContentProperty);
|
||||
set => SetValue(PopupInnerBottomContentProperty, value);
|
||||
}
|
||||
}
|
||||
325
src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs
Normal file
325
src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_PickerContainer, typeof(Grid))]
|
||||
[TemplatePart(PART_HourSelector, typeof(DateTimePickerPanel))]
|
||||
[TemplatePart(PART_MinuteSelector, typeof(DateTimePickerPanel))]
|
||||
[TemplatePart(PART_SecondSelector, typeof(DateTimePickerPanel))]
|
||||
[TemplatePart(PART_AmPmSelector, typeof(DateTimePickerPanel))]
|
||||
[TemplatePart(PART_HourScrollPanel, typeof(Control))]
|
||||
[TemplatePart(PART_MinuteScrollPanel, typeof(Control))]
|
||||
[TemplatePart(PART_SecondScrollPanel, typeof(Control))]
|
||||
[TemplatePart(PART_AmPmScrollPanel, typeof(Control))]
|
||||
[TemplatePart(PART_FirstSeparator, typeof(Control))]
|
||||
[TemplatePart(PART_SecondSeparator, typeof(Control))]
|
||||
[TemplatePart(PART_ThirdSeparator, typeof(Control))]
|
||||
public class TimePickerPresenter : TemplatedControl
|
||||
{
|
||||
public const string PART_HourSelector = "PART_HourSelector";
|
||||
public const string PART_MinuteSelector = "PART_MinuteSelector";
|
||||
public const string PART_SecondSelector = "PART_SecondSelector";
|
||||
public const string PART_AmPmSelector = "PART_AmPmSelector";
|
||||
public const string PART_PickerContainer = "PART_PickerContainer";
|
||||
|
||||
public const string PART_HourScrollPanel = "PART_HourScrollPanel";
|
||||
public const string PART_MinuteScrollPanel = "PART_MinuteScrollPanel";
|
||||
public const string PART_SecondScrollPanel = "PART_SecondScrollPanel";
|
||||
public const string PART_AmPmScrollPanel = "PART_AmPmScrollPanel";
|
||||
|
||||
public const string PART_FirstSeparator = "PART_FirstSeparator";
|
||||
public const string PART_SecondSeparator = "PART_SecondSeparator";
|
||||
public const string PART_ThirdSeparator = "PART_ThirdSeparator";
|
||||
|
||||
|
||||
public static readonly StyledProperty<bool> NeedsConfirmationProperty =
|
||||
AvaloniaProperty.Register<TimePickerPresenter, bool>(
|
||||
nameof(NeedsConfirmation));
|
||||
|
||||
public static readonly StyledProperty<int> MinuteIncrementProperty =
|
||||
AvaloniaProperty.Register<TimePickerPresenter, int>(
|
||||
nameof(MinuteIncrement));
|
||||
|
||||
public static readonly StyledProperty<int> SecondIncrementProperty =
|
||||
AvaloniaProperty.Register<TimePickerPresenter, int>(
|
||||
nameof(SecondIncrement));
|
||||
|
||||
public static readonly StyledProperty<TimeSpan?> TimeProperty =
|
||||
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan?>(
|
||||
nameof(Time));
|
||||
|
||||
public static readonly StyledProperty<string> PanelFormatProperty =
|
||||
AvaloniaProperty.Register<TimePickerPresenter, string>(
|
||||
nameof(PanelFormat), "HH mm ss t");
|
||||
|
||||
private Control? _ampmScrollPanel;
|
||||
private DateTimePickerPanel? _ampmSelector;
|
||||
private Control? _firstSeparator;
|
||||
private Control? _hourScrollPanel;
|
||||
|
||||
private DateTimePickerPanel? _hourSelector;
|
||||
private Control? _minuteScrollPanel;
|
||||
private DateTimePickerPanel? _minuteSelector;
|
||||
private Grid? _pickerContainer;
|
||||
private Control? _secondScrollPanel;
|
||||
private DateTimePickerPanel? _secondSelector;
|
||||
private Control? _secondSeparator;
|
||||
private Control? _thirdSeparator;
|
||||
internal TimeSpan _timeHolder;
|
||||
private bool _updateFromTimeChange;
|
||||
private bool _use12Clock;
|
||||
|
||||
static TimePickerPresenter()
|
||||
{
|
||||
PanelFormatProperty.Changed.AddClassHandler<TimePickerPresenter, string>((presenter, args) =>
|
||||
presenter.OnPanelFormatChanged(args));
|
||||
TimeProperty.Changed.AddClassHandler<TimePickerPresenter, TimeSpan?>((presenter, args) =>
|
||||
presenter.OnTimeChanged(args));
|
||||
}
|
||||
|
||||
public TimePickerPresenter()
|
||||
{
|
||||
SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
|
||||
}
|
||||
|
||||
public bool NeedsConfirmation
|
||||
{
|
||||
get => GetValue(NeedsConfirmationProperty);
|
||||
set => SetValue(NeedsConfirmationProperty, value);
|
||||
}
|
||||
|
||||
public int MinuteIncrement
|
||||
{
|
||||
get => GetValue(MinuteIncrementProperty);
|
||||
set => SetValue(MinuteIncrementProperty, value);
|
||||
}
|
||||
|
||||
public int SecondIncrement
|
||||
{
|
||||
get => GetValue(SecondIncrementProperty);
|
||||
set => SetValue(SecondIncrementProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan? Time
|
||||
{
|
||||
get => GetValue(TimeProperty);
|
||||
set => SetValue(TimeProperty, value);
|
||||
}
|
||||
|
||||
public string PanelFormat
|
||||
{
|
||||
get => GetValue(PanelFormatProperty);
|
||||
set => SetValue(PanelFormatProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler<TimePickerSelectedValueChangedEventArgs>? SelectedTimeChanged;
|
||||
|
||||
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args)
|
||||
{
|
||||
_updateFromTimeChange = true;
|
||||
UpdatePanelsFromSelectedTime(args.NewValue.Value);
|
||||
_updateFromTimeChange = false;
|
||||
SelectedTimeChanged?.Invoke(this,
|
||||
new TimePickerSelectedValueChangedEventArgs(args.OldValue.Value, args.NewValue.Value));
|
||||
}
|
||||
|
||||
private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs<string> args)
|
||||
{
|
||||
var format = args.NewValue.Value;
|
||||
UpdatePanelLayout(format);
|
||||
}
|
||||
|
||||
private void UpdatePanelLayout(string? panelFormat)
|
||||
{
|
||||
if (panelFormat is null) return;
|
||||
var parts = panelFormat.Split(new[] { ' ', '-', ':' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var panels = new List<Control?>();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.Length < 1) continue;
|
||||
try
|
||||
{
|
||||
if ((part.Contains('h') || part.Contains('H')) && !panels.Contains(_hourScrollPanel))
|
||||
{
|
||||
panels.Add(_hourScrollPanel);
|
||||
_use12Clock = part.Contains('h');
|
||||
_hourSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part.ToLower());
|
||||
if (_hourSelector is not null)
|
||||
{
|
||||
_hourSelector.MaximumValue = _use12Clock ? 12 : 23;
|
||||
_hourSelector.MinimumValue = _use12Clock ? 1 : 0;
|
||||
}
|
||||
}
|
||||
else if (part[0] == 'm' && !panels.Contains(_minuteSelector))
|
||||
{
|
||||
panels.Add(_minuteScrollPanel);
|
||||
_minuteSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part);
|
||||
}
|
||||
else if (part[0] == 's' && !panels.Contains(_secondScrollPanel))
|
||||
{
|
||||
panels.Add(_secondScrollPanel);
|
||||
_secondSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part.Replace('s', 'm'));
|
||||
}
|
||||
else if (part[0] == 't' && !panels.Contains(_ampmScrollPanel))
|
||||
{
|
||||
panels.Add(_ampmScrollPanel);
|
||||
_ampmSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (panels.Count < 1) return;
|
||||
IsVisibleProperty.SetValue(false, _hourScrollPanel, _minuteScrollPanel, _secondScrollPanel, _ampmScrollPanel,
|
||||
_firstSeparator, _secondSeparator, _thirdSeparator);
|
||||
for (var i = 0; i < panels.Count; i++)
|
||||
{
|
||||
var panel = panels[i];
|
||||
if (panel is null) continue;
|
||||
panel.IsVisible = true;
|
||||
Grid.SetColumn(panel, 2 * i);
|
||||
var separator = i switch
|
||||
{
|
||||
0 => _firstSeparator,
|
||||
1 => _secondSeparator,
|
||||
2 => _thirdSeparator,
|
||||
_ => null
|
||||
};
|
||||
if (i != panels.Count - 1) IsVisibleProperty.SetValue(true, separator);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_hourSelector is not null) _hourSelector.SelectionChanged -= OnPanelSelectionChanged;
|
||||
if (_minuteSelector is not null) _minuteSelector.SelectionChanged -= OnPanelSelectionChanged;
|
||||
if (_secondSelector is not null) _secondSelector.SelectionChanged -= OnPanelSelectionChanged;
|
||||
if (_ampmSelector is not null) _ampmSelector.SelectionChanged -= OnPanelSelectionChanged;
|
||||
_hourSelector = e.NameScope.Find<DateTimePickerPanel>(PART_HourSelector);
|
||||
_minuteSelector = e.NameScope.Find<DateTimePickerPanel>(PART_MinuteSelector);
|
||||
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(PART_SecondSelector);
|
||||
_ampmSelector = e.NameScope.Find<DateTimePickerPanel>(PART_AmPmSelector);
|
||||
if (_hourSelector is not null) _hourSelector.SelectionChanged += OnPanelSelectionChanged;
|
||||
if (_minuteSelector is not null) _minuteSelector.SelectionChanged += OnPanelSelectionChanged;
|
||||
if (_secondSelector is not null) _secondSelector.SelectionChanged += OnPanelSelectionChanged;
|
||||
if (_ampmSelector is not null) _ampmSelector.SelectionChanged += OnPanelSelectionChanged;
|
||||
_pickerContainer = e.NameScope.Find<Grid>(PART_PickerContainer);
|
||||
_hourScrollPanel = e.NameScope.Find<Control>(PART_HourScrollPanel);
|
||||
_minuteScrollPanel = e.NameScope.Find<Control>(PART_MinuteScrollPanel);
|
||||
_secondScrollPanel = e.NameScope.Find<Control>(PART_SecondScrollPanel);
|
||||
_ampmScrollPanel = e.NameScope.Find<Control>(PART_AmPmScrollPanel);
|
||||
_firstSeparator = e.NameScope.Find<Control>(PART_FirstSeparator);
|
||||
_secondSeparator = e.NameScope.Find<Control>(PART_SecondSeparator);
|
||||
_thirdSeparator = e.NameScope.Find<Control>(PART_ThirdSeparator);
|
||||
Initialize();
|
||||
UpdatePanelLayout(PanelFormat);
|
||||
UpdatePanelsFromSelectedTime(Time);
|
||||
}
|
||||
|
||||
private void OnPanelSelectionChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
if (_updateFromTimeChange) return;
|
||||
if (!_use12Clock && sender == _ampmSelector) return;
|
||||
var time = NeedsConfirmation ? _timeHolder : Time ?? DateTime.Now.TimeOfDay;
|
||||
var hour = _hourSelector?.SelectedValue ?? time.Hours;
|
||||
var minute = _minuteSelector?.SelectedValue ?? time.Minutes;
|
||||
var second = _secondSelector?.SelectedValue ?? time.Seconds;
|
||||
var ampm = _ampmSelector?.SelectedValue ?? (time.Hours >= 12 ? 1 : 0);
|
||||
if (_use12Clock)
|
||||
hour = ampm switch
|
||||
{
|
||||
0 when hour == 12 => 0,
|
||||
1 when hour < 12 => hour + 12,
|
||||
_ => hour
|
||||
};
|
||||
else
|
||||
{
|
||||
ampm = hour switch
|
||||
{
|
||||
>= 12 => 1,
|
||||
_ => 0
|
||||
};
|
||||
SetIfChanged(_ampmSelector, ampm);
|
||||
}
|
||||
var newTime = new TimeSpan(hour, minute, second);
|
||||
if (NeedsConfirmation)
|
||||
_timeHolder = newTime;
|
||||
else
|
||||
SetCurrentValue(TimeProperty, newTime);
|
||||
}
|
||||
|
||||
private void UpdatePanelsFromSelectedTime(TimeSpan? time)
|
||||
{
|
||||
if (time is null) return;
|
||||
if (_hourSelector is not null)
|
||||
{
|
||||
var index = _use12Clock ? time.Value.Hours % 12 : time.Value.Hours;
|
||||
if (_use12Clock && index == 0) index = 12;
|
||||
SetIfChanged(_hourSelector, index);
|
||||
}
|
||||
SetIfChanged(_minuteSelector, time.Value.Minutes);
|
||||
SetIfChanged(_secondSelector, time.Value.Seconds);
|
||||
var ampm = time.Value.Hours switch
|
||||
{
|
||||
>= 12 => 1,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
SetIfChanged(_ampmSelector, ampm);
|
||||
if (_ampmSelector is not null)
|
||||
{
|
||||
_ampmSelector.IsEnabled = _use12Clock;
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (_pickerContainer is null) return;
|
||||
if (_hourSelector is not null)
|
||||
{
|
||||
_hourSelector.ItemFormat = "hh";
|
||||
_hourSelector.MaximumValue = _use12Clock ? 12 : 23;
|
||||
_hourSelector.MinimumValue = _use12Clock ? 1 : 0;
|
||||
}
|
||||
|
||||
if (_minuteSelector is not null)
|
||||
{
|
||||
_minuteSelector.ItemFormat = "mm";
|
||||
_minuteSelector.MaximumValue = 59;
|
||||
_minuteSelector.MinimumValue = 0;
|
||||
}
|
||||
|
||||
if (_secondSelector is not null)
|
||||
{
|
||||
_secondSelector.ItemFormat = "mm";
|
||||
_secondSelector.MaximumValue = 59;
|
||||
_secondSelector.MinimumValue = 0;
|
||||
}
|
||||
|
||||
if (_ampmSelector is not null)
|
||||
{
|
||||
_ampmSelector.ItemFormat = "t";
|
||||
_ampmSelector.MaximumValue = 1;
|
||||
_ampmSelector.MinimumValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Confirm()
|
||||
{
|
||||
if (NeedsConfirmation) SetCurrentValue(TimeProperty, _timeHolder);
|
||||
}
|
||||
|
||||
private void SetIfChanged(DateTimePickerPanel? panel, int index)
|
||||
{
|
||||
if (panel is null) return;
|
||||
if (panel.SelectedValue != index) panel.SelectedValue = index;
|
||||
}
|
||||
}
|
||||
184
src/Ursa/Controls/DateTimePicker/TimeRangePicker.cs
Normal file
184
src/Ursa/Controls/DateTimePicker/TimeRangePicker.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Common;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_StartTextBox, typeof(TextBox))]
|
||||
[TemplatePart(PART_EndTextBox, typeof(TextBox))]
|
||||
[TemplatePart(PartNames.PART_Popup, typeof(Popup))]
|
||||
[TemplatePart(PART_StartPresenter, typeof(TimePickerPresenter))]
|
||||
[TemplatePart(PART_EndPresenter, typeof(TimePickerPresenter))]
|
||||
[TemplatePart(PART_Button, typeof(Button))]
|
||||
public class TimeRangePicker : TimePickerBase, IClearControl
|
||||
{
|
||||
public const string PART_StartTextBox = "PART_StartTextBox";
|
||||
public const string PART_EndTextBox = "PART_EndTextBox";
|
||||
public const string PART_StartPresenter = "PART_StartPresenter";
|
||||
public const string PART_EndPresenter = "PART_EndPresenter";
|
||||
public const string PART_Button = "PART_Button";
|
||||
|
||||
|
||||
public static readonly StyledProperty<TimeSpan?> StartTimeProperty =
|
||||
AvaloniaProperty.Register<TimeRangePicker, TimeSpan?>(
|
||||
nameof(StartTime), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<TimeSpan?> EndTimeProperty =
|
||||
AvaloniaProperty.Register<TimeRangePicker, TimeSpan?>(
|
||||
nameof(EndTime), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<string?> StartWatermarkProperty =
|
||||
AvaloniaProperty.Register<TimeRangePicker, string?>(
|
||||
nameof(StartWatermark));
|
||||
|
||||
public static readonly StyledProperty<string?> EndWatermarkProperty =
|
||||
AvaloniaProperty.Register<TimeRangePicker, string?>(
|
||||
nameof(EndWatermark));
|
||||
|
||||
private Button? _button;
|
||||
private TimePickerPresenter? _endPresenter;
|
||||
private TextBox? _endTextBox;
|
||||
private Popup? _popup;
|
||||
private TimePickerPresenter? _startPresenter;
|
||||
|
||||
private TextBox? _startTextBox;
|
||||
|
||||
|
||||
static TimeRangePicker()
|
||||
{
|
||||
StartTimeProperty.Changed.AddClassHandler<TimeRangePicker, TimeSpan?>((picker, args) =>
|
||||
picker.OnSelectionChanged(args));
|
||||
EndTimeProperty.Changed.AddClassHandler<TimeRangePicker, TimeSpan?>((picker, args) =>
|
||||
picker.OnSelectionChanged(args, false));
|
||||
}
|
||||
|
||||
public string? EndWatermark
|
||||
{
|
||||
get => GetValue(EndWatermarkProperty);
|
||||
set => SetValue(EndWatermarkProperty, value);
|
||||
}
|
||||
|
||||
public string? StartWatermark
|
||||
{
|
||||
get => GetValue(StartWatermarkProperty);
|
||||
set => SetValue(StartWatermarkProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan? StartTime
|
||||
{
|
||||
get => GetValue(StartTimeProperty);
|
||||
set => SetValue(StartTimeProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan? EndTime
|
||||
{
|
||||
get => GetValue(EndTimeProperty);
|
||||
set => SetValue(EndTimeProperty, value);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Focus(NavigationMethod.Pointer);
|
||||
_startPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
|
||||
_endPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args, bool start = true)
|
||||
{
|
||||
var textBox = start ? _startTextBox : _endTextBox;
|
||||
if (textBox is null) return;
|
||||
var time = args.NewValue.Value;
|
||||
if (time is null)
|
||||
{
|
||||
textBox.Text = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var date = new DateTime(1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds);
|
||||
var text = date.ToString(DisplayFormat);
|
||||
textBox.Text = text;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _startTextBox, _endTextBox);
|
||||
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _startTextBox, _endTextBox);
|
||||
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
|
||||
|
||||
_popup = e.NameScope.Find<Popup>(PartNames.PART_Popup);
|
||||
_startTextBox = e.NameScope.Find<TextBox>(PART_StartTextBox);
|
||||
_endTextBox = e.NameScope.Find<TextBox>(PART_EndTextBox);
|
||||
_startPresenter = e.NameScope.Find<TimePickerPresenter>(PART_StartPresenter);
|
||||
_endPresenter = e.NameScope.Find<TimePickerPresenter>(PART_EndPresenter);
|
||||
_button = e.NameScope.Find<Button>(PART_Button);
|
||||
|
||||
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _startTextBox, _endTextBox);
|
||||
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _startTextBox,
|
||||
_endTextBox);
|
||||
Button.ClickEvent.AddHandler(OnButtonClick, _button);
|
||||
}
|
||||
|
||||
private void OnButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Focus(NavigationMethod.Pointer);
|
||||
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
|
||||
}
|
||||
|
||||
private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, true);
|
||||
}
|
||||
|
||||
private void OnTextBoxGetFocus(object sender, GotFocusEventArgs e)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, true);
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Down)
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, true);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Tab)
|
||||
{
|
||||
if (e.Source == _endTextBox) SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
public void Confirm()
|
||||
{
|
||||
_startPresenter?.Confirm();
|
||||
_endPresenter?.Confirm();
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
Focus();
|
||||
}
|
||||
|
||||
public void Dismiss()
|
||||
{
|
||||
SetCurrentValue(IsDropdownOpenProperty, false);
|
||||
Focus();
|
||||
}
|
||||
}
|
||||
29
src/Ursa/Controls/DateTimePicker/UrsaDateTimeScrollPanel.cs
Normal file
29
src/Ursa/Controls/DateTimePicker/UrsaDateTimeScrollPanel.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class UrsaDateTimeScrollPanel : DateTimePickerPanel
|
||||
{
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var size = base.MeasureOverride(availableSize);
|
||||
var width = this.Children.Select(a=>a.DesiredSize.Width).Max();
|
||||
width = Math.Max(width, this.MinWidth);
|
||||
return new Size(width, size.Height);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var width = this.Children.Select(a=>a.DesiredSize.Width).Max();
|
||||
width = Math.Max(width, this.MinWidth);
|
||||
finalSize = new Size(width, finalSize.Height);
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
}
|
||||
40
src/Ursa/Controls/Dialog/CustomDialogControl.cs
Normal file
40
src/Ursa/Controls/Dialog/CustomDialogControl.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.GestureRecognizers;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Ursa.Common;
|
||||
using Ursa.Controls.OverlayShared;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class CustomDialogControl: DialogControlBase
|
||||
{
|
||||
internal bool IsCloseButtonVisible { get; set; }
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_closeButton is not null)
|
||||
{
|
||||
_closeButton.IsVisible = IsCloseButtonVisible;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnElementClosing(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/Ursa/Controls/Dialog/DefaultDialogControl.cs
Normal file
141
src/Ursa/Controls/Dialog/DefaultDialogControl.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_OKButton, typeof(Button))]
|
||||
[TemplatePart(PART_CancelButton, typeof(Button))]
|
||||
[TemplatePart(PART_YesButton, typeof(Button))]
|
||||
[TemplatePart(PART_NoButton, typeof(Button))]
|
||||
public class DefaultDialogControl: DialogControlBase
|
||||
{
|
||||
public const string PART_OKButton = "PART_OKButton";
|
||||
public const string PART_CancelButton = "PART_CancelButton";
|
||||
public const string PART_YesButton = "PART_YesButton";
|
||||
public const string PART_NoButton = "PART_NoButton";
|
||||
|
||||
private Button? _okButton;
|
||||
private Button? _cancelButton;
|
||||
private Button? _yesButton;
|
||||
private Button? _noButton;
|
||||
|
||||
public static readonly StyledProperty<string?> TitleProperty = AvaloniaProperty.Register<DefaultDialogControl, string?>(
|
||||
nameof(Title));
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<DialogButton> ButtonsProperty = AvaloniaProperty.Register<DefaultDialogControl, DialogButton>(
|
||||
nameof(Buttons));
|
||||
|
||||
public DialogButton Buttons
|
||||
{
|
||||
get => GetValue(ButtonsProperty);
|
||||
set => SetValue(ButtonsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<DialogMode> ModeProperty = AvaloniaProperty.Register<DefaultDialogControl, DialogMode>(
|
||||
nameof(Mode));
|
||||
|
||||
public DialogMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(DefaultButtonsClose, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
_okButton = e.NameScope.Find<Button>(PART_OKButton);
|
||||
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
|
||||
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
|
||||
_noButton = e.NameScope.Find<Button>(PART_NoButton);
|
||||
Button.ClickEvent.AddHandler(DefaultButtonsClose, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
SetButtonVisibility();
|
||||
}
|
||||
|
||||
|
||||
private void SetButtonVisibility()
|
||||
{
|
||||
bool isCloseButtonVisible = DataContext is IDialogContext || Buttons != DialogButton.YesNo;
|
||||
Button.IsVisibleProperty.SetValue(isCloseButtonVisible, _closeButton);
|
||||
switch (Buttons)
|
||||
{
|
||||
case DialogButton.None:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.OK:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.OKCancel:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.YesNo:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.YesNoCancel:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DefaultButtonsClose(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button == _okButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.OK);
|
||||
}
|
||||
else if (button == _cancelButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.Cancel);
|
||||
}
|
||||
else if (button == _yesButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.Yes);
|
||||
}
|
||||
else if (button == _noButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.No);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogResult result = Buttons switch
|
||||
{
|
||||
DialogButton.None => DialogResult.None,
|
||||
DialogButton.OK => DialogResult.OK,
|
||||
DialogButton.OKCancel => DialogResult.Cancel,
|
||||
DialogButton.YesNo => DialogResult.No,
|
||||
DialogButton.YesNoCancel => DialogResult.Cancel,
|
||||
_ => DialogResult.None
|
||||
};
|
||||
OnElementClosing(this, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
src/Ursa/Controls/Dialog/DefaultDialogWindow.cs
Normal file
148
src/Ursa/Controls/Dialog/DefaultDialogWindow.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_YesButton, typeof(Button))]
|
||||
[TemplatePart(PART_NoButton, typeof(Button))]
|
||||
[TemplatePart(PART_OKButton, typeof(Button))]
|
||||
[TemplatePart(PART_CancelButton, typeof(Button))]
|
||||
public class DefaultDialogWindow: DialogWindow
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(DefaultDialogWindow);
|
||||
|
||||
public const string PART_YesButton = "PART_YesButton";
|
||||
public const string PART_NoButton = "PART_NoButton";
|
||||
public const string PART_OKButton = "PART_OKButton";
|
||||
public const string PART_CancelButton = "PART_CancelButton";
|
||||
|
||||
private Button? _yesButton;
|
||||
private Button? _noButton;
|
||||
private Button? _okButton;
|
||||
private Button? _cancelButton;
|
||||
|
||||
public static readonly StyledProperty<DialogButton> ButtonsProperty = AvaloniaProperty.Register<DefaultDialogWindow, DialogButton>(
|
||||
nameof(Buttons));
|
||||
|
||||
public DialogButton Buttons
|
||||
{
|
||||
get => GetValue(ButtonsProperty);
|
||||
set => SetValue(ButtonsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<DialogMode> ModeProperty = AvaloniaProperty.Register<DefaultDialogWindow, DialogMode>(
|
||||
nameof(Mode));
|
||||
|
||||
public DialogMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(OnDefaultClose, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
_okButton = e.NameScope.Find<Button>(PART_OKButton);
|
||||
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
|
||||
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
|
||||
_noButton = e.NameScope.Find<Button>(PART_NoButton);
|
||||
Button.ClickEvent.AddHandler(OnDefaultClose, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
SetButtonVisibility();
|
||||
}
|
||||
|
||||
private void OnDefaultClose(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender == _yesButton)
|
||||
{
|
||||
Close(DialogResult.Yes);
|
||||
return;
|
||||
}
|
||||
if(sender == _noButton)
|
||||
{
|
||||
Close(DialogResult.No);
|
||||
return;
|
||||
}
|
||||
if(sender == _okButton)
|
||||
{
|
||||
Close(DialogResult.OK);
|
||||
return;
|
||||
}
|
||||
if(sender == _cancelButton)
|
||||
{
|
||||
Close(DialogResult.Cancel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetButtonVisibility()
|
||||
{
|
||||
bool closeButtonVisible = DataContext is IDialogContext || Buttons != DialogButton.YesNo;
|
||||
SetVisibility(_closeButton, closeButtonVisible);
|
||||
switch (Buttons)
|
||||
{
|
||||
case DialogButton.None:
|
||||
SetVisibility(_okButton, false);
|
||||
SetVisibility(_cancelButton, false);
|
||||
SetVisibility(_yesButton, false);
|
||||
SetVisibility(_noButton, false);
|
||||
break;
|
||||
case DialogButton.OK:
|
||||
SetVisibility(_okButton, true);
|
||||
SetVisibility(_cancelButton, false);
|
||||
SetVisibility(_yesButton, false);
|
||||
SetVisibility(_noButton, false);
|
||||
break;
|
||||
case DialogButton.OKCancel:
|
||||
SetVisibility(_okButton, true);
|
||||
SetVisibility(_cancelButton, true);
|
||||
SetVisibility(_yesButton, false);
|
||||
SetVisibility(_noButton, false);
|
||||
break;
|
||||
case DialogButton.YesNo:
|
||||
SetVisibility(_okButton, false);
|
||||
SetVisibility(_cancelButton, false);
|
||||
SetVisibility(_yesButton, true);
|
||||
SetVisibility(_noButton, true);
|
||||
break;
|
||||
case DialogButton.YesNoCancel:
|
||||
SetVisibility(_okButton, false);
|
||||
SetVisibility(_cancelButton, true);
|
||||
SetVisibility(_yesButton, true);
|
||||
SetVisibility(_noButton, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetVisibility(Button? button, bool visible)
|
||||
{
|
||||
if (button is not null) button.IsVisible = visible;
|
||||
}
|
||||
|
||||
protected internal override void OnCloseButtonClicked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogResult result = Buttons switch
|
||||
{
|
||||
DialogButton.None => DialogResult.None,
|
||||
DialogButton.OK => DialogResult.OK,
|
||||
DialogButton.OKCancel => DialogResult.Cancel,
|
||||
DialogButton.YesNo => DialogResult.No,
|
||||
DialogButton.YesNoCancel => DialogResult.Cancel,
|
||||
_ => DialogResult.None
|
||||
};
|
||||
Close(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/Ursa/Controls/Dialog/Dialog.cs
Normal file
237
src/Ursa/Controls/Dialog/Dialog.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public static class Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Show a Window Dialog that with all content fully customized. And the owner of the dialog is specified.
|
||||
/// </summary>
|
||||
/// <param name="vm">Dialog ViewModel instance</param>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <typeparam name="TView"></typeparam>
|
||||
/// <typeparam name="TViewModel"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static void ShowCustom<TView, TViewModel>(TViewModel? vm, Window? owner = null, DialogOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var window = new DialogWindow
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDialogWindow(window, options);
|
||||
owner ??= GetMainWindow();
|
||||
if (owner is null)
|
||||
{
|
||||
window.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.Show(owner);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a Window Dialog that with all content fully customized. And the owner of the dialog is specified.
|
||||
/// </summary>
|
||||
/// <param name="view">View to show in Dialog Window</param>
|
||||
/// <param name="vm">ViewModel</param>
|
||||
/// <param name="owner">Owner Window</param>
|
||||
/// <param name="options">Dialog options to configure the window. </param>
|
||||
public static void ShowCustom(Control view, object? vm, Window? owner = null, DialogOptions? options = null)
|
||||
{
|
||||
var window = new DialogWindow
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDialogWindow(window, options);
|
||||
owner ??= GetMainWindow();
|
||||
if (owner is null)
|
||||
{
|
||||
window.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.Show(owner);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a Modal Dialog Window with default style.
|
||||
/// </summary>
|
||||
/// <param name="vm"></param>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <typeparam name="TView"></typeparam>
|
||||
/// <typeparam name="TViewModel"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static Task<DialogResult> ShowModal<TView, TViewModel>(TViewModel vm, Window? owner = null,
|
||||
DialogOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var window = new DefaultDialogWindow
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogWindow(window, options);
|
||||
owner ??= GetMainWindow();
|
||||
if (owner is null)
|
||||
{
|
||||
window.Show();
|
||||
return Task.FromResult(DialogResult.None);
|
||||
}
|
||||
return window.ShowDialog<DialogResult>(owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a Modal Dialog Window with default style.
|
||||
/// </summary>
|
||||
/// <param name="view"></param>
|
||||
/// <param name="vm"></param>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
public static Task<DialogResult> ShowModal(Control view, object? vm, Window? owner = null, DialogOptions? options = null)
|
||||
{
|
||||
var window = new DefaultDialogWindow
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogWindow(window, options);
|
||||
owner ??= GetMainWindow();
|
||||
if (owner is null)
|
||||
{
|
||||
window.Show();
|
||||
return Task.FromResult(DialogResult.None);
|
||||
}
|
||||
return window.ShowDialog<DialogResult>(owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a Modal Dialog Window with all content fully customized.
|
||||
/// </summary>
|
||||
/// <param name="vm"></param>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <typeparam name="TView"></typeparam>
|
||||
/// <typeparam name="TViewModel"></typeparam>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static Task<TResult?> ShowCustomModal<TView, TViewModel, TResult>(TViewModel vm, Window? owner = null,
|
||||
DialogOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var window = new DialogWindow
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDialogWindow(window, options);
|
||||
owner ??= GetMainWindow();
|
||||
if (owner is null)
|
||||
{
|
||||
window.Show();
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
return window.ShowDialog<TResult?>(owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a Modal Dialog Window with all content fully customized.
|
||||
/// </summary>
|
||||
/// <param name="view"></param>
|
||||
/// <param name="vm"></param>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static Task<TResult?> ShowCustomModal<TResult>(Control view, object? vm, Window? owner = null,
|
||||
DialogOptions? options = null)
|
||||
{
|
||||
var window = new DialogWindow
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDialogWindow(window, options);
|
||||
owner ??= GetMainWindow();
|
||||
if (owner is null)
|
||||
{
|
||||
window.Show();
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
return window.ShowDialog<TResult?>(owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the main window of the application as default owner of the dialog.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static Window? GetMainWindow()
|
||||
{
|
||||
var lifetime = Application.Current?.ApplicationLifetime;
|
||||
return lifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } w } ? w : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach options to dialog window.
|
||||
/// </summary>
|
||||
/// <param name="window"></param>
|
||||
/// <param name="options"></param>
|
||||
private static void ConfigureDialogWindow(DialogWindow window, DialogOptions? options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
options = new DialogOptions();
|
||||
}
|
||||
window.WindowStartupLocation = options.StartupLocation;
|
||||
window.Title = options.Title;
|
||||
window.IsCloseButtonVisible = options.IsCloseButtonVisible;
|
||||
if (options.StartupLocation == WindowStartupLocation.Manual)
|
||||
{
|
||||
if (options.Position is not null)
|
||||
{
|
||||
window.Position = options.Position.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach options to default dialog window.
|
||||
/// </summary>
|
||||
/// <param name="window"></param>
|
||||
/// <param name="options"></param>
|
||||
private static void ConfigureDefaultDialogWindow(DefaultDialogWindow window, DialogOptions? options)
|
||||
{
|
||||
options ??= DialogOptions.Default;
|
||||
window.WindowStartupLocation = options.StartupLocation;
|
||||
window.Title = options.Title;
|
||||
window.Buttons = options.Button;
|
||||
window.Mode = options.Mode;
|
||||
if (options.StartupLocation == WindowStartupLocation.Manual)
|
||||
{
|
||||
if (options.Position is not null)
|
||||
{
|
||||
window.Position = options.Position.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
src/Ursa/Controls/Dialog/DialogControlBase.cs
Normal file
199
src/Ursa/Controls/Dialog/DialogControlBase.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
using Ursa.Controls.OverlayShared;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_CloseButton, typeof(Button))]
|
||||
[TemplatePart(PART_TitleArea, typeof(Panel))]
|
||||
[PseudoClasses(PC_Modal, PC_FullScreen)]
|
||||
public abstract class DialogControlBase : OverlayFeedbackElement
|
||||
{
|
||||
public const string PART_CloseButton = "PART_CloseButton";
|
||||
public const string PART_TitleArea = "PART_TitleArea";
|
||||
public const string PC_Modal = ":modal";
|
||||
public const string PC_FullScreen = ":full-screen";
|
||||
|
||||
internal HorizontalPosition HorizontalAnchor { get; set; } = HorizontalPosition.Center;
|
||||
internal VerticalPosition VerticalAnchor { get; set; } = VerticalPosition.Center;
|
||||
internal HorizontalPosition ActualHorizontalAnchor { get; set; }
|
||||
internal VerticalPosition ActualVerticalAnchor { get; set; }
|
||||
internal double? HorizontalOffset { get; set; }
|
||||
internal double? VerticalOffset { get; set; }
|
||||
internal double? HorizontalOffsetRatio { get; set; }
|
||||
internal double? VerticalOffsetRatio { get; set; }
|
||||
internal bool CanLightDismiss { get; set; }
|
||||
|
||||
private bool _isFullScreen;
|
||||
|
||||
public static readonly DirectProperty<DialogControlBase, bool> IsFullScreenProperty = AvaloniaProperty.RegisterDirect<DialogControlBase, bool>(
|
||||
nameof(IsFullScreen), o => o.IsFullScreen, (o, v) => o.IsFullScreen = v);
|
||||
|
||||
public bool IsFullScreen
|
||||
{
|
||||
get => _isFullScreen;
|
||||
set => SetAndRaise(IsFullScreenProperty, ref _isFullScreen, value);
|
||||
}
|
||||
|
||||
protected internal Button? _closeButton;
|
||||
private Panel? _titleArea;
|
||||
|
||||
#region Layer Management
|
||||
|
||||
public static readonly RoutedEvent<DialogLayerChangeEventArgs> LayerChangedEvent =
|
||||
RoutedEvent.Register<CustomDialogControl, DialogLayerChangeEventArgs>(
|
||||
nameof(LayerChanged), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<DialogLayerChangeEventArgs> LayerChanged
|
||||
{
|
||||
add => AddHandler(LayerChangedEvent, value);
|
||||
remove => RemoveHandler(LayerChangedEvent, value);
|
||||
}
|
||||
|
||||
public void UpdateLayer(object? o)
|
||||
{
|
||||
if (o is DialogLayerChangeType t)
|
||||
{
|
||||
RaiseEvent(new DialogLayerChangeEventArgs(LayerChangedEvent, t));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DragMove AttachedPropert
|
||||
|
||||
public static readonly AttachedProperty<bool> CanDragMoveProperty =
|
||||
AvaloniaProperty.RegisterAttached<DialogControlBase, InputElement, bool>("CanDragMove");
|
||||
|
||||
public static void SetCanDragMove(InputElement obj, bool value) => obj.SetValue(CanDragMoveProperty, value);
|
||||
public static bool GetCanDragMove(InputElement obj) => obj.GetValue(CanDragMoveProperty);
|
||||
|
||||
private static void OnCanDragMoveChanged(InputElement arg1, AvaloniaPropertyChangedEventArgs<bool> arg2)
|
||||
{
|
||||
if (arg2.NewValue.Value)
|
||||
{
|
||||
arg1.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Bubble);
|
||||
arg1.AddHandler(PointerMovedEvent, OnPointerMoved, RoutingStrategies.Bubble);
|
||||
arg1.AddHandler(PointerReleasedEvent, OnPointerReleased, RoutingStrategies.Bubble);
|
||||
}
|
||||
else
|
||||
{
|
||||
arg1.RemoveHandler(PointerPressedEvent, OnPointerPressed);
|
||||
arg1.RemoveHandler(PointerMovedEvent, OnPointerMoved);
|
||||
arg1.RemoveHandler(PointerReleasedEvent, OnPointerReleased);
|
||||
}
|
||||
|
||||
void OnPointerPressed(InputElement sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender.FindLogicalAncestorOfType<DialogControlBase>() is { } dialog)
|
||||
{
|
||||
e.Source = dialog;
|
||||
}
|
||||
}
|
||||
|
||||
void OnPointerMoved(InputElement sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender.FindLogicalAncestorOfType<DialogControlBase>() is { } dialog)
|
||||
{
|
||||
e.Source = dialog;
|
||||
}
|
||||
}
|
||||
|
||||
void OnPointerReleased(InputElement sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (sender.FindLogicalAncestorOfType<DialogControlBase>() is { } dialog)
|
||||
{
|
||||
e.Source = dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Close AttachedProperty
|
||||
|
||||
public static readonly AttachedProperty<bool> CanCloseProperty =
|
||||
AvaloniaProperty.RegisterAttached<DialogControlBase, InputElement, bool>("CanClose");
|
||||
|
||||
public static void SetCanClose(InputElement obj, bool value) => obj.SetValue(CanCloseProperty, value);
|
||||
public static bool GetCanClose(InputElement obj) => obj.GetValue(CanCloseProperty);
|
||||
private static void OnCanCloseChanged(InputElement arg1, AvaloniaPropertyChangedEventArgs<bool> arg2)
|
||||
{
|
||||
if (arg2.NewValue.Value)
|
||||
{
|
||||
arg1.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Bubble);
|
||||
}
|
||||
void OnPointerPressed(InputElement sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender.FindLogicalAncestorOfType<DialogControlBase>() is { } dialog)
|
||||
{
|
||||
dialog.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
static DialogControlBase()
|
||||
{
|
||||
CanDragMoveProperty.Changed.AddClassHandler<InputElement, bool>(OnCanDragMoveChanged);
|
||||
CanCloseProperty.Changed.AddClassHandler<InputElement, bool>(OnCanCloseChanged);
|
||||
IsFullScreenProperty.AffectsPseudoClass<DialogControlBase>(PC_FullScreen);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_titleArea = e.NameScope.Find<Panel>(PART_TitleArea);
|
||||
if (GetCanDragMove(this))
|
||||
{
|
||||
_titleArea?.RemoveHandler(PointerMovedEvent, OnTitlePointerMove);
|
||||
_titleArea?.RemoveHandler(PointerPressedEvent, OnTitlePointerPressed);
|
||||
_titleArea?.RemoveHandler(PointerReleasedEvent, OnTitlePointerRelease);
|
||||
|
||||
_titleArea?.AddHandler(PointerMovedEvent, OnTitlePointerMove, RoutingStrategies.Bubble);
|
||||
_titleArea?.AddHandler(PointerPressedEvent, OnTitlePointerPressed, RoutingStrategies.Bubble);
|
||||
_titleArea?.AddHandler(PointerReleasedEvent, OnTitlePointerRelease, RoutingStrategies.Bubble);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_titleArea is not null) _titleArea.IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
Button.ClickEvent.RemoveHandler(OnCloseButtonClick, _closeButton);
|
||||
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
|
||||
Button.ClickEvent.AddHandler(OnCloseButtonClick, _closeButton);
|
||||
}
|
||||
|
||||
private void OnTitlePointerPressed(InputElement sender, PointerPressedEventArgs e)
|
||||
{
|
||||
e.Source = this;
|
||||
}
|
||||
|
||||
private void OnTitlePointerMove(InputElement sender, PointerEventArgs e)
|
||||
{
|
||||
e.Source = this;
|
||||
}
|
||||
|
||||
private void OnTitlePointerRelease(InputElement sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
e.Source = this;
|
||||
}
|
||||
|
||||
private void OnCloseButtonClick(object sender, RoutedEventArgs args) => Close();
|
||||
|
||||
internal void SetAsModal(bool modal)
|
||||
{
|
||||
PseudoClasses.Set(PC_Modal, modal);
|
||||
}
|
||||
}
|
||||
25
src/Ursa/Controls/Dialog/DialogLayerChangeEventArgs.cs
Normal file
25
src/Ursa/Controls/Dialog/DialogLayerChangeEventArgs.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class DialogLayerChangeEventArgs: RoutedEventArgs
|
||||
{
|
||||
public DialogLayerChangeType ChangeType { get; }
|
||||
|
||||
public DialogLayerChangeEventArgs(DialogLayerChangeType type)
|
||||
{
|
||||
ChangeType = type;
|
||||
}
|
||||
public DialogLayerChangeEventArgs(RoutedEvent routedEvent, DialogLayerChangeType type): base(routedEvent)
|
||||
{
|
||||
ChangeType = type;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DialogLayerChangeType
|
||||
{
|
||||
BringForward,
|
||||
SendBackward,
|
||||
BringToFront,
|
||||
SendToBack
|
||||
}
|
||||
78
src/Ursa/Controls/Dialog/DialogWindow.cs
Normal file
78
src/Ursa/Controls/Dialog/DialogWindow.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_CloseButton, typeof(Button))]
|
||||
[TemplatePart(PART_TitleArea, typeof(Panel))]
|
||||
public class DialogWindow: Window
|
||||
{
|
||||
public const string PART_CloseButton = "PART_CloseButton";
|
||||
public const string PART_TitleArea = "PART_TitleArea";
|
||||
protected override Type StyleKeyOverride { get; } = typeof(DialogWindow);
|
||||
|
||||
protected internal Button? _closeButton;
|
||||
private Panel? _titleArea;
|
||||
|
||||
internal bool IsCloseButtonVisible { get; set; }
|
||||
|
||||
static DialogWindow()
|
||||
{
|
||||
DataContextProperty.Changed.AddClassHandler<DialogWindow, object?>((o, e) => o.OnDataContextChange(e));
|
||||
}
|
||||
|
||||
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
if (args.OldValue.Value is IDialogContext oldContext)
|
||||
{
|
||||
oldContext.RequestClose-= OnContextRequestClose;
|
||||
}
|
||||
|
||||
if (args.NewValue.Value is IDialogContext newContext)
|
||||
{
|
||||
newContext.RequestClose += OnContextRequestClose;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(OnCloseButtonClicked, _closeButton);
|
||||
_titleArea?.RemoveHandler(PointerPressedEvent, OnTitlePointerPressed);
|
||||
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
|
||||
Button.IsVisibleProperty.SetValue(IsCloseButtonVisible, _closeButton);
|
||||
Button.ClickEvent.AddHandler(OnCloseButtonClicked, _closeButton);
|
||||
_titleArea = e.NameScope.Find<Panel>(PART_TitleArea);
|
||||
_titleArea?.AddHandler(PointerPressedEvent, OnTitlePointerPressed, RoutingStrategies.Bubble);
|
||||
|
||||
}
|
||||
|
||||
private void OnContextRequestClose(object? sender, object? args)
|
||||
{
|
||||
Close(args);
|
||||
}
|
||||
|
||||
protected internal virtual void OnCloseButtonClicked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Close(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTitlePointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
this.BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
29
src/Ursa/Controls/Dialog/Options/DialogOptions.cs
Normal file
29
src/Ursa/Controls/Dialog/Options/DialogOptions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class DialogOptions
|
||||
{
|
||||
internal static DialogOptions Default { get; } = new DialogOptions();
|
||||
/// <summary>
|
||||
/// The Startup Location of DialogWindow. Default is <see cref="WindowStartupLocation.CenterOwner"/>
|
||||
/// </summary>
|
||||
public WindowStartupLocation StartupLocation { get; set; } = WindowStartupLocation.CenterOwner;
|
||||
/// <summary>
|
||||
/// The Position of DialogWindow startup location if <see cref="StartupLocation"/> is <see cref="WindowStartupLocation.Manual"/>
|
||||
/// </summary>
|
||||
public PixelPoint? Position { get; set; }
|
||||
/// <summary>
|
||||
/// Title of DialogWindow, Default is null
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
/// <summary>
|
||||
/// DialogWindow's Mode, Default is <see cref="DialogMode.None"/>
|
||||
/// </summary>
|
||||
public DialogMode Mode { get; set; } = DialogMode.None;
|
||||
|
||||
public DialogButton Button { get; set; } = DialogButton.OKCancel;
|
||||
|
||||
public bool IsCloseButtonVisible { get; set; } = true;
|
||||
}
|
||||
49
src/Ursa/Controls/Dialog/Options/OverlayDialogOptions.cs
Normal file
49
src/Ursa/Controls/Dialog/Options/OverlayDialogOptions.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum HorizontalPosition
|
||||
{
|
||||
Left,
|
||||
Center,
|
||||
Right
|
||||
}
|
||||
|
||||
public enum VerticalPosition
|
||||
{
|
||||
Top,
|
||||
Center,
|
||||
Bottom
|
||||
}
|
||||
|
||||
public class OverlayDialogOptions
|
||||
{
|
||||
internal static OverlayDialogOptions Default { get; } = new OverlayDialogOptions();
|
||||
public bool FullScreen { get; set; }
|
||||
public HorizontalPosition HorizontalAnchor { get; set; } = HorizontalPosition.Center;
|
||||
public VerticalPosition VerticalAnchor { get; set; } = VerticalPosition.Center;
|
||||
/// <summary>
|
||||
/// This attribute is only used when HorizontalAnchor is not Center
|
||||
/// </summary>
|
||||
public double? HorizontalOffset { get; set; } = null;
|
||||
/// <summary>
|
||||
/// This attribute is only used when VerticalAnchor is not Center
|
||||
/// </summary>
|
||||
public double? VerticalOffset { get; set; } = null;
|
||||
/// <summary>
|
||||
/// Only works for DefaultDialogControl
|
||||
/// </summary>
|
||||
public DialogMode Mode { get; set; } = DialogMode.None;
|
||||
/// <summary>
|
||||
/// Only works for DefaultDialogControl
|
||||
/// </summary>
|
||||
public DialogButton Buttons { get; set; } = DialogButton.OKCancel;
|
||||
/// <summary>
|
||||
/// Only works for DefaultDialogControl
|
||||
/// </summary>
|
||||
public string? Title { get; set; } = null;
|
||||
/// <summary>
|
||||
/// Only works for CustomDialogControl
|
||||
/// </summary>
|
||||
public bool ShowCloseButton { get; set; } = true;
|
||||
public bool CanLightDismiss { get; set; }
|
||||
public bool CanDragMove { get; set; } = true;
|
||||
}
|
||||
235
src/Ursa/Controls/Dialog/OverlayDialog.cs
Normal file
235
src/Ursa/Controls/Dialog/OverlayDialog.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public static class OverlayDialog
|
||||
{
|
||||
public static void Show<TView, TViewModel>(TViewModel vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null)
|
||||
where TView : Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var t = new DefaultDialogControl()
|
||||
{
|
||||
Content = new TView(){ DataContext = vm },
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogControl(t, options);
|
||||
host.AddDialog(t);
|
||||
}
|
||||
|
||||
public static void Show(Control control, object? vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var t = new DefaultDialogControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogControl(t, options);
|
||||
host.AddDialog(t);
|
||||
|
||||
}
|
||||
|
||||
public static void Show(object? vm, string? hostId = null, OverlayDialogOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl();
|
||||
view.DataContext = vm;
|
||||
var t = new DefaultDialogControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogControl(t, options);
|
||||
host.AddDialog(t);
|
||||
}
|
||||
|
||||
public static void ShowCustom<TView, TViewModel>(TViewModel vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var t = new CustomDialogControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDialogControl(t, options);
|
||||
host.AddDialog(t);
|
||||
}
|
||||
|
||||
public static void ShowCustom(Control control, object? vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var t = new CustomDialogControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDialogControl(t, options);
|
||||
host.AddDialog(t);
|
||||
}
|
||||
|
||||
public static void ShowCustom(object? vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
|
||||
view.DataContext = vm;
|
||||
var t = new CustomDialogControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDialogControl(t, options);
|
||||
host.AddDialog(t);
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowModal<TView, TViewModel>(TViewModel vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null, CancellationToken? token = default)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(DialogResult.None);
|
||||
var t = new DefaultDialogControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogControl(t, options);
|
||||
host.AddModalDialog(t);
|
||||
return t.ShowAsync<DialogResult>(token);
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowModal(Control control, object? vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null, CancellationToken? token = default)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(DialogResult.None);
|
||||
var t = new DefaultDialogControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDialogControl(t, options);
|
||||
host.AddModalDialog(t);
|
||||
return t.ShowAsync<DialogResult>(token);
|
||||
}
|
||||
|
||||
public static Task<TResult?> ShowCustomModal<TView, TViewModel, TResult>(TViewModel vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null, CancellationToken? token = default)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(default(TResult));
|
||||
var t = new CustomDialogControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDialogControl(t, options);
|
||||
host.AddModalDialog(t);
|
||||
return t.ShowAsync<TResult?>(token);
|
||||
}
|
||||
|
||||
public static Task<TResult?> ShowCustomModal<TResult>(Control control, object? vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null, CancellationToken? token = default)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(default(TResult));
|
||||
var t = new CustomDialogControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDialogControl(t, options);
|
||||
host.AddModalDialog(t);
|
||||
return t.ShowAsync<TResult?>(token);
|
||||
}
|
||||
|
||||
public static Task<TResult?> ShowCustomModal<TResult>(object? vm, string? hostId = null,
|
||||
OverlayDialogOptions? options = null, CancellationToken? token = default)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(default(TResult));
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
|
||||
view.DataContext = vm;
|
||||
var t = new CustomDialogControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDialogControl(t, options);
|
||||
host.AddModalDialog(t);
|
||||
return t.ShowAsync<TResult?>(token);
|
||||
}
|
||||
|
||||
private static void ConfigureCustomDialogControl(CustomDialogControl control, OverlayDialogOptions? options)
|
||||
{
|
||||
options ??= OverlayDialogOptions.Default;
|
||||
control.IsFullScreen = options.FullScreen;
|
||||
if (options.FullScreen)
|
||||
{
|
||||
control.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
control.VerticalAlignment = VerticalAlignment.Stretch;
|
||||
}
|
||||
control.HorizontalAnchor = options.HorizontalAnchor;
|
||||
control.VerticalAnchor = options.VerticalAnchor;
|
||||
control.ActualHorizontalAnchor = options.HorizontalAnchor;
|
||||
control.ActualVerticalAnchor = options.VerticalAnchor;
|
||||
control.HorizontalOffset =
|
||||
control.HorizontalAnchor == HorizontalPosition.Center ? null : options.HorizontalOffset;
|
||||
control.VerticalOffset =
|
||||
options.VerticalAnchor == VerticalPosition.Center ? null : options.VerticalOffset;
|
||||
control.IsCloseButtonVisible = options.ShowCloseButton;
|
||||
control.CanLightDismiss = options.CanLightDismiss;
|
||||
DialogControlBase.SetCanDragMove(control, options.CanDragMove);
|
||||
}
|
||||
|
||||
private static void ConfigureDefaultDialogControl(DefaultDialogControl control, OverlayDialogOptions? options)
|
||||
{
|
||||
if (options is null) options = new OverlayDialogOptions();
|
||||
control.IsFullScreen = options.FullScreen;
|
||||
if (options.FullScreen)
|
||||
{
|
||||
control.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
control.VerticalAlignment = VerticalAlignment.Stretch;
|
||||
}
|
||||
control.HorizontalAnchor = options.HorizontalAnchor;
|
||||
control.VerticalAnchor = options.VerticalAnchor;
|
||||
control.ActualHorizontalAnchor = options.HorizontalAnchor;
|
||||
control.ActualVerticalAnchor = options.VerticalAnchor;
|
||||
control.HorizontalOffset =
|
||||
control.HorizontalAnchor == HorizontalPosition.Center ? null : options.HorizontalOffset;
|
||||
control.VerticalOffset =
|
||||
options.VerticalAnchor == VerticalPosition.Center ? null : options.VerticalOffset;
|
||||
control.Mode = options.Mode;
|
||||
control.Buttons = options.Buttons;
|
||||
control.Title = options.Title;
|
||||
control.CanLightDismiss = options.CanLightDismiss;
|
||||
DialogControlBase.SetCanDragMove(control, options.CanDragMove);
|
||||
}
|
||||
|
||||
internal static T? Recall<T>(string? hostId) where T: Control
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return null;
|
||||
var item = host.Recall<T>();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
29
src/Ursa/Controls/DisableContainer/DisableContainer.cs
Normal file
29
src/Ursa/Controls/DisableContainer/DisableContainer.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class DisableContainer: TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<InputElement?> ContentProperty = AvaloniaProperty.Register<DisableContainer, InputElement?>(
|
||||
nameof(Content));
|
||||
|
||||
[Content]
|
||||
public InputElement? Content
|
||||
{
|
||||
get => GetValue(ContentProperty);
|
||||
set => SetValue(ContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> DisabledTipProperty = AvaloniaProperty.Register<DisableContainer, object?>(
|
||||
nameof(DisabledTip));
|
||||
|
||||
public object? DisabledTip
|
||||
{
|
||||
get => GetValue(DisabledTipProperty);
|
||||
set => SetValue(DisabledTipProperty, value);
|
||||
}
|
||||
}
|
||||
53
src/Ursa/Controls/DisableContainer/DisabledAdorner.cs
Normal file
53
src/Ursa/Controls/DisableContainer/DisabledAdorner.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Irihi.Avalonia.Shared.Shapes;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class DisabledAdorner
|
||||
{
|
||||
public static readonly AttachedProperty<bool> IsEnabledProperty =
|
||||
AvaloniaProperty.RegisterAttached<DisabledAdorner, InputElement, bool>("IsEnabled");
|
||||
|
||||
public static void SetIsEnabled(InputElement obj, bool value) => obj.SetValue(IsEnabledProperty, value);
|
||||
public static bool GetIsEnabled(InputElement obj) => obj.GetValue(IsEnabledProperty);
|
||||
|
||||
public static readonly AttachedProperty<object?> DisabledTipProperty =
|
||||
AvaloniaProperty.RegisterAttached<DisabledAdorner, InputElement, object?>("DisabledTip");
|
||||
|
||||
public static void SetDisabledTip(InputElement obj, object? value) => obj.SetValue(DisabledTipProperty, value);
|
||||
public static object? GetDisabledTip(InputElement obj) => obj.GetValue(DisabledTipProperty);
|
||||
|
||||
static DisabledAdorner()
|
||||
{
|
||||
IsEnabledProperty.Changed.AddClassHandler<InputElement, bool>(OnIsEnabledChanged);
|
||||
}
|
||||
|
||||
private static void OnIsEnabledChanged(InputElement arg1, AvaloniaPropertyChangedEventArgs<bool> arg2)
|
||||
{
|
||||
if (arg2.NewValue.Value)
|
||||
{
|
||||
var pureRectangle = new PureRectangle()
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
IsHitTestVisible = true,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
Cursor = new Cursor(StandardCursorType.No),
|
||||
[!ToolTip.TipProperty] = arg1[!DisabledTipProperty],
|
||||
};
|
||||
var binding = arg1.GetObservable(InputElement.IsEnabledProperty, converter: (a) => !a).ToBinding();
|
||||
pureRectangle.Bind(Visual.IsVisibleProperty, binding);
|
||||
AdornerLayer.SetAdorner(arg1, pureRectangle);
|
||||
}
|
||||
else
|
||||
{
|
||||
AdornerLayer.SetAdorner(arg1, null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
28
src/Ursa/Controls/Drawer/CustomDrawerControl.cs
Normal file
28
src/Ursa/Controls/Drawer/CustomDrawerControl.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class CustomDrawerControl: DrawerControlBase
|
||||
{
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_closeButton is not null)
|
||||
{
|
||||
_closeButton.IsVisible = IsCloseButtonVisible;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnElementClosing(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/Ursa/Controls/Drawer/DefaultDrawerControl.cs
Normal file
139
src/Ursa/Controls/Drawer/DefaultDrawerControl.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_YesButton, typeof(Button))]
|
||||
[TemplatePart(PART_NoButton, typeof(Button))]
|
||||
[TemplatePart(PART_OKButton, typeof(Button))]
|
||||
[TemplatePart(PART_CancelButton, typeof(Button))]
|
||||
public class DefaultDrawerControl: DrawerControlBase
|
||||
{
|
||||
public const string PART_YesButton = "PART_YesButton";
|
||||
public const string PART_NoButton = "PART_NoButton";
|
||||
public const string PART_OKButton = "PART_OKButton";
|
||||
public const string PART_CancelButton = "PART_CancelButton";
|
||||
|
||||
private Button? _yesButton;
|
||||
private Button? _noButton;
|
||||
private Button? _okButton;
|
||||
private Button? _cancelButton;
|
||||
|
||||
public static readonly StyledProperty<DialogButton> ButtonsProperty = AvaloniaProperty.Register<DefaultDrawerControl, DialogButton>(
|
||||
nameof(Buttons), DialogButton.OKCancel);
|
||||
|
||||
public DialogButton Buttons
|
||||
{
|
||||
get => GetValue(ButtonsProperty);
|
||||
set => SetValue(ButtonsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<DialogMode> ModeProperty = AvaloniaProperty.Register<DefaultDrawerControl, DialogMode>(
|
||||
nameof(Mode), DialogMode.None);
|
||||
|
||||
public DialogMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> TitleProperty = AvaloniaProperty.Register<DefaultDrawerControl, string?>(
|
||||
nameof(Title));
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
|
||||
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
|
||||
_noButton = e.NameScope.Find<Button>(PART_NoButton);
|
||||
_okButton = e.NameScope.Find<Button>(PART_OKButton);
|
||||
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
|
||||
Button.ClickEvent.AddHandler(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
|
||||
SetButtonVisibility();
|
||||
}
|
||||
|
||||
private void SetButtonVisibility()
|
||||
{
|
||||
bool isCloseButtonVisible = DataContext is IDialogContext || Buttons != DialogButton.YesNo;
|
||||
Button.IsVisibleProperty.SetValue(isCloseButtonVisible, _closeButton);
|
||||
switch (Buttons)
|
||||
{
|
||||
case DialogButton.None:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.OK:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.OKCancel:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.YesNo:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _yesButton, _noButton);
|
||||
break;
|
||||
case DialogButton.YesNoCancel:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDefaultButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button == _okButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.OK);
|
||||
}
|
||||
else if (button == _cancelButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.Cancel);
|
||||
}
|
||||
else if (button == _yesButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.Yes);
|
||||
}
|
||||
else if (button == _noButton)
|
||||
{
|
||||
OnElementClosing(this, DialogResult.No);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogResult result = Buttons switch
|
||||
{
|
||||
DialogButton.None => DialogResult.None,
|
||||
DialogButton.OK => DialogResult.OK,
|
||||
DialogButton.OKCancel => DialogResult.Cancel,
|
||||
DialogButton.YesNo => DialogResult.No,
|
||||
DialogButton.YesNoCancel => DialogResult.Cancel,
|
||||
_ => DialogResult.None
|
||||
};
|
||||
RaiseEvent(new ResultEventArgs(ClosedEvent, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
227
src/Ursa/Controls/Drawer/Drawer.cs
Normal file
227
src/Ursa/Controls/Drawer/Drawer.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Ursa.Common;
|
||||
using Ursa.Controls.Options;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public static class Drawer
|
||||
{
|
||||
public static void Show<TView, TViewModel>(TViewModel vm, string? hostId = null, DrawerOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var drawer = new DefaultDrawerControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDrawer(drawer, options);
|
||||
host.AddDrawer(drawer);
|
||||
}
|
||||
|
||||
public static void Show(Control control, object? vm, string? hostId = null,
|
||||
DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var drawer = new DefaultDrawerControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDrawer(drawer, options);
|
||||
host.AddDrawer(drawer);
|
||||
}
|
||||
|
||||
public static void Show(object? vm, string? hostId = null, DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
|
||||
view.DataContext = vm;
|
||||
var drawer = new DefaultDrawerControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDrawer(drawer, options);
|
||||
host.AddDrawer(drawer);
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowModal<TView, TViewModel>(TViewModel vm, string? hostId = null, DrawerOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(DialogResult.None);
|
||||
var drawer = new DefaultDrawerControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDrawer(drawer, options);
|
||||
host.AddModalDrawer(drawer);
|
||||
return drawer.ShowAsync<DialogResult>();
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowModal(Control control, object? vm, string? hostId = null,
|
||||
DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(DialogResult.None);
|
||||
var drawer = new DefaultDrawerControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDrawer(drawer, options);
|
||||
host.AddModalDrawer(drawer);
|
||||
return drawer.ShowAsync<DialogResult>();
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowModal(object? vm, string? hostId = null, DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult(DialogResult.None);
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
|
||||
view.DataContext = vm;
|
||||
var drawer = new DefaultDrawerControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureDefaultDrawer(drawer, options);
|
||||
host.AddModalDrawer(drawer);
|
||||
return drawer.ShowAsync<DialogResult>();
|
||||
}
|
||||
|
||||
public static void ShowCustom<TView, TViewModel>(TViewModel vm, string? hostId = null, DrawerOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var dialog = new CustomDrawerControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDrawer(dialog, options);
|
||||
host.AddDrawer(dialog);
|
||||
}
|
||||
|
||||
public static void ShowCustom(Control control, object? vm, string? hostId = null, DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var dialog = new CustomDrawerControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDrawer(dialog, options);
|
||||
host.AddDrawer(dialog);
|
||||
}
|
||||
|
||||
public static void ShowCustom(object? vm, string? hostId = null, DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return;
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
|
||||
view.DataContext = vm;
|
||||
var dialog = new CustomDrawerControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDrawer(dialog, options);
|
||||
host.AddDrawer(dialog);
|
||||
}
|
||||
|
||||
public static Task<TResult?> ShowCustomModal<TView, TViewModel, TResult>(TViewModel vm, string? hostId = null, DrawerOptions? options = null)
|
||||
where TView: Control, new()
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult<TResult?>(default);
|
||||
var dialog = new CustomDrawerControl()
|
||||
{
|
||||
Content = new TView(),
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDrawer(dialog, options);
|
||||
host.AddModalDrawer(dialog);
|
||||
return dialog.ShowAsync<TResult?>();
|
||||
}
|
||||
|
||||
public static Task<TResult?> ShowCustomModal<TResult>(Control control, object? vm, string? hostId = null, DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult<TResult?>(default);
|
||||
var dialog = new CustomDrawerControl()
|
||||
{
|
||||
Content = control,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDrawer(dialog, options);
|
||||
host.AddModalDrawer(dialog);
|
||||
return dialog.ShowAsync<TResult?>();
|
||||
}
|
||||
|
||||
public static Task<TResult?> ShowCustomModal<TResult>(object? vm, string? hostId = null, DrawerOptions? options = null)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return Task.FromResult<TResult?>(default);
|
||||
var view = host.GetDataTemplate(vm)?.Build(vm);
|
||||
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
|
||||
view.DataContext = vm;
|
||||
var dialog = new CustomDrawerControl()
|
||||
{
|
||||
Content = view,
|
||||
DataContext = vm,
|
||||
};
|
||||
ConfigureCustomDrawer(dialog, options);
|
||||
host.AddModalDrawer(dialog);
|
||||
return dialog.ShowAsync<TResult?>();
|
||||
}
|
||||
|
||||
private static void ConfigureCustomDrawer(CustomDrawerControl drawer, DrawerOptions? options)
|
||||
{
|
||||
options ??= DrawerOptions.Default;
|
||||
drawer.Position = options.Position;
|
||||
drawer.IsCloseButtonVisible = options.ShowCloseButton;
|
||||
drawer.CanLightDismiss = options.CanLightDismiss;
|
||||
if (options.Position == Position.Left || options.Position == Position.Right)
|
||||
{
|
||||
drawer.MinWidth = options.MinWidth ?? 0.0;
|
||||
drawer.MaxWidth = options.MaxWidth ?? double.PositiveInfinity;
|
||||
}
|
||||
if (options.Position is Position.Top or Position.Bottom)
|
||||
{
|
||||
drawer.MinHeight = options.MinHeight ?? 0.0;
|
||||
drawer.MaxHeight = options.MaxHeight ?? double.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureDefaultDrawer(DefaultDrawerControl drawer, DrawerOptions? options)
|
||||
{
|
||||
options ??= DrawerOptions.Default;
|
||||
drawer.Position = options.Position;
|
||||
drawer.IsCloseButtonVisible = options.IsCloseButtonVisible;
|
||||
drawer.CanLightDismiss = options.CanLightDismiss;
|
||||
drawer.Buttons = options.Buttons;
|
||||
drawer.Title = options.Title;
|
||||
if (options.Position == Position.Left || options.Position == Position.Right)
|
||||
{
|
||||
drawer.MinWidth = options.MinWidth ?? 0.0;
|
||||
drawer.MaxWidth = options.MaxWidth ?? double.PositiveInfinity;
|
||||
}
|
||||
if (options.Position is Position.Top or Position.Bottom)
|
||||
{
|
||||
drawer.MinHeight = options.MinHeight ?? 0.0;
|
||||
drawer.MaxHeight = options.MaxHeight ?? double.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/Ursa/Controls/Drawer/DrawerControlBase.cs
Normal file
96
src/Ursa/Controls/Drawer/DrawerControlBase.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
using Ursa.Controls.OverlayShared;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_CloseButton, typeof(Button))]
|
||||
public abstract class DrawerControlBase: OverlayFeedbackElement
|
||||
{
|
||||
public const string PART_CloseButton = "PART_CloseButton";
|
||||
|
||||
protected internal Button? _closeButton;
|
||||
|
||||
public static readonly StyledProperty<Position> PositionProperty =
|
||||
AvaloniaProperty.Register<DrawerControlBase, Position>(
|
||||
nameof(Position), defaultValue: Position.Right);
|
||||
|
||||
public Position Position
|
||||
{
|
||||
get => GetValue(PositionProperty);
|
||||
set => SetValue(PositionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsOpenProperty = AvaloniaProperty.Register<DrawerControlBase, bool>(
|
||||
nameof(IsOpen));
|
||||
|
||||
public bool IsOpen
|
||||
{
|
||||
get => GetValue(IsOpenProperty);
|
||||
set => SetValue(IsOpenProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsCloseButtonVisibleProperty =
|
||||
AvaloniaProperty.Register<DrawerControlBase, bool>(
|
||||
nameof(IsCloseButtonVisible), defaultValue: true);
|
||||
|
||||
public bool IsCloseButtonVisible
|
||||
{
|
||||
get => GetValue(IsCloseButtonVisibleProperty);
|
||||
set => SetValue(IsCloseButtonVisibleProperty, value);
|
||||
}
|
||||
|
||||
protected internal bool CanLightDismiss { get; set; }
|
||||
|
||||
static DrawerControlBase()
|
||||
{
|
||||
DataContextProperty.Changed.AddClassHandler<DrawerControlBase, object?>((o, e) => o.OnDataContextChange(e));
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(OnCloseButtonClick, _closeButton);
|
||||
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
|
||||
Button.ClickEvent.AddHandler(OnCloseButtonClick, _closeButton);
|
||||
}
|
||||
|
||||
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
if(args.OldValue.Value is IDialogContext oldContext)
|
||||
{
|
||||
oldContext.RequestClose -= OnContextRequestClose;
|
||||
}
|
||||
if(args.NewValue.Value is IDialogContext newContext)
|
||||
{
|
||||
newContext.RequestClose += OnContextRequestClose;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnContextRequestClose(object sender, object? e)
|
||||
{
|
||||
RaiseEvent(new ResultEventArgs(ClosedEvent, e));
|
||||
}
|
||||
|
||||
private void OnCloseButtonClick(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (DataContext is IDialogContext context)
|
||||
{
|
||||
context.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseEvent(new ResultEventArgs(ClosedEvent, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Ursa/Controls/Drawer/Options/DrawerOptions.cs
Normal file
18
src/Ursa/Controls/Drawer/Options/DrawerOptions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls.Options;
|
||||
|
||||
public class DrawerOptions
|
||||
{
|
||||
internal static DrawerOptions Default => new ();
|
||||
public Position Position { get; set; } = Position.Right;
|
||||
public bool CanLightDismiss { get; set; } = true;
|
||||
public bool IsCloseButtonVisible { get; set; } = true;
|
||||
public double? MinWidth { get; set; } = null;
|
||||
public double? MinHeight { get; set; } = null;
|
||||
public double? MaxWidth { get; set; } = null;
|
||||
public double? MaxHeight { get; set; } = null;
|
||||
public DialogButton Buttons { get; set; } = DialogButton.OKCancel;
|
||||
public string? Title { get; set; }
|
||||
public bool ShowCloseButton { get; set; } = true;
|
||||
}
|
||||
158
src/Ursa/Controls/EnumSelector/EnumSelector.cs
Normal file
158
src/Ursa/Controls/EnumSelector/EnumSelector.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class EnumItemTuple
|
||||
{
|
||||
public string? DisplayName { get; set; }
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
|
||||
public class EnumSelector: TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<Type?> EnumTypeProperty = AvaloniaProperty.Register<EnumSelector, Type?>(
|
||||
nameof(EnumType), validate: OnTypeValidate);
|
||||
|
||||
public Type? EnumType
|
||||
{
|
||||
get => GetValue(EnumTypeProperty);
|
||||
set => SetValue(EnumTypeProperty, value);
|
||||
}
|
||||
|
||||
private static bool OnTypeValidate(Type? arg)
|
||||
{
|
||||
if (arg is null) return true;
|
||||
return arg.IsEnum;
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> ValueProperty = AvaloniaProperty.Register<EnumSelector, object?>(
|
||||
nameof(Value), defaultBindingMode: BindingMode.TwoWay, coerce:OnValueCoerce);
|
||||
|
||||
private static object? OnValueCoerce(AvaloniaObject o, object? value)
|
||||
{
|
||||
if (o is not EnumSelector selector) return null;
|
||||
if (value is null) return null;
|
||||
if (value.GetType() != selector.EnumType) return null;
|
||||
var first = selector.Values?.FirstOrDefault(a => Equals(a.Value, value));
|
||||
if (first is null) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public object? Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
private EnumItemTuple? _selectedValue;
|
||||
|
||||
public static readonly DirectProperty<EnumSelector, EnumItemTuple?> SelectedValueProperty = AvaloniaProperty.RegisterDirect<EnumSelector, EnumItemTuple?>(
|
||||
nameof(SelectedValue), o => o.SelectedValue, (o, v) => o.SelectedValue = v);
|
||||
|
||||
public EnumItemTuple? SelectedValue
|
||||
{
|
||||
get => _selectedValue;
|
||||
private set => SetAndRaise(SelectedValueProperty, ref _selectedValue, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<EnumSelector, IList<EnumItemTuple>?> ValuesProperty = AvaloniaProperty.RegisterDirect<EnumSelector, IList<EnumItemTuple>?>(
|
||||
nameof(Values), o => o.Values);
|
||||
|
||||
private IList<EnumItemTuple>? _values;
|
||||
internal IList<EnumItemTuple>? Values
|
||||
{
|
||||
get => _values;
|
||||
private set => SetAndRaise(ValuesProperty, ref _values, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> DisplayDescriptionProperty = AvaloniaProperty.Register<EnumSelector, bool>(
|
||||
nameof(DisplayDescription));
|
||||
|
||||
public bool DisplayDescription
|
||||
{
|
||||
get => GetValue(DisplayDescriptionProperty);
|
||||
set => SetValue(DisplayDescriptionProperty, value);
|
||||
}
|
||||
|
||||
static EnumSelector()
|
||||
{
|
||||
EnumTypeProperty.Changed.AddClassHandler<EnumSelector, Type?>((o, e) => o.OnTypeChanged(e));
|
||||
SelectedValueProperty.Changed.AddClassHandler<EnumSelector, EnumItemTuple?>((o, e) => o.OnSelectedValueChanged(e));
|
||||
ValueProperty.Changed.AddClassHandler<EnumSelector, object?>((o, e) => o.OnValueChanged(e));
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
if (_updateFromComboBox) return;
|
||||
var newValue = args.NewValue.Value;
|
||||
if (newValue is null)
|
||||
{
|
||||
SetCurrentValue(SelectedValueProperty, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (newValue.GetType() != EnumType)
|
||||
{
|
||||
SetCurrentValue(SelectedValueProperty, null);
|
||||
}
|
||||
var tuple = Values?.FirstOrDefault(x => Equals(x.Value, newValue));
|
||||
SetCurrentValue(SelectedValueProperty, tuple);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _updateFromComboBox;
|
||||
|
||||
private void OnSelectedValueChanged(AvaloniaPropertyChangedEventArgs<EnumItemTuple?> args)
|
||||
{
|
||||
_updateFromComboBox = true;
|
||||
var newValue = args.NewValue.Value;
|
||||
SetCurrentValue(ValueProperty, newValue?.Value);
|
||||
_updateFromComboBox = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void OnTypeChanged(AvaloniaPropertyChangedEventArgs<Type?> args)
|
||||
{
|
||||
Values?.Clear();
|
||||
var newType = args.GetNewValue<Type?>();
|
||||
if (newType is null || !newType.IsEnum)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Values = GenerateItemTuple();
|
||||
}
|
||||
|
||||
private List<EnumItemTuple> GenerateItemTuple()
|
||||
{
|
||||
if (EnumType is null) return new List<EnumItemTuple>();
|
||||
var values = Enum.GetValues(EnumType);
|
||||
List<EnumItemTuple> list = new();
|
||||
var fields = EnumType.GetFields();
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (value.GetType() == EnumType)
|
||||
{
|
||||
var displayName = value.ToString();
|
||||
var field = EnumType.GetField(displayName);
|
||||
var description = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault();
|
||||
if (description is not null)
|
||||
{
|
||||
displayName = ((DescriptionAttribute) description).Description;
|
||||
}
|
||||
list.Add(new EnumItemTuple()
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
NoLabelProperty.AffectsPseudoClass<FormItem>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -43,7 +44,7 @@ public class IPv4Box: TemplatedControl
|
||||
private TextPresenter? _currentActivePresenter;
|
||||
|
||||
public static readonly StyledProperty<IPAddress?> IPAddressProperty = AvaloniaProperty.Register<IPv4Box, IPAddress?>(
|
||||
nameof(IPAddress));
|
||||
nameof(IPAddress), defaultBindingMode: BindingMode.TwoWay);
|
||||
public IPAddress? IPAddress
|
||||
{
|
||||
get => GetValue(IPAddressProperty);
|
||||
@@ -115,6 +116,19 @@ public class IPv4Box: TemplatedControl
|
||||
_presenters[1] = _secondText;
|
||||
_presenters[2] = _thirdText;
|
||||
_presenters[3] = _fourthText;
|
||||
if (this.IPAddress != null)
|
||||
{
|
||||
var sections = IPAddress.ToString().Split('.');
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var presenter = _presenters[i];
|
||||
if (presenter != null)
|
||||
{
|
||||
presenter.Text = sections[i];
|
||||
}
|
||||
}
|
||||
ParseBytes(ShowLeadingZero);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
@@ -122,7 +136,7 @@ public class IPv4Box: TemplatedControl
|
||||
if (_currentActivePresenter is null) return;
|
||||
var keymap = TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration;
|
||||
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
|
||||
if (e.Key == Key.Enter)
|
||||
if (e.Key is Key.Enter or Key.Return)
|
||||
{
|
||||
ParseBytes(ShowLeadingZero);
|
||||
SetIPAddressInternal();
|
||||
@@ -143,6 +157,10 @@ public class IPv4Box: TemplatedControl
|
||||
{
|
||||
Paste();
|
||||
}
|
||||
else if (keymap is not null && Match(keymap.Cut))
|
||||
{
|
||||
Cut();
|
||||
}
|
||||
if (e.Key == Key.Tab)
|
||||
{
|
||||
_currentActivePresenter?.HideCaret();
|
||||
@@ -156,25 +174,6 @@ public class IPv4Box: TemplatedControl
|
||||
_currentActivePresenter?.ShowCaret();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.OemPeriod || e.Key == Key.Decimal)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentActivePresenter.Text))
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
return;
|
||||
}
|
||||
_currentActivePresenter?.HideCaret();
|
||||
_currentActivePresenter.ClearSelection();
|
||||
if (Equals(_currentActivePresenter, _fourthText))
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
return;
|
||||
}
|
||||
MoveToNextPresenter(_currentActivePresenter, false);
|
||||
_currentActivePresenter?.ShowCaret();
|
||||
_currentActivePresenter.MoveCaretToStart();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Back)
|
||||
{
|
||||
DeleteImplementation(_currentActivePresenter);
|
||||
@@ -198,10 +197,20 @@ public class IPv4Box: TemplatedControl
|
||||
if (e.Handled) return;
|
||||
string? s = e.Text;
|
||||
if (string.IsNullOrEmpty(s)) return;
|
||||
if (s == ".")
|
||||
{
|
||||
_currentActivePresenter?.HideCaret();
|
||||
_currentActivePresenter.ClearSelection();
|
||||
MoveToNextPresenter(_currentActivePresenter, false);
|
||||
_currentActivePresenter?.ShowCaret();
|
||||
_currentActivePresenter.MoveCaretToStart();
|
||||
e.Handled = false;
|
||||
return;
|
||||
}
|
||||
if (!char.IsNumber(s![0])) return;
|
||||
if (_currentActivePresenter != null)
|
||||
{
|
||||
int index = _currentActivePresenter.CaretIndex;
|
||||
int index = Math.Min(_currentActivePresenter.CaretIndex, _currentActivePresenter.Text.Length);
|
||||
string? oldText = _currentActivePresenter.Text;
|
||||
if (oldText is null)
|
||||
{
|
||||
|
||||
87
src/Ursa/Controls/Icons/TwoTonePathIcon.cs
Normal file
87
src/Ursa/Controls/Icons/TwoTonePathIcon.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Active)]
|
||||
public class TwoTonePathIcon: TemplatedControl
|
||||
{
|
||||
public const string PC_Active = ":active";
|
||||
|
||||
public static readonly StyledProperty<IBrush?> StrokeBrushProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||
nameof(StrokeBrush));
|
||||
|
||||
public IBrush? StrokeBrush
|
||||
{
|
||||
get => GetValue(StrokeBrushProperty);
|
||||
set => SetValue(StrokeBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Geometry> DataProperty = AvaloniaProperty.Register<PathIcon, Geometry>(
|
||||
nameof(Data));
|
||||
|
||||
public Geometry Data
|
||||
{
|
||||
get => GetValue(DataProperty);
|
||||
set => SetValue(DataProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsActiveProperty = AvaloniaProperty.Register<TwoTonePathIcon, bool>(
|
||||
nameof(IsActive), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool IsActive
|
||||
{
|
||||
get => GetValue(IsActiveProperty);
|
||||
set => SetValue(IsActiveProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> ActiveForegroundProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||
nameof(ActiveForeground));
|
||||
|
||||
public IBrush? ActiveForeground
|
||||
{
|
||||
get => GetValue(ActiveForegroundProperty);
|
||||
set => SetValue(ActiveForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> ActiveStrokeBrushProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||
nameof(ActiveStrokeBrush));
|
||||
|
||||
public IBrush? ActiveStrokeBrush
|
||||
{
|
||||
get => GetValue(ActiveStrokeBrushProperty);
|
||||
set => SetValue(ActiveStrokeBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> StrokeThicknessProperty =
|
||||
AvaloniaProperty.Register<TwoTonePathIcon, double>(
|
||||
nameof(StrokeThickness));
|
||||
public double StrokeThickness
|
||||
{
|
||||
get => GetValue(StrokeThicknessProperty);
|
||||
set => SetValue(StrokeThicknessProperty, value);
|
||||
}
|
||||
|
||||
static TwoTonePathIcon()
|
||||
{
|
||||
AffectsRender<TwoTonePathIcon>(
|
||||
DataProperty,
|
||||
StrokeBrushProperty,
|
||||
ForegroundProperty,
|
||||
ActiveForegroundProperty,
|
||||
ActiveStrokeBrushProperty);
|
||||
IsActiveProperty.AffectsPseudoClass<TwoTonePathIcon>(PC_Active);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
PseudoClasses.Set(PC_Active, IsActive);
|
||||
}
|
||||
}
|
||||
321
src/Ursa/Controls/ImageViewer/ImageViewer.cs
Normal file
321
src/Ursa/Controls/ImageViewer/ImageViewer.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_Image, typeof(Image))]
|
||||
[TemplatePart(PART_Layer, typeof(VisualLayerManager))]
|
||||
[PseudoClasses(PC_Moving)]
|
||||
public class ImageViewer: TemplatedControl
|
||||
{
|
||||
public const string PART_Image = "PART_Image";
|
||||
public const string PART_Layer = "PART_Layer";
|
||||
public const string PC_Moving = ":moving";
|
||||
|
||||
private Image? _image = null!;
|
||||
private VisualLayerManager? _layer;
|
||||
private Point? _lastClickPoint;
|
||||
private Point? _lastLocation;
|
||||
private bool _moving;
|
||||
|
||||
public static readonly StyledProperty<Control?> OverlayerProperty = AvaloniaProperty.Register<ImageViewer, Control?>(
|
||||
nameof(Overlayer));
|
||||
|
||||
public Control? Overlayer
|
||||
{
|
||||
get => GetValue(OverlayerProperty);
|
||||
set => SetValue(OverlayerProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IImage?> SourceProperty = Image.SourceProperty.AddOwner<ImageViewer>();
|
||||
public IImage? Source
|
||||
{
|
||||
get => GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
private double _scale = 1;
|
||||
|
||||
public static readonly DirectProperty<ImageViewer, double> ScaleProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||
nameof(Scale), o => o.Scale, (o,v)=> o.Scale = v, unsetValue: 1);
|
||||
|
||||
public double Scale
|
||||
{
|
||||
get => _scale;
|
||||
set => SetAndRaise(ScaleProperty, ref _scale, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<ImageViewer, double> MinScaleProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||
nameof(MinScale), o => o.MinScale, (o, v) => o.MinScale = v, unsetValue: 0.1);
|
||||
|
||||
public double MinScale
|
||||
{
|
||||
get => _minScale;
|
||||
set => SetAndRaise(ScaleProperty, ref _minScale, value);
|
||||
}
|
||||
private double _minScale = 1;
|
||||
|
||||
private double _translateX;
|
||||
|
||||
public static readonly DirectProperty<ImageViewer, double> TranslateXProperty = AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||
nameof(TranslateX), o => o.TranslateX, (o,v)=>o.TranslateX = v, unsetValue: 0);
|
||||
|
||||
public double TranslateX
|
||||
{
|
||||
get => _translateX;
|
||||
set => SetAndRaise(TranslateXProperty, ref _translateX, value);
|
||||
}
|
||||
|
||||
private double _translateY;
|
||||
|
||||
public static readonly DirectProperty<ImageViewer, double> TranslateYProperty =
|
||||
AvaloniaProperty.RegisterDirect<ImageViewer, double>(
|
||||
nameof(TranslateY), o => o.TranslateY, (o, v) => o.TranslateY = v, unsetValue: 0);
|
||||
|
||||
public double TranslateY
|
||||
{
|
||||
get => _translateY;
|
||||
set => SetAndRaise(TranslateYProperty, ref _translateY, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> SmallChangeProperty = AvaloniaProperty.Register<ImageViewer, double>(
|
||||
nameof(SmallChange), defaultValue: 1);
|
||||
|
||||
public double SmallChange
|
||||
{
|
||||
get => GetValue(SmallChangeProperty);
|
||||
set => SetValue(SmallChangeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> LargeChangeProperty = AvaloniaProperty.Register<ImageViewer, double>(
|
||||
nameof(LargeChange), defaultValue: 10);
|
||||
|
||||
public double LargeChange
|
||||
{
|
||||
get => GetValue(LargeChangeProperty);
|
||||
set => SetValue(LargeChangeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Stretch> StretchProperty =
|
||||
Image.StretchProperty.AddOwner<ImageViewer>(new StyledPropertyMetadata<Stretch>(Stretch.Uniform));
|
||||
|
||||
public Stretch Stretch
|
||||
{
|
||||
get => GetValue(StretchProperty);
|
||||
set => SetValue(StretchProperty, value);
|
||||
}
|
||||
|
||||
private double _sourceMinScale = 0.1;
|
||||
|
||||
static ImageViewer()
|
||||
{
|
||||
FocusableProperty.OverrideDefaultValue<ImageViewer>(true);
|
||||
OverlayerProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnOverlayerChanged(e));
|
||||
SourceProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnSourceChanged(e));
|
||||
TranslateXProperty.Changed.AddClassHandler<ImageViewer>((o,e)=>o.OnTranslateXChanged(e));
|
||||
TranslateYProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnTranslateYChanged(e));
|
||||
StretchProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnStretchChanged(e));
|
||||
MinScaleProperty.Changed.AddClassHandler<ImageViewer>((o, e) => o.OnMinScaleChanged(e));
|
||||
}
|
||||
|
||||
private void OnTranslateYChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
if (_moving) return;
|
||||
var newValue = args.GetNewValue<double>();
|
||||
if (_lastLocation is not null)
|
||||
{
|
||||
_lastLocation = _lastLocation.Value.WithY(newValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastLocation = new Point(0, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTranslateXChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
if (_moving) return;
|
||||
var newValue = args.GetNewValue<double>();
|
||||
if (_lastLocation is not null)
|
||||
{
|
||||
_lastLocation = _lastLocation.Value.WithX(newValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastLocation = new Point(newValue, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOverlayerChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
var control = args.GetNewValue<Control?>();
|
||||
if (control is { } c)
|
||||
{
|
||||
AdornerLayer.SetAdorner(this, c);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSourceChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
if(!IsLoaded) return;
|
||||
IImage image = args.GetNewValue<IImage>();
|
||||
Size size = image.Size;
|
||||
double width = this.Bounds.Width;
|
||||
double height = this.Bounds.Height;
|
||||
if (_image is not null)
|
||||
{
|
||||
_image.Width = size.Width;
|
||||
_image.Height = size.Height;
|
||||
}
|
||||
Scale = GetScaleRatio(width/size.Width, height/size.Height, this.Stretch);
|
||||
_sourceMinScale = Math.Min(width * MinScale / size.Width, height * MinScale / size.Height);
|
||||
}
|
||||
|
||||
private void OnStretchChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
var stretch = args.GetNewValue<Stretch>();
|
||||
Scale = GetScaleRatio(Width / _image!.Width, Height / _image!.Height, stretch);
|
||||
if(_image is { })
|
||||
{
|
||||
_sourceMinScale = Math.Min(Width * MinScale / _image.Width, Height * MinScale / _image.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sourceMinScale = MinScale;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMinScaleChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
if (_image is { })
|
||||
{
|
||||
_sourceMinScale = Math.Min(Width * MinScale / _image.Width, Height * MinScale / _image.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sourceMinScale = MinScale;
|
||||
}
|
||||
|
||||
if (_sourceMinScale > Scale)
|
||||
{
|
||||
Scale = _sourceMinScale;
|
||||
}
|
||||
}
|
||||
|
||||
private double GetScaleRatio(double widthRatio, double heightRatio, Stretch stretch)
|
||||
{
|
||||
return stretch switch
|
||||
{
|
||||
Stretch.Fill => 1d,
|
||||
Stretch.None => 1d,
|
||||
Stretch.Uniform => Math.Min(widthRatio, heightRatio),
|
||||
Stretch.UniformToFill => Math.Max(widthRatio, heightRatio),
|
||||
_ => 1d,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_image = e.NameScope.Get<Image>(PART_Image);
|
||||
_layer = e.NameScope.Get<VisualLayerManager>(PART_Layer);
|
||||
if (Overlayer is { } c)
|
||||
{
|
||||
AdornerLayer.SetAdorner(this, c);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
if (Source is { } i && _image is { })
|
||||
{
|
||||
Size size = i.Size;
|
||||
double width = Bounds.Width;
|
||||
double height = Bounds.Height;
|
||||
_image.Width = size.Width;
|
||||
_image.Height = size.Height;
|
||||
Scale = GetScaleRatio(width/size.Width, height/size.Height, this.Stretch);
|
||||
_sourceMinScale = Math.Min(width * MinScale / size.Width, height * MinScale / size.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sourceMinScale = MinScale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
|
||||
{
|
||||
base.OnPointerWheelChanged(e);
|
||||
if(e.Delta.Y > 0)
|
||||
{
|
||||
Scale *= 1.1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var scale = Scale;
|
||||
scale /= 1.1;
|
||||
if (scale < _sourceMinScale) scale = _sourceMinScale;
|
||||
Scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
base.OnPointerMoved(e);
|
||||
if (e.Pointer.Captured == this && _lastClickPoint != null)
|
||||
{
|
||||
PseudoClasses.Set(PC_Moving, true);
|
||||
Point p = e.GetPosition(this);
|
||||
double deltaX = p.X - _lastClickPoint.Value.X;
|
||||
double deltaY = p.Y - _lastClickPoint.Value.Y;
|
||||
TranslateX = deltaX + (_lastLocation?.X ?? 0);
|
||||
TranslateY = deltaY + (_lastLocation?.Y ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
e.Pointer.Capture(this);
|
||||
_lastClickPoint = e.GetPosition(this);
|
||||
_moving = true;
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
e.Pointer.Capture(null);
|
||||
_lastLocation = new Point(TranslateX, TranslateY);
|
||||
PseudoClasses.Set(PC_Moving, false);
|
||||
_moving = false;
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
double step = e.KeyModifiers.HasFlag(KeyModifiers.Control) ? LargeChange : SmallChange;
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left:
|
||||
TranslateX -= step;
|
||||
break;
|
||||
case Key.Right:
|
||||
TranslateX += step;
|
||||
break;
|
||||
case Key.Up:
|
||||
TranslateY -= step;
|
||||
break;
|
||||
case Key.Down:
|
||||
TranslateY += step;
|
||||
break;
|
||||
}
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Converters;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class KeyGestureInput: TemplatedControl
|
||||
[PseudoClasses(PC_Empty)]
|
||||
public class KeyGestureInput: TemplatedControl, IClearControl, IInnerContentControl
|
||||
{
|
||||
public const string PC_Empty = ":empty";
|
||||
static KeyGestureInput()
|
||||
{
|
||||
InputElement.FocusableProperty.OverrideDefaultValue<KeyGestureInput>(true);
|
||||
FocusableProperty.OverrideDefaultValue<KeyGestureInput>(true);
|
||||
GestureProperty.Changed.AddClassHandler<KeyGestureInput, KeyGesture?>((x, e) =>
|
||||
x.PseudoClasses.Set(PC_Empty, e.NewValue.Value is null));
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<KeyGesture> GestureProperty = AvaloniaProperty.Register<KeyGestureInput, KeyGesture>(
|
||||
public static readonly StyledProperty<KeyGesture?> GestureProperty = AvaloniaProperty.Register<KeyGestureInput, KeyGesture?>(
|
||||
nameof(Gesture));
|
||||
|
||||
public KeyGesture Gesture
|
||||
public KeyGesture? Gesture
|
||||
{
|
||||
get => GetValue(GestureProperty);
|
||||
set => SetValue(GestureProperty, value);
|
||||
@@ -60,7 +66,30 @@ public class KeyGestureInput: TemplatedControl
|
||||
get => GetValue(VerticalContentAlignmentProperty);
|
||||
set => SetValue(VerticalContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly StyledProperty<object?> InnerLeftContentProperty = AvaloniaProperty.Register<KeyGestureInput, object?>(
|
||||
nameof(InnerLeftContent));
|
||||
|
||||
public object? InnerLeftContent
|
||||
{
|
||||
get => GetValue(InnerLeftContentProperty);
|
||||
set => SetValue(InnerLeftContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerRightContentProperty = AvaloniaProperty.Register<KeyGestureInput, object?>(
|
||||
nameof(InnerRightContent));
|
||||
|
||||
public object? InnerRightContent
|
||||
{
|
||||
get => GetValue(InnerRightContentProperty);
|
||||
set => SetValue(InnerRightContentProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
PseudoClasses.Set(PC_Empty, Gesture is null);
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
@@ -94,4 +123,9 @@ public class KeyGestureInput: TemplatedControl
|
||||
Gesture = gesture;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SetCurrentValue(GestureProperty, null);
|
||||
}
|
||||
}
|
||||
8
src/Ursa/Controls/Layout/DefaultDialogLayout.cs
Normal file
8
src/Ursa/Controls/Layout/DefaultDialogLayout.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace Ursa.Controls.Layout;
|
||||
|
||||
public class DefaultDialogLayout: TemplatedControl
|
||||
{
|
||||
|
||||
}
|
||||
81
src/Ursa/Controls/MessageBox/MessageBox.cs
Normal file
81
src/Ursa/Controls/MessageBox/MessageBox.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public static class MessageBox
|
||||
{
|
||||
public static async Task<MessageBoxResult> ShowAsync(
|
||||
string message,
|
||||
string? title = null,
|
||||
MessageBoxIcon icon = MessageBoxIcon.None,
|
||||
MessageBoxButton button = MessageBoxButton.OKCancel)
|
||||
{
|
||||
var messageWindow = new MessageBoxWindow(button)
|
||||
{
|
||||
Content = message,
|
||||
Title = title,
|
||||
MessageIcon = icon,
|
||||
};
|
||||
var lifetime = Application.Current?.ApplicationLifetime;
|
||||
if (lifetime is IClassicDesktopStyleApplicationLifetime classLifetime)
|
||||
{
|
||||
var main = classLifetime.MainWindow;
|
||||
if (main is null)
|
||||
{
|
||||
messageWindow.Show();
|
||||
return MessageBoxResult.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await messageWindow.ShowDialog<MessageBoxResult>(main);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return MessageBoxResult.None;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<MessageBoxResult> ShowAsync(
|
||||
Window owner,
|
||||
string message,
|
||||
string title,
|
||||
MessageBoxIcon icon = MessageBoxIcon.None,
|
||||
MessageBoxButton button = MessageBoxButton.OKCancel)
|
||||
{
|
||||
var messageWindow = new MessageBoxWindow(button)
|
||||
{
|
||||
Content = message,
|
||||
Title = title,
|
||||
MessageIcon = icon,
|
||||
};
|
||||
var result = await messageWindow.ShowDialog<MessageBoxResult>(owner);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<MessageBoxResult> ShowOverlayAsync(
|
||||
string message,
|
||||
string? title = null,
|
||||
string? hostId = null,
|
||||
MessageBoxIcon icon = MessageBoxIcon.None,
|
||||
MessageBoxButton button = MessageBoxButton.OKCancel)
|
||||
{
|
||||
var host = OverlayDialogManager.GetHost(hostId);
|
||||
if (host is null) return MessageBoxResult.None;
|
||||
var messageControl = new MessageBoxControl()
|
||||
{
|
||||
Content = message,
|
||||
Title = title,
|
||||
Buttons = button,
|
||||
MessageIcon = icon,
|
||||
};
|
||||
host.AddModalDialog(messageControl);
|
||||
var result = await messageControl.ShowAsync<MessageBoxResult>();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
9
src/Ursa/Controls/MessageBox/MessageBoxButton.cs
Normal file
9
src/Ursa/Controls/MessageBox/MessageBoxButton.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum MessageBoxButton
|
||||
{
|
||||
OK,
|
||||
OKCancel,
|
||||
YesNo,
|
||||
YesNoCancel,
|
||||
}
|
||||
136
src/Ursa/Controls/MessageBox/MessageBoxControl.cs
Normal file
136
src/Ursa/Controls/MessageBox/MessageBoxControl.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// The messageBox used to display in OverlayDialogHost.
|
||||
/// </summary>
|
||||
[TemplatePart(PART_NoButton, typeof(Button))]
|
||||
[TemplatePart(PART_OKButton, typeof(Button))]
|
||||
[TemplatePart(PART_CancelButton, typeof(Button))]
|
||||
[TemplatePart(PART_YesButton, typeof(Button))]
|
||||
public class MessageBoxControl: DialogControlBase
|
||||
{
|
||||
public const string PART_YesButton = "PART_YesButton";
|
||||
public const string PART_NoButton = "PART_NoButton";
|
||||
public const string PART_OKButton = "PART_OKButton";
|
||||
public const string PART_CancelButton = "PART_CancelButton";
|
||||
|
||||
private Button? _yesButton;
|
||||
private Button? _noButton;
|
||||
private Button? _okButton;
|
||||
private Button? _cancelButton;
|
||||
|
||||
public static readonly StyledProperty<MessageBoxIcon> MessageIconProperty =
|
||||
AvaloniaProperty.Register<MessageBoxWindow, MessageBoxIcon>(
|
||||
nameof(MessageIcon));
|
||||
|
||||
public MessageBoxIcon MessageIcon
|
||||
{
|
||||
get => GetValue(MessageIconProperty);
|
||||
set => SetValue(MessageIconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<MessageBoxButton> ButtonsProperty = AvaloniaProperty.Register<MessageBoxControl, MessageBoxButton>(
|
||||
nameof(Buttons), MessageBoxButton.OK);
|
||||
|
||||
public MessageBoxButton Buttons
|
||||
{
|
||||
get => GetValue(ButtonsProperty);
|
||||
set => SetValue(ButtonsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> TitleProperty = AvaloniaProperty.Register<MessageBoxControl, string?>(
|
||||
nameof(Title));
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
static MessageBoxControl()
|
||||
{
|
||||
ButtonsProperty.Changed.AddClassHandler<MessageBoxControl>((o, e) => { o.SetButtonVisibility(); });
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(DefaultButtonsClose, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
_okButton = e.NameScope.Find<Button>(PART_OKButton);
|
||||
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
|
||||
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
|
||||
_noButton = e.NameScope.Find<Button>(PART_NoButton);
|
||||
Button.ClickEvent.AddHandler(DefaultButtonsClose, _okButton, _cancelButton, _yesButton, _noButton);
|
||||
SetButtonVisibility();
|
||||
}
|
||||
|
||||
private void DefaultButtonsClose(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button == _okButton)
|
||||
{
|
||||
OnElementClosing(this, MessageBoxResult.OK);
|
||||
}
|
||||
else if (button == _cancelButton)
|
||||
{
|
||||
OnElementClosing(this, MessageBoxResult.Cancel);
|
||||
}
|
||||
else if (button == _yesButton)
|
||||
{
|
||||
OnElementClosing(this, MessageBoxResult.Yes);
|
||||
}
|
||||
else if (button == _noButton)
|
||||
{
|
||||
OnElementClosing(this, MessageBoxResult.No);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetButtonVisibility()
|
||||
{
|
||||
switch (Buttons)
|
||||
{
|
||||
case MessageBoxButton.OK:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
case MessageBoxButton.OKCancel:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _yesButton, _noButton);
|
||||
break;
|
||||
case MessageBoxButton.YesNo:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _yesButton, _noButton);
|
||||
break;
|
||||
case MessageBoxButton.YesNoCancel:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
MessageBoxResult result = Buttons switch
|
||||
{
|
||||
MessageBoxButton.OK => MessageBoxResult.OK,
|
||||
MessageBoxButton.OKCancel => MessageBoxResult.Cancel,
|
||||
MessageBoxButton.YesNo => MessageBoxResult.No,
|
||||
MessageBoxButton.YesNoCancel => MessageBoxResult.Cancel,
|
||||
_ => MessageBoxResult.None
|
||||
};
|
||||
OnElementClosing(this, result);
|
||||
}
|
||||
}
|
||||
15
src/Ursa/Controls/MessageBox/MessageBoxIcon.cs
Normal file
15
src/Ursa/Controls/MessageBox/MessageBoxIcon.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum MessageBoxIcon
|
||||
{
|
||||
Asterisk, // Same as Information
|
||||
Error,
|
||||
Exclamation, // Same as Warning
|
||||
Hand, // Same as Error
|
||||
Information,
|
||||
None,
|
||||
Question,
|
||||
Stop, // Same as Error
|
||||
Warning,
|
||||
Success,
|
||||
}
|
||||
10
src/Ursa/Controls/MessageBox/MessageBoxResult.cs
Normal file
10
src/Ursa/Controls/MessageBox/MessageBoxResult.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum MessageBoxResult
|
||||
{
|
||||
Cancel,
|
||||
No,
|
||||
None,
|
||||
OK,
|
||||
Yes,
|
||||
}
|
||||
137
src/Ursa/Controls/MessageBox/MessageBoxWindow.cs
Normal file
137
src/Ursa/Controls/MessageBox/MessageBoxWindow.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_CloseButton, typeof(Button))]
|
||||
[TemplatePart(PART_NoButton, typeof(Button))]
|
||||
[TemplatePart(PART_OKButton, typeof(Button))]
|
||||
[TemplatePart(PART_CancelButton, typeof(Button))]
|
||||
[TemplatePart(PART_YesButton, typeof(Button))]
|
||||
public class MessageBoxWindow : Window
|
||||
{
|
||||
public const string PART_CloseButton = "PART_CloseButton";
|
||||
public const string PART_YesButton = "PART_YesButton";
|
||||
public const string PART_NoButton = "PART_NoButton";
|
||||
public const string PART_OKButton = "PART_OKButton";
|
||||
public const string PART_CancelButton = "PART_CancelButton";
|
||||
|
||||
private MessageBoxButton _buttonConfigs;
|
||||
|
||||
private Button? _closeButton;
|
||||
private Button? _yesButton;
|
||||
private Button? _noButton;
|
||||
private Button? _okButton;
|
||||
private Button? _cancelButton;
|
||||
|
||||
protected override Type StyleKeyOverride => typeof(MessageBoxWindow);
|
||||
|
||||
public static readonly StyledProperty<MessageBoxIcon> MessageIconProperty =
|
||||
AvaloniaProperty.Register<MessageBoxWindow, MessageBoxIcon>(
|
||||
nameof(MessageIcon));
|
||||
|
||||
public MessageBoxIcon MessageIcon
|
||||
{
|
||||
get => GetValue(MessageIconProperty);
|
||||
set => SetValue(MessageIconProperty, value);
|
||||
}
|
||||
|
||||
public MessageBoxWindow()
|
||||
{
|
||||
_buttonConfigs = MessageBoxButton.OK;
|
||||
}
|
||||
|
||||
public MessageBoxWindow(MessageBoxButton buttons)
|
||||
{
|
||||
_buttonConfigs = buttons;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
|
||||
Button.ClickEvent.RemoveHandler(OnCloseButtonClick, _closeButton);
|
||||
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
|
||||
_noButton = e.NameScope.Find<Button>(PART_NoButton);
|
||||
_okButton = e.NameScope.Find<Button>(PART_OKButton);
|
||||
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
|
||||
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
|
||||
Button.ClickEvent.AddHandler(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
|
||||
Button.ClickEvent.AddHandler(OnCloseButtonClick, _closeButton);
|
||||
SetButtonVisibility();
|
||||
}
|
||||
|
||||
private void SetButtonVisibility()
|
||||
{
|
||||
switch (_buttonConfigs)
|
||||
{
|
||||
case MessageBoxButton.OK:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
case MessageBoxButton.OKCancel:
|
||||
Button.IsVisibleProperty.SetValue(true, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(false, _yesButton, _noButton);
|
||||
break;
|
||||
case MessageBoxButton.YesNo:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton, _cancelButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _yesButton, _noButton);
|
||||
break;
|
||||
case MessageBoxButton.YesNoCancel:
|
||||
Button.IsVisibleProperty.SetValue(false, _okButton);
|
||||
Button.IsVisibleProperty.SetValue(true, _cancelButton, _yesButton, _noButton);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_buttonConfigs == MessageBoxButton.OK)
|
||||
{
|
||||
Close(MessageBoxResult.OK);
|
||||
}
|
||||
|
||||
Close(MessageBoxResult.Cancel);
|
||||
}
|
||||
|
||||
private void OnDefaultButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender == _okButton)
|
||||
{
|
||||
Close(MessageBoxResult.OK);
|
||||
}
|
||||
else if (sender == _cancelButton)
|
||||
{
|
||||
Close(MessageBoxResult.Cancel);
|
||||
}
|
||||
else if (sender == _yesButton)
|
||||
{
|
||||
Close(MessageBoxResult.Yes);
|
||||
}
|
||||
else if (sender == _noButton)
|
||||
{
|
||||
Close(MessageBoxResult.No);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
if (e.Key == Key.Escape && _buttonConfigs == MessageBoxButton.OK)
|
||||
{
|
||||
Close(MessageBoxResult.OK);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
294
src/Ursa/Controls/NavMenu/NavMenu.cs
Normal file
294
src/Ursa/Controls/NavMenu/NavMenu.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Metadata;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_HorizontalCollapsed)]
|
||||
public class NavMenu: ItemsControl
|
||||
{
|
||||
public const string PC_HorizontalCollapsed = ":horizontal-collapsed";
|
||||
|
||||
public static readonly StyledProperty<object?> SelectedItemProperty = AvaloniaProperty.Register<NavMenu, object?>(
|
||||
nameof(SelectedItem), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => GetValue(SelectedItemProperty);
|
||||
set => SetValue(SelectedItemProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> IconBindingProperty = AvaloniaProperty.Register<NavMenu, IBinding?>(
|
||||
nameof(IconBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? IconBinding
|
||||
{
|
||||
get => GetValue(IconBindingProperty);
|
||||
set => SetValue(IconBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> HeaderBindingProperty = AvaloniaProperty.Register<NavMenu, IBinding?>(
|
||||
nameof(HeaderBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? HeaderBinding
|
||||
{
|
||||
get => GetValue(HeaderBindingProperty);
|
||||
set => SetValue(HeaderBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> SubMenuBindingProperty = AvaloniaProperty.Register<NavMenu, IBinding?>(
|
||||
nameof(SubMenuBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? SubMenuBinding
|
||||
{
|
||||
get => GetValue(SubMenuBindingProperty);
|
||||
set => SetValue(SubMenuBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBinding?> CommandBindingProperty = AvaloniaProperty.Register<NavMenu, IBinding?>(
|
||||
nameof(CommandBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? CommandBinding
|
||||
{
|
||||
get => GetValue(CommandBindingProperty);
|
||||
set => SetValue(CommandBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty = AvaloniaProperty.Register<NavMenu, IDataTemplate?>(
|
||||
nameof(HeaderTemplate));
|
||||
|
||||
/// <summary>
|
||||
/// Header Template is used for MenuItem headers, not menu header.
|
||||
/// </summary>
|
||||
public IDataTemplate? HeaderTemplate
|
||||
{
|
||||
get => GetValue(HeaderTemplateProperty);
|
||||
set => SetValue(HeaderTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<NavMenu, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> SubMenuIndentProperty = AvaloniaProperty.Register<NavMenu, double>(
|
||||
nameof(SubMenuIndent));
|
||||
|
||||
public double SubMenuIndent
|
||||
{
|
||||
get => GetValue(SubMenuIndentProperty);
|
||||
set => SetValue(SubMenuIndentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsHorizontalCollapsedProperty = AvaloniaProperty.Register<NavMenu, bool>(
|
||||
nameof(IsHorizontalCollapsed));
|
||||
|
||||
public bool IsHorizontalCollapsed
|
||||
{
|
||||
get => GetValue(IsHorizontalCollapsedProperty);
|
||||
set => SetValue(IsHorizontalCollapsedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> HeaderProperty =
|
||||
HeaderedContentControl.HeaderProperty.AddOwner<NavMenu>();
|
||||
|
||||
public object? Header
|
||||
{
|
||||
get => GetValue(HeaderProperty);
|
||||
set => SetValue(HeaderProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> FooterProperty = AvaloniaProperty.Register<NavMenu, object?>(
|
||||
nameof(Footer));
|
||||
|
||||
public object? Footer
|
||||
{
|
||||
get => GetValue(FooterProperty);
|
||||
set => SetValue(FooterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> ExpandWidthProperty = AvaloniaProperty.Register<NavMenu, double>(
|
||||
nameof(ExpandWidth), double.NaN);
|
||||
|
||||
public double ExpandWidth
|
||||
{
|
||||
get => GetValue(ExpandWidthProperty);
|
||||
set => SetValue(ExpandWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> CollapseWidthProperty = AvaloniaProperty.Register<NavMenu, double>(
|
||||
nameof(CollapseWidth), double.NaN);
|
||||
|
||||
public double CollapseWidth
|
||||
{
|
||||
get => GetValue(CollapseWidthProperty);
|
||||
set => SetValue(CollapseWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly AttachedProperty<bool> CanToggleProperty =
|
||||
AvaloniaProperty.RegisterAttached<NavMenu, InputElement, bool>("CanToggle");
|
||||
|
||||
public static void SetCanToggle(InputElement obj, bool value) => obj.SetValue(CanToggleProperty, value);
|
||||
public static bool GetCanToggle(InputElement obj) => obj.GetValue(CanToggleProperty);
|
||||
|
||||
public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent = RoutedEvent.Register<NavMenu, SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<SelectionChangedEventArgs>? SelectionChanged
|
||||
{
|
||||
add => AddHandler(SelectionChangedEvent, value);
|
||||
remove => RemoveHandler(SelectionChangedEvent, value);
|
||||
}
|
||||
|
||||
static NavMenu()
|
||||
{
|
||||
SelectedItemProperty.Changed.AddClassHandler<NavMenu, object?>((o, e) => o.OnSelectedItemChange(e));
|
||||
IsHorizontalCollapsedProperty.AffectsPseudoClass<NavMenu>(PC_HorizontalCollapsed);
|
||||
CanToggleProperty.Changed.AddClassHandler<InputElement, bool>(OnInputRegisteredAsToggle);
|
||||
}
|
||||
|
||||
private static void OnInputRegisteredAsToggle(InputElement input, AvaloniaPropertyChangedEventArgs<bool> e)
|
||||
{
|
||||
if (e.NewValue.Value)
|
||||
{
|
||||
input.AddHandler(PointerPressedEvent, OnElementToggle);
|
||||
}
|
||||
else
|
||||
{
|
||||
input.RemoveHandler(PointerPressedEvent, OnElementToggle);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnElementToggle(object? sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is not InputElement input) return;
|
||||
var nav = input.FindLogicalAncestorOfType<NavMenu>();
|
||||
if(nav is null) return;
|
||||
bool collapsed = nav.IsHorizontalCollapsed;
|
||||
nav.IsHorizontalCollapsed = !collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// this implementation only works in the case that only leaf menu item is allowed to select. It will be changed if we introduce parent level selection in the future.
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
private void OnSelectedItemChange(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
SelectionChangedEventArgs a = new SelectionChangedEventArgs(
|
||||
SelectionChangedEvent,
|
||||
new [] { args.OldValue.Value },
|
||||
new [] { args.NewValue.Value });
|
||||
if (_updateFromUI)
|
||||
{
|
||||
RaiseEvent(a);
|
||||
return;
|
||||
}
|
||||
var newValue = args.NewValue.Value;
|
||||
if (newValue is null)
|
||||
{
|
||||
ClearAll();
|
||||
RaiseEvent(a);
|
||||
return;
|
||||
}
|
||||
var leaves = GetLeafMenus();
|
||||
bool found = false;
|
||||
foreach (var leaf in leaves)
|
||||
{
|
||||
if (leaf == newValue || leaf.DataContext == newValue)
|
||||
{
|
||||
leaf.SelectItem(leaf);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
ClearAll();
|
||||
}
|
||||
RaiseEvent(a);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<NavMenuItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new NavMenuItem();
|
||||
}
|
||||
|
||||
private bool _updateFromUI;
|
||||
|
||||
internal void SelectItem(NavMenuItem item, NavMenuItem parent)
|
||||
{
|
||||
_updateFromUI = true;
|
||||
foreach (var child in LogicalChildren)
|
||||
{
|
||||
if (child == parent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (child is NavMenuItem navMenuItem)
|
||||
{
|
||||
navMenuItem.ClearSelection();
|
||||
}
|
||||
}
|
||||
if (item.DataContext is not null && item.DataContext != this.DataContext)
|
||||
{
|
||||
SelectedItem = item.DataContext;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedItem = item;
|
||||
}
|
||||
item.BringIntoView();
|
||||
_updateFromUI = false;
|
||||
}
|
||||
|
||||
private IEnumerable<NavMenuItem> GetLeafMenus()
|
||||
{
|
||||
foreach (var child in LogicalChildren)
|
||||
{
|
||||
if (child is NavMenuItem item)
|
||||
{
|
||||
var leafs = item.GetLeafMenus();
|
||||
foreach (var leaf in leafs)
|
||||
{
|
||||
yield return leaf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearAll()
|
||||
{
|
||||
foreach (var child in LogicalChildren)
|
||||
{
|
||||
if (child is NavMenuItem item)
|
||||
{
|
||||
item.ClearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
396
src/Ursa/Controls/NavMenu/NavMenuItem.cs
Normal file
396
src/Ursa/Controls/NavMenu/NavMenuItem.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.VisualTree;
|
||||
using Irihi.Avalonia.Shared.Common;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Navigation Menu Item
|
||||
/// </summary>
|
||||
[PseudoClasses(PC_Highlighted, PC_HorizontalCollapsed, PC_VerticalCollapsed, PC_FirstLevel, PC_Selector)]
|
||||
public class NavMenuItem: HeaderedItemsControl
|
||||
{
|
||||
public const string PC_Highlighted = ":highlighted";
|
||||
public const string PC_FirstLevel = ":first-level";
|
||||
public const string PC_HorizontalCollapsed = ":horizontal-collapsed";
|
||||
public const string PC_VerticalCollapsed = ":vertical-collapsed";
|
||||
public const string PC_Selector = ":selector";
|
||||
|
||||
private NavMenu? _rootMenu;
|
||||
private Panel? _popupPanel;
|
||||
private Popup? _popup;
|
||||
private Panel? _overflowPanel;
|
||||
|
||||
private static readonly Point s_invalidPoint = new (double.NaN, double.NaN);
|
||||
private Point _pointerDownPoint = s_invalidPoint;
|
||||
|
||||
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<NavMenuItem, object?>(
|
||||
nameof(Icon));
|
||||
|
||||
public object? Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<NavMenuItem, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ICommand?> CommandProperty = Button.CommandProperty.AddOwner<NavMenuItem>();
|
||||
|
||||
public ICommand? Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> CommandParameterProperty =
|
||||
Button.CommandParameterProperty.AddOwner<NavMenuItem>();
|
||||
|
||||
public object? CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsSelectedProperty =
|
||||
SelectingItemsControl.IsSelectedProperty.AddOwner<NavMenuItem>();
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => GetValue(IsSelectedProperty);
|
||||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent = RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble);
|
||||
|
||||
private bool _isHighlighted;
|
||||
|
||||
public static readonly DirectProperty<NavMenuItem, bool> IsHighlightedProperty =
|
||||
AvaloniaProperty.RegisterDirect<NavMenuItem, bool>(
|
||||
nameof(IsHighlighted), o => o.IsHighlighted, (o, v) => o.IsHighlighted = v,
|
||||
defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool IsHighlighted
|
||||
{
|
||||
get => _isHighlighted;
|
||||
private set => SetAndRaise(IsHighlightedProperty, ref _isHighlighted, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsHorizontalCollapsedProperty =
|
||||
NavMenu.IsHorizontalCollapsedProperty.AddOwner<NavMenuItem>();
|
||||
|
||||
public bool IsHorizontalCollapsed
|
||||
{
|
||||
get => GetValue(IsHorizontalCollapsedProperty);
|
||||
set => SetValue(IsHorizontalCollapsedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsVerticalCollapsedProperty = AvaloniaProperty.Register<NavMenuItem, bool>(
|
||||
nameof(IsVerticalCollapsed));
|
||||
|
||||
public bool IsVerticalCollapsed
|
||||
{
|
||||
get => GetValue(IsVerticalCollapsedProperty);
|
||||
set => SetValue(IsVerticalCollapsedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> SubMenuIndentProperty =
|
||||
NavMenu.SubMenuIndentProperty.AddOwner<NavMenuItem>();
|
||||
|
||||
public double SubMenuIndent
|
||||
{
|
||||
get => GetValue(SubMenuIndentProperty);
|
||||
set => SetValue(SubMenuIndentProperty, value);
|
||||
}
|
||||
|
||||
internal static readonly DirectProperty<NavMenuItem, int> LevelProperty = AvaloniaProperty.RegisterDirect<NavMenuItem, int>(
|
||||
nameof(Level), o => o.Level, (o, v) => o.Level = v);
|
||||
private int _level;
|
||||
internal int Level
|
||||
{
|
||||
get => _level;
|
||||
set => SetAndRaise(LevelProperty, ref _level, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsSeparatorProperty = AvaloniaProperty.Register<NavMenuItem, bool>(
|
||||
nameof(IsSeparator));
|
||||
|
||||
public bool IsSeparator
|
||||
{
|
||||
get => GetValue(IsSeparatorProperty);
|
||||
set => SetValue(IsSeparatorProperty, value);
|
||||
}
|
||||
|
||||
static NavMenuItem()
|
||||
{
|
||||
// SelectableMixin.Attach<NavMenuItem>(IsSelectedProperty);
|
||||
PressedMixin.Attach<NavMenuItem>();
|
||||
LevelProperty.Changed.AddClassHandler<NavMenuItem, int>((item, args) => item.OnLevelChange(args));
|
||||
IsHighlightedProperty.AffectsPseudoClass<NavMenuItem>(PC_Highlighted);
|
||||
IsHorizontalCollapsedProperty.AffectsPseudoClass<NavMenuItem>(PC_HorizontalCollapsed);
|
||||
IsVerticalCollapsedProperty.AffectsPseudoClass<NavMenuItem>(PC_VerticalCollapsed);
|
||||
IsSelectedProperty.AffectsPseudoClass<NavMenuItem>(PseudoClassName.PC_Selected, IsSelectedChangedEvent);
|
||||
IsHorizontalCollapsedProperty.Changed.AddClassHandler<NavMenuItem, bool>((item, args) =>
|
||||
item.OnIsHorizontalCollapsedChanged(args));
|
||||
}
|
||||
|
||||
private void OnIsHorizontalCollapsedChanged(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
if (args.NewValue.Value)
|
||||
{
|
||||
if (this.ItemsPanelRoot is OverflowStackPanel s)
|
||||
{
|
||||
s.MoveChildrenToOverflowPanel();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.ItemsPanelRoot is OverflowStackPanel s)
|
||||
{
|
||||
s.MoveChildrenToMainPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLevelChange(AvaloniaPropertyChangedEventArgs<int> args)
|
||||
{
|
||||
PseudoClasses.Set(PC_FirstLevel, args.NewValue.Value == 1);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<NavMenuItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new NavMenuItem();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_rootMenu = GetRootMenu();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
SetCurrentValue(LevelProperty,CalculateDistanceFromLogicalParent<NavMenu>(this));
|
||||
_popup = e.NameScope.Find<Popup>("PART_Popup");
|
||||
_overflowPanel = e.NameScope.Find<Panel>("PART_OverflowPanel");
|
||||
if (_rootMenu is not null)
|
||||
{
|
||||
this.TryBind(IconProperty, _rootMenu.IconBinding);
|
||||
this.TryBind(HeaderProperty, _rootMenu.HeaderBinding);
|
||||
this.TryBind(ItemsSourceProperty, _rootMenu.SubMenuBinding);
|
||||
this.TryBind(CommandProperty, _rootMenu.CommandBinding);
|
||||
this[!IconTemplateProperty] = _rootMenu[!NavMenu.IconTemplateProperty];
|
||||
this[!HeaderTemplateProperty] = _rootMenu[!NavMenu.HeaderTemplateProperty];
|
||||
this[!SubMenuIndentProperty] = _rootMenu[!NavMenu.SubMenuIndentProperty];
|
||||
this[!IsHorizontalCollapsedProperty] = _rootMenu[!NavMenu.IsHorizontalCollapsedProperty];
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
var root = this.ItemsPanelRoot;
|
||||
if (root is OverflowStackPanel stack)
|
||||
{
|
||||
stack.OverflowPanel = _overflowPanel;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
if (IsSeparator)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
base.OnPointerPressed(e);
|
||||
if (e.Handled) return;
|
||||
|
||||
var p = e.GetCurrentPoint(this);
|
||||
if (p.Properties.PointerUpdateKind is not (PointerUpdateKind.LeftButtonPressed
|
||||
or PointerUpdateKind.RightButtonPressed)) return;
|
||||
if (p.Pointer.Type == PointerType.Mouse)
|
||||
{
|
||||
if (this.ItemCount == 0)
|
||||
{
|
||||
SelectItem(this);
|
||||
Command?.Execute(CommandParameter);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!IsHorizontalCollapsed)
|
||||
{
|
||||
SetCurrentValue(IsVerticalCollapsedProperty, !IsVerticalCollapsed);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_popup is null || e.Source is not Visual v || _popup.IsInsidePopup(v)) return;
|
||||
if (_popup.IsOpen)
|
||||
{
|
||||
_popup.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_pointerDownPoint = p.Position;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
if (!e.Handled && !double.IsNaN(_pointerDownPoint.X) &&
|
||||
e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right)
|
||||
{
|
||||
var point = e.GetCurrentPoint(this);
|
||||
if (!new Rect(Bounds.Size).ContainsExclusive(point.Position) || e.Pointer.Type != PointerType.Touch) return;
|
||||
if (this.ItemCount == 0)
|
||||
{
|
||||
SelectItem(this);
|
||||
Command?.Execute(CommandParameter);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!IsHorizontalCollapsed)
|
||||
{
|
||||
SetCurrentValue(IsVerticalCollapsedProperty, !IsVerticalCollapsed);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_popup is null || e.Source is not Visual v || _popup.IsInsidePopup(v)) return;
|
||||
if (_popup.IsOpen)
|
||||
{
|
||||
_popup.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SelectItem(NavMenuItem item)
|
||||
{
|
||||
if (item == this)
|
||||
{
|
||||
SetCurrentValue(IsSelectedProperty, true);
|
||||
SetCurrentValue(IsHighlightedProperty, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCurrentValue(IsSelectedProperty, false);
|
||||
SetCurrentValue(IsHighlightedProperty, true);
|
||||
}
|
||||
if (this.Parent is NavMenuItem menuItem)
|
||||
{
|
||||
menuItem.SelectItem(item);
|
||||
var items = menuItem.LogicalChildren.OfType<NavMenuItem>();
|
||||
foreach (var child in items)
|
||||
{
|
||||
if (child != this)
|
||||
{
|
||||
child.ClearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.Parent is NavMenu menu)
|
||||
{
|
||||
menu.SelectItem(item, this);
|
||||
}
|
||||
if(_popup is not null)
|
||||
{
|
||||
_popup.Close();
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearSelection()
|
||||
{
|
||||
SetCurrentValue(IsHighlightedProperty, false);
|
||||
SetCurrentValue(IsSelectedProperty, false);
|
||||
foreach (var child in LogicalChildren)
|
||||
{
|
||||
if (child is NavMenuItem item)
|
||||
{
|
||||
item.ClearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NavMenu? GetRootMenu()
|
||||
{
|
||||
var root = this.FindAncestorOfType<NavMenu>() ?? this.FindLogicalAncestorOfType<NavMenu>();
|
||||
return root;
|
||||
}
|
||||
|
||||
private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class
|
||||
{
|
||||
var result = 0;
|
||||
|
||||
while (logical != null && !(logical is T))
|
||||
{
|
||||
if (logical is NavMenuItem)
|
||||
{
|
||||
result++;
|
||||
}
|
||||
logical = logical.LogicalParent;
|
||||
}
|
||||
|
||||
return logical != null ? result : @default;
|
||||
}
|
||||
|
||||
internal IEnumerable<NavMenuItem> GetLeafMenus()
|
||||
{
|
||||
if (this.ItemCount == 0)
|
||||
{
|
||||
yield return this;
|
||||
yield break;
|
||||
}
|
||||
foreach (var child in LogicalChildren)
|
||||
{
|
||||
if (child is NavMenuItem item)
|
||||
{
|
||||
var items = item.GetLeafMenus();
|
||||
foreach (var i in items)
|
||||
{
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Ursa/Controls/NavMenu/OverflowStackPanel.cs
Normal file
30
src/Ursa/Controls/NavMenu/OverflowStackPanel.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class OverflowStackPanel: StackPanel
|
||||
{
|
||||
public Panel? OverflowPanel { get; set; }
|
||||
public void MoveChildrenToOverflowPanel()
|
||||
{
|
||||
var children = this.Children.ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
Children.Remove(child);
|
||||
OverflowPanel?.Children.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveChildrenToMainPanel()
|
||||
{
|
||||
var children = this.OverflowPanel?.Children.ToList();
|
||||
if (children != null && children.Count > 0)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
OverflowPanel?.Children.Remove(child);
|
||||
Children.Add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml.Templates;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Closed)]
|
||||
[TemplatePart(Name = PART_CloseButton, Type = typeof(ToggleButton))]
|
||||
|
||||
public class NavigationMenu: HeaderedItemsControl
|
||||
{
|
||||
public const string PC_Closed = ":closed";
|
||||
public const string PART_CloseButton = "PART_CloseButton";
|
||||
|
||||
public static readonly StyledProperty<object?> FooterProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
|
||||
nameof(Footer));
|
||||
|
||||
public object? Footer
|
||||
{
|
||||
get => GetValue(FooterProperty);
|
||||
set => SetValue(FooterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate> FooterTemplateProperty = AvaloniaProperty.Register<NavigationMenu, IDataTemplate>(
|
||||
nameof(FooterTemplate));
|
||||
|
||||
public IDataTemplate FooterTemplate
|
||||
{
|
||||
get => GetValue(FooterTemplateProperty);
|
||||
set => SetValue(FooterTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
|
||||
nameof(Icon));
|
||||
|
||||
public object? Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly StyledProperty<object?> SelectedItemProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
|
||||
nameof(SelectedItem));
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => GetValue(SelectedItemProperty);
|
||||
set => SetValue(SelectedItemProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowCollapseButtonProperty = AvaloniaProperty.Register<NavigationMenu, bool>(
|
||||
nameof(ShowCollapseButton));
|
||||
|
||||
public bool ShowCollapseButton
|
||||
{
|
||||
get => GetValue(ShowCollapseButtonProperty);
|
||||
set => SetValue(ShowCollapseButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsClosedProperty = AvaloniaProperty.Register<NavigationMenu, bool>(
|
||||
nameof(IsClosed));
|
||||
|
||||
public bool IsClosed
|
||||
{
|
||||
get => GetValue(IsClosedProperty);
|
||||
set => SetValue(IsClosedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> OpenedWidthProperty = AvaloniaProperty.Register<NavigationMenu, double>(
|
||||
nameof(OpenedWidth));
|
||||
|
||||
public double OpenedWidth
|
||||
{
|
||||
get => GetValue(OpenedWidthProperty);
|
||||
set => SetValue(OpenedWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> ClosedWidthProperty = AvaloniaProperty.Register<NavigationMenu, double>(
|
||||
nameof(ClosedWidth));
|
||||
|
||||
public double ClosedWidth
|
||||
{
|
||||
get => GetValue(ClosedWidthProperty);
|
||||
set => SetValue(ClosedWidthProperty, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static NavigationMenu()
|
||||
{
|
||||
SelectedItemProperty.Changed.AddClassHandler<NavigationMenu>((o, e) => o.OnSelectionItemChanged(e));
|
||||
IsClosedProperty.Changed.AddClassHandler<NavigationMenu>((o,e)=>o.OnIsClosedChanged(e));
|
||||
}
|
||||
|
||||
private void OnSelectionItemChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
var newItem = args.GetNewValue<object?>();
|
||||
if (newItem is not null)
|
||||
{
|
||||
UpdateSelectionFromSelectedItem(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIsClosedChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
bool newValue = args.GetNewValue<bool>();
|
||||
PseudoClasses.Set(PC_Closed, newValue);
|
||||
}
|
||||
|
||||
internal void UpdateSelection(NavigationMenuItem source)
|
||||
{
|
||||
var children = this.ItemsPanelRoot?.Children;
|
||||
if (children is not null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
NavigationMenuItem? item = NavigationMenuItem.GetMenuItemFromControl(child);
|
||||
if (item != null)
|
||||
{
|
||||
if(Equals(item, source)) continue;
|
||||
item.SetSelection(null, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateSelectionFromSelectedItem(object? o)
|
||||
{
|
||||
var children = this.ItemsPanelRoot?.Children;
|
||||
if (children is not null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
NavigationMenuItem? item = NavigationMenuItem.GetMenuItemFromControl(child);
|
||||
if(item is null) continue;
|
||||
item.UpdateSelectionFromSelectedItem(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml.Templates;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Closed, PC_Selected, PC_Highlighted, PC_Collapsed, PC_TopLevel)]
|
||||
[TemplatePart(PART_Popup, typeof(Popup))]
|
||||
public class NavigationMenuItem: HeaderedSelectingItemsControl
|
||||
{
|
||||
public const string PC_Closed = ":closed";
|
||||
public const string PC_Selected = ":selected";
|
||||
public const string PC_Highlighted= ":highlighted";
|
||||
public const string PC_Collapsed = ":collapsed";
|
||||
public const string PC_TopLevel = ":top-level";
|
||||
public const string PART_Popup = "PART_Popup";
|
||||
|
||||
private NavigationMenu? _rootMenu;
|
||||
private IDisposable? _ownerSubscription;
|
||||
private IDisposable? _itemsBinding;
|
||||
private bool _isCollapsed;
|
||||
private Popup? _popup;
|
||||
|
||||
public static readonly StyledProperty<bool> IsClosedProperty = AvaloniaProperty.Register<NavigationMenuItem, bool>(
|
||||
nameof(IsClosed));
|
||||
|
||||
public bool IsClosed
|
||||
{
|
||||
get => GetValue(IsClosedProperty);
|
||||
set => SetValue(IsClosedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<NavigationMenuItem, object?>(
|
||||
nameof(Icon));
|
||||
|
||||
public object? Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate> IconTemplateProperty = AvaloniaProperty.Register<NavigationMenuItem, IDataTemplate>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IDataTemplate IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<NavigationMenuItem, int> LevelProperty = AvaloniaProperty.RegisterDirect<NavigationMenuItem, int>(
|
||||
nameof(Level), o => o.Level);
|
||||
private int _level;
|
||||
public int Level
|
||||
{
|
||||
get => _level;
|
||||
private set => SetAndRaise(LevelProperty, ref _level, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ICommand> CommandProperty = AvaloniaProperty.Register<NavigationMenuItem, ICommand>(
|
||||
nameof(Command));
|
||||
|
||||
public ICommand Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> CommandParameterProperty = AvaloniaProperty.Register<NavigationMenuItem, object?>(
|
||||
nameof(CommandParameter));
|
||||
|
||||
public object? CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<NavigationMenuItem, bool> IsTopLevelMenuItemProperty = AvaloniaProperty.RegisterDirect<NavigationMenuItem, bool>(
|
||||
nameof(IsTopLevelMenuItem), o => o.IsTopLevelMenuItem, (o, v) => o.IsTopLevelMenuItem = v);
|
||||
private bool _isTopLevelMenuItem;
|
||||
public bool IsTopLevelMenuItem
|
||||
{
|
||||
get => _isTopLevelMenuItem;
|
||||
set => SetAndRaise(IsTopLevelMenuItemProperty, ref _isTopLevelMenuItem, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsPopupOpenProperty = AvaloniaProperty.Register<NavigationMenuItem, bool>(
|
||||
nameof(IsPopupOpen));
|
||||
|
||||
public bool IsPopupOpen
|
||||
{
|
||||
get => GetValue(IsPopupOpenProperty);
|
||||
set => SetValue(IsPopupOpenProperty, value);
|
||||
}
|
||||
|
||||
static NavigationMenuItem()
|
||||
{
|
||||
IsClosedProperty.Changed.AddClassHandler<NavigationMenuItem>((o, e) => o.OnIsClosedChanged(e));
|
||||
PressedMixin.Attach<NavigationMenuItem>();
|
||||
}
|
||||
|
||||
private void OnIsClosedChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
bool newValue = args.GetNewValue<bool>();
|
||||
PseudoClasses.Set(PC_Closed, newValue);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
GetRootMenu();
|
||||
if (ItemTemplate == null && _rootMenu?.ItemTemplate != null)
|
||||
{
|
||||
SetCurrentValue(ItemTemplateProperty, _rootMenu.ItemTemplate);
|
||||
}
|
||||
if (ItemContainerTheme == null && _rootMenu?.ItemContainerTheme != null)
|
||||
{
|
||||
SetCurrentValue(ItemContainerThemeProperty, _rootMenu.ItemContainerTheme);
|
||||
}
|
||||
|
||||
if (_rootMenu is not null)
|
||||
{
|
||||
// IsClosed = _rootMenu.IsClosed;
|
||||
}
|
||||
|
||||
_rootMenu?.GetObservable(NavigationMenu.IsClosedProperty)
|
||||
.Subscribe(new AnonymousObserver<bool>(a => this.IsClosed = a));
|
||||
_rootMenu?.UpdateSelectionFromSelectedItem(_rootMenu.SelectedItem);
|
||||
_popup = e.NameScope.Find<Popup>(PART_Popup);
|
||||
Level = CalculateDistanceFromLogicalParent<NavigationMenu>(this) - 1;
|
||||
bool isTopLevel = Level == 0;
|
||||
IsTopLevelMenuItem = isTopLevel;
|
||||
PseudoClasses.Set(PC_TopLevel, isTopLevel);
|
||||
}
|
||||
|
||||
private void GetRootMenu()
|
||||
{
|
||||
_rootMenu = this.FindAncestorOfType<NavigationMenu>();
|
||||
if (_rootMenu is null)
|
||||
{
|
||||
var parents = this.FindLogicalAncestorOfType<NavigationMenu>();
|
||||
if (parents is not null)
|
||||
{
|
||||
_rootMenu = parents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
// Leaf menu node, can be selected.
|
||||
if (this.ItemCount == 0)
|
||||
{
|
||||
if (_rootMenu is not null )
|
||||
{
|
||||
object? o = this.DataContext == _rootMenu.DataContext ? this : this.DataContext ?? this;
|
||||
_rootMenu.SelectedItem = o;
|
||||
}
|
||||
SetSelection(this, true, true);
|
||||
}
|
||||
// Non-leaf node, act as a toggle button.
|
||||
else
|
||||
{
|
||||
_isCollapsed = !_isCollapsed;
|
||||
this.PseudoClasses.Set(PC_Collapsed, _isCollapsed);
|
||||
if (_popup is not null)
|
||||
{
|
||||
_popup.IsOpen = !_popup.IsOpen;
|
||||
}
|
||||
}
|
||||
e.Handled = true;
|
||||
Command?.Execute(CommandParameter);
|
||||
}
|
||||
|
||||
internal void SetSelection(NavigationMenuItem? source, bool selected, bool propagateToParent = false)
|
||||
{
|
||||
if (Equals(this, source) && this.ItemCount == 0)
|
||||
{
|
||||
this.PseudoClasses.Set(PC_Highlighted, selected);
|
||||
this.PseudoClasses.Set(PC_Selected, selected);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.PseudoClasses.Set(PC_Selected, false);
|
||||
this.PseudoClasses.Set(PC_Highlighted, selected);
|
||||
}
|
||||
var children = this.ItemsPanelRoot?.Children;
|
||||
if (children is not null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
NavigationMenuItem? item = GetMenuItemFromControl(child);
|
||||
if (item != null)
|
||||
{
|
||||
if(Equals(item, source)) continue;
|
||||
item.SetSelection(this, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (propagateToParent)
|
||||
{
|
||||
var parent = this.FindLogicalAncestorOfType<NavigationMenuItem>();
|
||||
if (parent != null)
|
||||
{
|
||||
parent.SetSelection(this, selected, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (selected && source!=null)
|
||||
{
|
||||
_rootMenu?.UpdateSelection(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateSelectionFromSelectedItem(object? o)
|
||||
{
|
||||
if (o is null)
|
||||
{
|
||||
this.SetSelection(this, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Equals(this, o) || Equals(this.DataContext, o))
|
||||
{
|
||||
this.SetSelection(this, true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var children = this.ItemsPanelRoot?.Children;
|
||||
if (children is not null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
NavigationMenuItem? item = GetMenuItemFromControl(child);
|
||||
if (item != null)
|
||||
{
|
||||
item.UpdateSelectionFromSelectedItem(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class
|
||||
{
|
||||
var result = 0;
|
||||
|
||||
while (logical != null && !(logical is T))
|
||||
{
|
||||
if (logical is NavigationMenuItem)
|
||||
{
|
||||
result++;
|
||||
}
|
||||
logical = logical.LogicalParent;
|
||||
}
|
||||
|
||||
return logical != null ? result : @default;
|
||||
}
|
||||
|
||||
public static NavigationMenuItem? GetMenuItemFromControl(Control? control)
|
||||
{
|
||||
if (control is null) return null;
|
||||
if (control is NavigationMenuItem item) return item;
|
||||
if (control is ContentPresenter { Child: NavigationMenuItem item2 }) return item2;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class NavigationMenuSeparator: NavigationMenuItem
|
||||
{
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
109
src/Ursa/Controls/NumPad/NumPad.cs
Normal file
109
src/Ursa/Controls/NumPad/NumPad.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class NumPad: TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<InputElement?> TargetProperty = AvaloniaProperty.Register<NumPad, InputElement?>(
|
||||
nameof(Target));
|
||||
|
||||
public InputElement? Target
|
||||
{
|
||||
get => GetValue(TargetProperty);
|
||||
set => SetValue(TargetProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> NumModeProperty = AvaloniaProperty.Register<NumPad, bool>(
|
||||
nameof(NumMode), defaultValue: true);
|
||||
|
||||
public bool NumMode
|
||||
{
|
||||
get => GetValue(NumModeProperty);
|
||||
set => SetValue(NumModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly AttachedProperty<bool> AttachProperty =
|
||||
AvaloniaProperty.RegisterAttached<NumPad, InputElement, bool>("Attach");
|
||||
|
||||
public static void SetAttach(InputElement obj, bool value) => obj.SetValue(AttachProperty, value);
|
||||
public static bool GetAttach(InputElement obj) => obj.GetValue(AttachProperty);
|
||||
|
||||
static NumPad()
|
||||
{
|
||||
AttachProperty.Changed.AddClassHandler<InputElement, bool>(OnAttachNumPad);
|
||||
}
|
||||
|
||||
private static void OnAttachNumPad(InputElement input, AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
if (args.NewValue.Value)
|
||||
{
|
||||
GotFocusEvent.AddHandler(OnTargetGotFocus, input);
|
||||
}
|
||||
else
|
||||
{
|
||||
GotFocusEvent.RemoveHandler(OnTargetGotFocus, input);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnTargetGotFocus(object sender, GotFocusEventArgs e)
|
||||
{
|
||||
if (sender is not InputElement) return;
|
||||
var existing = OverlayDialog.Recall<NumPad>(null);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.Target = sender as InputElement;
|
||||
return;
|
||||
}
|
||||
var numPad = new NumPad() { Target = sender as InputElement };
|
||||
OverlayDialog.Show(numPad, new object(), options: new OverlayDialogOptions() { Buttons = DialogButton.None });
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Key, string> KeyInputMapping = new()
|
||||
{
|
||||
[Key.NumPad0] = "0",
|
||||
[Key.NumPad1] = "1",
|
||||
[Key.NumPad2] = "2",
|
||||
[Key.NumPad3] = "3",
|
||||
[Key.NumPad4] = "4",
|
||||
[Key.NumPad5] = "5",
|
||||
[Key.NumPad6] = "6",
|
||||
[Key.NumPad7] = "7",
|
||||
[Key.NumPad8] = "8",
|
||||
[Key.NumPad9] = "9",
|
||||
[Key.Add] = "+",
|
||||
[Key.Subtract] = "-",
|
||||
[Key.Multiply] = "*",
|
||||
[Key.Divide] = "/",
|
||||
[Key.Decimal] = ".",
|
||||
};
|
||||
|
||||
public void ProcessClick(object o)
|
||||
{
|
||||
if (Target is null || o is not NumPadButton b) return;
|
||||
var key = (b.NumMode ? b.NumKey : b.FunctionKey)?? Key.None;
|
||||
if (KeyInputMapping.TryGetValue(key, out string s))
|
||||
{
|
||||
Target.RaiseEvent(new TextInputEventArgs()
|
||||
{
|
||||
Source = this,
|
||||
RoutedEvent = TextInputEvent,
|
||||
Text = s,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Target.RaiseEvent(new KeyEventArgs()
|
||||
{
|
||||
Source = this,
|
||||
RoutedEvent = KeyDownEvent,
|
||||
Key = key,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Ursa/Controls/NumPad/NumPadButton.cs
Normal file
54
src/Ursa/Controls/NumPad/NumPadButton.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class NumPadButton: RepeatButton
|
||||
{
|
||||
public static readonly StyledProperty<Key?> NumKeyProperty = AvaloniaProperty.Register<NumPadButton, Key?>(
|
||||
nameof(NumKey));
|
||||
|
||||
public Key? NumKey
|
||||
{
|
||||
get => GetValue(NumKeyProperty);
|
||||
set => SetValue(NumKeyProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Key?> FunctionKeyProperty = AvaloniaProperty.Register<NumPadButton, Key?>(
|
||||
nameof(FunctionKey));
|
||||
|
||||
public Key? FunctionKey
|
||||
{
|
||||
get => GetValue(FunctionKeyProperty);
|
||||
set => SetValue(FunctionKeyProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> NumModeProperty = AvaloniaProperty.Register<NumPadButton, bool>(
|
||||
nameof(NumMode));
|
||||
|
||||
public bool NumMode
|
||||
{
|
||||
get => GetValue(NumModeProperty);
|
||||
set => SetValue(NumModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> NumContentProperty = AvaloniaProperty.Register<NumPadButton, object?>(
|
||||
nameof(NumContent));
|
||||
|
||||
public object? NumContent
|
||||
{
|
||||
get => GetValue(NumContentProperty);
|
||||
set => SetValue(NumContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> FunctionContentProperty = AvaloniaProperty.Register<NumPadButton, object?>(
|
||||
nameof(FunctionContent));
|
||||
|
||||
public object? FunctionContent
|
||||
{
|
||||
get => GetValue(FunctionContentProperty);
|
||||
set => SetValue(FunctionContentProperty, value);
|
||||
}
|
||||
|
||||
}
|
||||
81
src/Ursa/Controls/NumberDisplayer/Implementations.cs
Normal file
81
src/Ursa/Controls/NumberDisplayer/Implementations.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Avalonia.Animation;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class Int32Displayer : NumberDisplayer<int>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase);
|
||||
|
||||
protected override InterpolatingAnimator<int> GetAnimator()
|
||||
{
|
||||
return new IntAnimator();
|
||||
}
|
||||
|
||||
private class IntAnimator : InterpolatingAnimator<int>
|
||||
{
|
||||
public override int Interpolate(double progress, int oldValue, int newValue)
|
||||
{
|
||||
return oldValue + (int)((newValue - oldValue) * progress);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetString(int value)
|
||||
{
|
||||
return value.ToString(StringFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public class DoubleDisplayer : NumberDisplayer<double>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase);
|
||||
|
||||
protected override InterpolatingAnimator<double> GetAnimator()
|
||||
{
|
||||
return new DoubleAnimator();
|
||||
}
|
||||
|
||||
private class DoubleAnimator : InterpolatingAnimator<double>
|
||||
{
|
||||
public override double Interpolate(double progress, double oldValue, double newValue)
|
||||
{
|
||||
return oldValue + (newValue - oldValue) * progress;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetString(double value)
|
||||
{
|
||||
return value.ToString(StringFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public class DateDisplay : NumberDisplayer<DateTime>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase);
|
||||
|
||||
protected override InterpolatingAnimator<DateTime> GetAnimator()
|
||||
{
|
||||
return new DateAnimator();
|
||||
}
|
||||
|
||||
private class DateAnimator : InterpolatingAnimator<DateTime>
|
||||
{
|
||||
public override DateTime Interpolate(double progress, DateTime oldValue, DateTime newValue)
|
||||
{
|
||||
var diff = (newValue - oldValue).TotalSeconds;
|
||||
try
|
||||
{
|
||||
return oldValue + TimeSpan.FromSeconds(diff * progress);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetString(DateTime value)
|
||||
{
|
||||
return value.ToString(StringFormat);
|
||||
}
|
||||
}
|
||||
133
src/Ursa/Controls/NumberDisplayer/NumberDisplayerBase.cs
Normal file
133
src/Ursa/Controls/NumberDisplayer/NumberDisplayerBase.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public abstract class NumberDisplayerBase : TemplatedControl
|
||||
{
|
||||
public static readonly DirectProperty<NumberDisplayerBase, string> InternalTextProperty = AvaloniaProperty.RegisterDirect<NumberDisplayerBase, string>(
|
||||
nameof(InternalText), o => o.InternalText, (o, v) => o.InternalText = v);
|
||||
private string _internalText;
|
||||
|
||||
internal string InternalText
|
||||
{
|
||||
get => _internalText;
|
||||
set => SetAndRaise(InternalTextProperty, ref _internalText, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> DurationProperty = AvaloniaProperty.Register<NumberDisplayerBase, TimeSpan>(
|
||||
nameof(Duration));
|
||||
|
||||
public TimeSpan Duration
|
||||
{
|
||||
get => GetValue(DurationProperty);
|
||||
set => SetValue(DurationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string> StringFormatProperty = AvaloniaProperty.Register<NumberDisplayerBase, string>(
|
||||
nameof(StringFormat));
|
||||
|
||||
public string StringFormat
|
||||
{
|
||||
get => GetValue(StringFormatProperty);
|
||||
set => SetValue(StringFormatProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsSelectableProperty = AvaloniaProperty.Register<NumberDisplayerBase, bool>(
|
||||
nameof(IsSelectable));
|
||||
|
||||
public bool IsSelectable
|
||||
{
|
||||
get => GetValue(IsSelectableProperty);
|
||||
set => SetValue(IsSelectableProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class NumberDisplayer<T>: NumberDisplayerBase
|
||||
{
|
||||
private Animation? _animation;
|
||||
private CancellationTokenSource _cts = new ();
|
||||
|
||||
public static readonly StyledProperty<T?> ValueProperty = AvaloniaProperty.Register<NumberDisplayer<T>, T?>(
|
||||
nameof(Value), defaultBindingMode:BindingMode.TwoWay);
|
||||
|
||||
public T? Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
private static readonly StyledProperty<T?> InternalValueProperty = AvaloniaProperty.Register<NumberDisplayer<T>, T?>(
|
||||
nameof(InternalValue), defaultBindingMode:BindingMode.TwoWay);
|
||||
|
||||
private T? InternalValue
|
||||
{
|
||||
get => GetValue(InternalValueProperty);
|
||||
set => SetValue(InternalValueProperty, value);
|
||||
}
|
||||
|
||||
static NumberDisplayer()
|
||||
{
|
||||
ValueProperty.Changed.AddClassHandler<NumberDisplayer<T>, T?>((item, args) =>
|
||||
{
|
||||
item.OnValueChanged(args.OldValue.Value, args.NewValue.Value);
|
||||
});
|
||||
InternalValueProperty.Changed.AddClassHandler<NumberDisplayer<T>, T?>((item, args) =>
|
||||
{
|
||||
item.InternalText = args.NewValue.Value is null ? string.Empty : item.GetString(args.NewValue.Value);
|
||||
});
|
||||
DurationProperty.Changed.AddClassHandler<NumberDisplayer<T>, TimeSpan>((item, args) =>item.OnDurationChanged(args));
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_animation = new Animation
|
||||
{
|
||||
Duration = Duration,
|
||||
FillMode = FillMode.Forward
|
||||
};
|
||||
_animation.Children.Add(new KeyFrame()
|
||||
{
|
||||
Cue = new Cue(0.0),
|
||||
Setters = { new Setter{Property = InternalValueProperty } }
|
||||
});
|
||||
_animation.Children.Add(new KeyFrame()
|
||||
{
|
||||
Cue = new Cue(1.0),
|
||||
Setters = { new Setter{Property = InternalValueProperty } }
|
||||
});
|
||||
Animation.SetAnimator(_animation.Children[0].Setters[0], GetAnimator());
|
||||
Animation.SetAnimator(_animation.Children[1].Setters[0], GetAnimator());
|
||||
|
||||
// Display value directly to text on initialization in case value equals to default.
|
||||
SetCurrentValue(InternalTextProperty, this.GetString(Value));
|
||||
}
|
||||
|
||||
private void OnDurationChanged(AvaloniaPropertyChangedEventArgs<TimeSpan> args)
|
||||
{
|
||||
if (_animation is null) return;
|
||||
_animation.Duration = args.NewValue.Value;
|
||||
}
|
||||
|
||||
private void OnValueChanged(T? oldValue, T? newValue)
|
||||
{
|
||||
if (_animation is null)
|
||||
{
|
||||
SetCurrentValue(InternalValueProperty, newValue);
|
||||
return;
|
||||
}
|
||||
_cts.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
(_animation.Children[0].Setters[0] as Setter)!.Value = oldValue;
|
||||
(_animation.Children[1].Setters[0] as Setter)!.Value = newValue;
|
||||
_animation.RunAsync(this, _cts.Token);
|
||||
}
|
||||
|
||||
protected abstract InterpolatingAnimator<T> GetAnimator();
|
||||
|
||||
protected abstract string GetString(T? value);
|
||||
}
|
||||
322
src/Ursa/Controls/NumericUpDown/IntUpDown.cs
Normal file
322
src/Ursa/Controls/NumericUpDown/IntUpDown.cs
Normal file
@@ -0,0 +1,322 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class NumericIntUpDown : NumericUpDownBase<int>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericIntUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericIntUpDown>(int.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericIntUpDown>(int.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericIntUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out int number) =>
|
||||
int.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(int? value)
|
||||
{
|
||||
return value?.ToString(FormatString, NumberFormat);
|
||||
}
|
||||
|
||||
protected override int Zero => 0;
|
||||
|
||||
protected override int? Add(int? a, int? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return result < Value ? Maximum : result;
|
||||
}
|
||||
|
||||
protected override int? Minus(int? a, int? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return result > Value ? Minimum : result;
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericUIntUpDown : NumericUpDownBase<uint>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericUIntUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericUIntUpDown>(uint.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericUIntUpDown>(uint.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericUIntUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out uint number)
|
||||
{
|
||||
return uint.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
}
|
||||
|
||||
protected override string? ValueToString(uint? value)
|
||||
{
|
||||
return value?.ToString(FormatString, NumberFormat);
|
||||
}
|
||||
|
||||
protected override uint Zero => 0;
|
||||
|
||||
protected override uint? Add(uint? a, uint? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return result < Value ? Maximum : result;
|
||||
}
|
||||
|
||||
protected override uint? Minus(uint? a, uint? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return result > Value ? Minimum : result;
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericDoubleUpDown : NumericUpDownBase<double>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericDoubleUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericDoubleUpDown>(double.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericDoubleUpDown>(double.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericDoubleUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out double number) =>
|
||||
double.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(double? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override double Zero => 0;
|
||||
|
||||
protected override double? Add(double? a, double? b) => a + b;
|
||||
|
||||
protected override double? Minus(double? a, double? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericByteUpDown : NumericUpDownBase<byte>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericByteUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericByteUpDown>(byte.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericByteUpDown>(byte.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericByteUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out byte number) =>
|
||||
byte.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(byte? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override byte Zero => 0;
|
||||
|
||||
protected override byte? Add(byte? a, byte? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return (byte?)(result < Value ? Maximum : result);
|
||||
}
|
||||
|
||||
protected override byte? Minus(byte? a, byte? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return (byte?)(result > Value ? Minimum : result);
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericSByteUpDown : NumericUpDownBase<sbyte>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericSByteUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericSByteUpDown>(sbyte.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericSByteUpDown>(sbyte.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericSByteUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out sbyte number) =>
|
||||
sbyte.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(sbyte? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override sbyte Zero => 0;
|
||||
|
||||
protected override sbyte? Add(sbyte? a, sbyte? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return (sbyte?)(result < Value ? Maximum : result);
|
||||
}
|
||||
|
||||
protected override sbyte? Minus(sbyte? a, sbyte? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return (sbyte?)(result > Value ? Minimum : result);
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericShortUpDown : NumericUpDownBase<short>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericShortUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericShortUpDown>(short.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericShortUpDown>(short.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericShortUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out short number) =>
|
||||
short.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(short? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override short Zero => 0;
|
||||
|
||||
protected override short? Add(short? a, short? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return (short?)(result < Value ? Maximum : result);
|
||||
}
|
||||
|
||||
protected override short? Minus(short? a, short? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return (short?)(result > Value ? Minimum : result);
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericUShortUpDown : NumericUpDownBase<ushort>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericUShortUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericUShortUpDown>(ushort.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericUShortUpDown>(ushort.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericUShortUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out ushort number) =>
|
||||
ushort.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(ushort? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override ushort Zero => 0;
|
||||
|
||||
protected override ushort? Add(ushort? a, ushort? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return (ushort?)(result < Value ? Maximum : result);
|
||||
}
|
||||
|
||||
protected override ushort? Minus(ushort? a, ushort? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return (ushort?)(result > Value ? Minimum : result);
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericLongUpDown : NumericUpDownBase<long>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericLongUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericLongUpDown>(long.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericLongUpDown>(long.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericLongUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out long number) =>
|
||||
long.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(long? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override long Zero => 0;
|
||||
|
||||
protected override long? Add(long? a, long? b)
|
||||
{
|
||||
var result = a + b;
|
||||
return result < Value ? Maximum : result;
|
||||
}
|
||||
|
||||
protected override long? Minus(long? a, long? b)
|
||||
{
|
||||
var result = a - b;
|
||||
return result > Value ? Minimum : result;
|
||||
}
|
||||
}
|
||||
|
||||
public class NumericULongUpDown : NumericUpDownBase<ulong>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericULongUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericULongUpDown>(ulong.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericULongUpDown>(ulong.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericULongUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out ulong number) =>
|
||||
ulong.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(ulong? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override ulong Zero => 0;
|
||||
|
||||
protected override ulong? Add(ulong? a, ulong? b) => a + b;
|
||||
|
||||
protected override ulong? Minus(ulong? a, ulong? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericFloatUpDown : NumericUpDownBase<float>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericFloatUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericFloatUpDown>(float.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericFloatUpDown>(float.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericFloatUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out float number) =>
|
||||
float.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(float? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override float Zero => 0;
|
||||
|
||||
protected override float? Add(float? a, float? b) => a + b;
|
||||
|
||||
protected override float? Minus(float? a, float? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericDecimalUpDown : NumericUpDownBase<decimal>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericDecimalUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericDecimalUpDown>(decimal.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericDecimalUpDown>(decimal.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericDecimalUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out decimal number) =>
|
||||
decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(decimal? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override decimal Zero => 0;
|
||||
|
||||
protected override decimal? Add(decimal? a, decimal? b) => a + b;
|
||||
|
||||
protected override decimal? Minus(decimal? a, decimal? b) => a - b;
|
||||
}
|
||||
751
src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs
Normal file
751
src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs
Normal file
@@ -0,0 +1,751 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net.Mime;
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_Spinner, typeof(ButtonSpinner))]
|
||||
[TemplatePart(PART_TextBox, typeof(TextBox))]
|
||||
[TemplatePart(PART_DragPanel, typeof(Panel))]
|
||||
public abstract class NumericUpDown : TemplatedControl, IClearControl
|
||||
{
|
||||
public const string PART_Spinner = "PART_Spinner";
|
||||
public const string PART_TextBox = "PART_TextBox";
|
||||
public const string PART_DragPanel = "PART_DragPanel";
|
||||
|
||||
protected internal ButtonSpinner? _spinner;
|
||||
protected internal TextBox? _textBox;
|
||||
protected internal Panel? _dragPanel;
|
||||
|
||||
private Point? _point;
|
||||
protected internal bool _updateFromTextInput;
|
||||
|
||||
|
||||
protected internal bool _canIncrease = true;
|
||||
|
||||
protected internal bool _canDecrease = true;
|
||||
|
||||
|
||||
public static readonly StyledProperty<bool> AllowDragProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
|
||||
nameof(AllowDrag), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool AllowDrag
|
||||
{
|
||||
get => GetValue(AllowDragProperty);
|
||||
set => SetValue(AllowDragProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsReadOnlyProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
|
||||
nameof(IsReadOnly), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool IsReadOnly
|
||||
{
|
||||
get => GetValue(IsReadOnlyProperty);
|
||||
set => SetValue(IsReadOnlyProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
|
||||
ContentControl.HorizontalContentAlignmentProperty.AddOwner<NumericUpDown>();
|
||||
public HorizontalAlignment HorizontalContentAlignment
|
||||
{
|
||||
get => GetValue(HorizontalContentAlignmentProperty);
|
||||
set => SetValue(HorizontalContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerLeftContentProperty = AvaloniaProperty.Register<NumericUpDown, object?>(
|
||||
nameof(InnerLeftContent));
|
||||
|
||||
public object? InnerLeftContent
|
||||
{
|
||||
get => GetValue(InnerLeftContentProperty);
|
||||
set => SetValue(InnerLeftContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<NumericUpDown, string?>(
|
||||
nameof(Watermark));
|
||||
|
||||
public string? Watermark
|
||||
{
|
||||
get => GetValue(WatermarkProperty);
|
||||
set => SetValue(WatermarkProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<NumberFormatInfo?> NumberFormatProperty = AvaloniaProperty.Register<NumericUpDown, NumberFormatInfo?>(
|
||||
nameof(NumberFormat), defaultValue: NumberFormatInfo.CurrentInfo);
|
||||
|
||||
public NumberFormatInfo? NumberFormat
|
||||
{
|
||||
get => GetValue(NumberFormatProperty);
|
||||
set => SetValue(NumberFormatProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string> FormatStringProperty = AvaloniaProperty.Register<NumericUpDown, string>(
|
||||
nameof(FormatString), string.Empty);
|
||||
|
||||
public string FormatString
|
||||
{
|
||||
get => GetValue(FormatStringProperty);
|
||||
set => SetValue(FormatStringProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<NumberStyles> ParsingNumberStyleProperty = AvaloniaProperty.Register<NumericUpDown, NumberStyles>(
|
||||
nameof(ParsingNumberStyle), defaultValue: NumberStyles.Any);
|
||||
|
||||
public NumberStyles ParsingNumberStyle
|
||||
{
|
||||
get => GetValue(ParsingNumberStyleProperty);
|
||||
set => SetValue(ParsingNumberStyleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IValueConverter?> TextConverterProperty = AvaloniaProperty.Register<NumericUpDown, IValueConverter?>(
|
||||
nameof(TextConverter));
|
||||
|
||||
public IValueConverter? TextConverter
|
||||
{
|
||||
get => GetValue(TextConverterProperty);
|
||||
set => SetValue(TextConverterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> AllowSpinProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
|
||||
nameof(AllowSpin), true);
|
||||
|
||||
public bool AllowSpin
|
||||
{
|
||||
get => GetValue(AllowSpinProperty);
|
||||
set => SetValue(AllowSpinProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
|
||||
ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>();
|
||||
|
||||
public bool ShowButtonSpinner
|
||||
{
|
||||
get => GetValue(ShowButtonSpinnerProperty);
|
||||
set => SetValue(ShowButtonSpinnerProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler<SpinEventArgs>? Spinned;
|
||||
|
||||
static NumericUpDown()
|
||||
{
|
||||
NumberFormatProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
|
||||
FormatStringProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
|
||||
IsReadOnlyProperty.Changed.AddClassHandler<NumericUpDown, bool>((o, args) => o.OnIsReadOnlyChanged(args));
|
||||
TextConverterProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
|
||||
AllowDragProperty.Changed.AddClassHandler<NumericUpDown, bool>((o, e) => o.OnAllowDragChange(e));
|
||||
}
|
||||
|
||||
private void OnAllowDragChange(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanel);
|
||||
}
|
||||
|
||||
private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
ChangeToSetSpinDirection(args, false);
|
||||
TextBox.IsReadOnlyProperty.SetValue(args.NewValue.Value, _textBox);
|
||||
}
|
||||
|
||||
protected void ChangeToSetSpinDirection(AvaloniaPropertyChangedEventArgs e, bool afterInitialization = false)
|
||||
{
|
||||
if (afterInitialization)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnFormatChange(AvaloniaPropertyChangedEventArgs arg)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SyncTextAndValue(false, null, true);//sync text update while OnFormatChange
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Spinner.SpinEvent.RemoveHandler(OnSpin, _spinner);
|
||||
PointerPressedEvent.RemoveHandler(OnDragPanelPointerPressed, _dragPanel);
|
||||
PointerMovedEvent.RemoveHandler(OnDragPanelPointerMoved, _dragPanel);
|
||||
PointerReleasedEvent.RemoveHandler(OnDragPanelPointerReleased, _dragPanel);
|
||||
_spinner = e.NameScope.Find<ButtonSpinner>(PART_Spinner);
|
||||
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
|
||||
_dragPanel = e.NameScope.Find<Panel>(PART_DragPanel);
|
||||
IsVisibleProperty.SetValue(AllowDrag, _dragPanel);
|
||||
TextBox.IsReadOnlyProperty.SetValue(IsReadOnly, _textBox);
|
||||
Spinner.SpinEvent.AddHandler(OnSpin, _spinner);
|
||||
PointerPressedEvent.AddHandler(OnDragPanelPointerPressed, _dragPanel);
|
||||
PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanel);
|
||||
PointerReleasedEvent.AddHandler(OnDragPanelPointerReleased, _dragPanel);
|
||||
}
|
||||
|
||||
protected override void OnLostFocus(RoutedEventArgs e)
|
||||
{
|
||||
CommitInput(true);
|
||||
base.OnLostFocus(e);
|
||||
if (AllowDrag && _dragPanel is not null)
|
||||
{
|
||||
_dragPanel.IsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
var commitSuccess = CommitInput(true);
|
||||
e.Handled = !commitSuccess;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
if (AllowDrag && _dragPanel is not null)
|
||||
{
|
||||
_dragPanel.IsVisible = true;
|
||||
// _dragPanel.Focus();
|
||||
_textBox?.ClearSelection();
|
||||
_spinner?.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_point = e.GetPosition(this);
|
||||
if (e.ClickCount == 2 && _dragPanel is not null && AllowDrag)
|
||||
{
|
||||
IsVisibleProperty.SetValue(false, _dragPanel);
|
||||
_textBox?.Focus();
|
||||
TextBox.IsReadOnlyProperty.SetValue(IsReadOnly, _textBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
_textBox?.Focus();
|
||||
TextBox.IsReadOnlyProperty.SetValue(true, _textBox);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTextInput(TextInputEventArgs e)
|
||||
{
|
||||
if (IsReadOnly) return;
|
||||
_textBox?.RaiseEvent(e);
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_point = null;
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (!AllowDrag || IsReadOnly) return;
|
||||
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
|
||||
var point = e.GetPosition(this);
|
||||
var delta = point - _point;
|
||||
if (delta is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int d = GetDelta(delta.Value);
|
||||
if (d > 0)
|
||||
{
|
||||
if (_canIncrease)
|
||||
Increase();
|
||||
}
|
||||
else if (d < 0)
|
||||
{
|
||||
if (_canDecrease)
|
||||
Decrease();
|
||||
}
|
||||
_point = point;
|
||||
}
|
||||
|
||||
private int GetDelta(Point point)
|
||||
{
|
||||
bool horizontal = Math.Abs(point.X) > Math.Abs(point.Y);
|
||||
var value = horizontal ? point.X : -point.Y;
|
||||
return value switch
|
||||
{
|
||||
> 0 => 1,
|
||||
< 0 => -1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private void OnSpin(object sender, SpinEventArgs e)
|
||||
{
|
||||
if (AllowSpin && !IsReadOnly)
|
||||
{
|
||||
var spin = !e.UsingMouseWheel;
|
||||
spin |= _textBox is { IsFocused: true };
|
||||
if (spin)
|
||||
{
|
||||
e.Handled = true;
|
||||
var handler = Spinned;
|
||||
handler?.Invoke(this, e);
|
||||
if (e.Direction == SpinDirection.Increase)
|
||||
{
|
||||
Increase();
|
||||
}
|
||||
else
|
||||
{
|
||||
Decrease();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void SetValidSpinDirection();
|
||||
|
||||
protected abstract void Increase();
|
||||
|
||||
protected abstract void Decrease();
|
||||
|
||||
protected virtual bool CommitInput(bool forceTextUpdate = false)
|
||||
{
|
||||
return SyncTextAndValue(true, _textBox?.Text, forceTextUpdate);
|
||||
}
|
||||
|
||||
protected abstract bool SyncTextAndValue(bool fromTextToValue = false, string? text = null,
|
||||
bool forceTextUpdate = false);
|
||||
|
||||
public abstract void Clear();
|
||||
}
|
||||
|
||||
public abstract class NumericUpDownBase<T> : NumericUpDown where T : struct, IComparable<T>
|
||||
{
|
||||
protected static string TrimString(string? text, NumberStyles numberStyles)
|
||||
{
|
||||
if (text is null) return string.Empty;
|
||||
text = text.Trim();
|
||||
if (text.Contains("_")) // support _ like 0x1024_1024(hex), 10_24 (normal)
|
||||
{
|
||||
text = text.Replace("_", "");
|
||||
}
|
||||
|
||||
if ((numberStyles & NumberStyles.AllowHexSpecifier) != 0)
|
||||
{
|
||||
if (text.StartsWith("0x") || text.StartsWith("0X")) // support 0x hex while user input
|
||||
{
|
||||
text = text.Substring(2);
|
||||
}
|
||||
else if (text.StartsWith("h'") || text.StartsWith("H'")) // support verilog hex while user input
|
||||
{
|
||||
text = text.Substring(2);
|
||||
}
|
||||
else if (text.StartsWith("h") || text.StartsWith("H")) // support hex while user input
|
||||
{
|
||||
text = text.Substring(1);
|
||||
}
|
||||
}
|
||||
#if NET8_0_OR_GREATER
|
||||
else if ((numberStyles & NumberStyles.AllowBinarySpecifier) != 0)
|
||||
{
|
||||
if (text.StartsWith("0b") || text.StartsWith("0B")) // support 0b bin while user input
|
||||
{
|
||||
text = text.Substring(2);
|
||||
}
|
||||
else if (text.StartsWith("b'") || text.StartsWith("B'")) // support verilog bin while user input
|
||||
{
|
||||
text = text.Substring(2);
|
||||
}
|
||||
else if (text.StartsWith("b") || text.StartsWith("B")) // support bin while user input
|
||||
{
|
||||
text = text.Substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
return text;
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T?> ValueProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
||||
nameof(Value), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public T? Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T> MaximumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||
nameof(Maximum), defaultBindingMode: BindingMode.TwoWay, coerce: CoerceMaximum);
|
||||
|
||||
public T Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T> MinimumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||
nameof(Minimum), defaultBindingMode: BindingMode.TwoWay, coerce: CoerceMinimum);
|
||||
|
||||
public T Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
#region Max and Min Coerce
|
||||
private static T CoerceMaximum(AvaloniaObject instance, T value)
|
||||
{
|
||||
if (instance is NumericUpDownBase<T> n)
|
||||
{
|
||||
return n.CoerceMaximum(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private T CoerceMaximum(T value)
|
||||
{
|
||||
if (value.CompareTo(Minimum) < 0)
|
||||
{
|
||||
return Minimum;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static T CoerceMinimum(AvaloniaObject instance, T value)
|
||||
{
|
||||
if (instance is NumericUpDownBase<T> n)
|
||||
{
|
||||
return n.CoerceMinimum(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private T CoerceMinimum(T value)
|
||||
{
|
||||
if (value.CompareTo(Maximum) > 0)
|
||||
{
|
||||
return Maximum;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static readonly StyledProperty<T> StepProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||
nameof(Step));
|
||||
|
||||
public T Step
|
||||
{
|
||||
get => GetValue(StepProperty);
|
||||
set => SetValue(StepProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T?> EmptyInputValueProperty =
|
||||
AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
||||
nameof(EmptyInputValue), defaultValue: null);
|
||||
|
||||
public T? EmptyInputValue
|
||||
{
|
||||
get => GetValue(EmptyInputValueProperty);
|
||||
set => SetValue(EmptyInputValueProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly StyledProperty<ICommand?> CommandProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, ICommand?>(
|
||||
nameof(Command));
|
||||
|
||||
public ICommand? Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> CommandParameterProperty =
|
||||
AvaloniaProperty.Register<NumericUpDownBase<T>, object?>(nameof(CommandParameter));
|
||||
|
||||
public object? CommandParameter
|
||||
{
|
||||
get => this.GetValue(CommandParameterProperty);
|
||||
set => this.SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
private void InvokeCommand(object? cp)
|
||||
{
|
||||
if (this.Command != null && this.Command.CanExecute(cp))
|
||||
{
|
||||
this.Command.Execute(cp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ValueChanged"/> event.
|
||||
/// </summary>
|
||||
public static readonly RoutedEvent<ValueChangedEventArgs<T>> ValueChangedEvent =
|
||||
RoutedEvent.Register<NumericUpDown, ValueChangedEventArgs<T>>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the <see cref="Value"/> changes.
|
||||
/// </summary>
|
||||
public event EventHandler<ValueChangedEventArgs<T>>? ValueChanged
|
||||
{
|
||||
add => AddHandler(ValueChangedEvent, value);
|
||||
remove => RemoveHandler(ValueChangedEvent, value);
|
||||
}
|
||||
|
||||
static NumericUpDownBase()
|
||||
{
|
||||
StepProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.ChangeToSetSpinDirection(e));
|
||||
MaximumProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.OnConstraintChanged(e));
|
||||
MinimumProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.OnConstraintChanged(e));
|
||||
ValueProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.OnValueChanged(e));
|
||||
}
|
||||
|
||||
private void OnConstraintChanged(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
if (Value.HasValue)
|
||||
{
|
||||
SetCurrentValue(ValueProperty, Clamp(Value, Maximum, Minimum));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SyncTextAndValue(false, null, true);
|
||||
SetValidSpinDirection();
|
||||
T? oldValue = args.GetOldValue<T?>();
|
||||
T? newValue = args.GetNewValue<T?>();
|
||||
var e = new ValueChangedEventArgs<T>(ValueChangedEvent, oldValue, newValue);
|
||||
RaiseEventCommand(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseEventCommand(ValueChangedEventArgs<T> e)
|
||||
{
|
||||
InvokeCommand(this.CommandParameter ?? e.NewValue);
|
||||
RaiseEvent(e);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_textBox != null)
|
||||
{
|
||||
_textBox.Text = ConvertValueToText(Value);
|
||||
}
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
|
||||
protected virtual T? Clamp(T? value, T max, T min)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (value.Value.CompareTo(max) > 0)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
if (value.Value.CompareTo(min) < 0)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected override void SetValidSpinDirection()
|
||||
{
|
||||
var validDirection = ValidSpinDirections.None;
|
||||
_canIncrease = false;
|
||||
_canDecrease = false;
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
if (Value is null)
|
||||
{
|
||||
validDirection = ValidSpinDirections.Increase | ValidSpinDirections.Decrease;
|
||||
}
|
||||
if (Value.HasValue && Value.Value.CompareTo(Maximum) < 0)
|
||||
{
|
||||
validDirection |= ValidSpinDirections.Increase;
|
||||
_canIncrease = true;
|
||||
}
|
||||
|
||||
if (Value.HasValue && Value.Value.CompareTo(Minimum) > 0)
|
||||
{
|
||||
validDirection |= ValidSpinDirections.Decrease;
|
||||
_canDecrease = true;
|
||||
}
|
||||
}
|
||||
if (_spinner != null)
|
||||
{
|
||||
_spinner.ValidSpinDirection = validDirection;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isSyncingTextAndValue;
|
||||
|
||||
protected override bool SyncTextAndValue(bool fromTextToValue = false, string? text = null, bool forceTextUpdate = false)
|
||||
{
|
||||
if (_isSyncingTextAndValue) return true;
|
||||
_isSyncingTextAndValue = true;
|
||||
var parsedTextIsValid = true;
|
||||
try
|
||||
{
|
||||
if (fromTextToValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newValue = ConvertTextToValue(text);
|
||||
if (EmptyInputValue is not null && newValue is null)
|
||||
{
|
||||
newValue = EmptyInputValue;
|
||||
}
|
||||
if (!Equals(newValue, Value))
|
||||
{
|
||||
if (Equals(Clamp(newValue, Maximum, Minimum), newValue))
|
||||
{
|
||||
SetCurrentValue(ValueProperty, newValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedTextIsValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
parsedTextIsValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_updateFromTextInput)
|
||||
{
|
||||
if (forceTextUpdate)
|
||||
{
|
||||
var newText = ConvertValueToText(Value);
|
||||
if (_textBox != null && !Equals(_textBox.Text, newText))
|
||||
{
|
||||
_textBox.Text = newText;
|
||||
_textBox.CaretIndex = newText?.Length ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_updateFromTextInput && !parsedTextIsValid)
|
||||
{
|
||||
if (_spinner is not null)
|
||||
{
|
||||
_spinner.ValidSpinDirection = ValidSpinDirections.None;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSyncingTextAndValue = false;
|
||||
}
|
||||
return parsedTextIsValid;
|
||||
}
|
||||
|
||||
protected virtual T? ConvertTextToValue(string? text)
|
||||
{
|
||||
T? result;
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
if (TextConverter != null)
|
||||
{
|
||||
var valueFromText = TextConverter.Convert(text, typeof(T?), null, CultureInfo.CurrentCulture);
|
||||
return (T?)valueFromText;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = TrimString(text, ParsingNumberStyle);
|
||||
if (!ParseText(text, out var outputValue))
|
||||
{
|
||||
throw new InvalidDataException("Input string was not in a correct format.");
|
||||
}
|
||||
|
||||
result = outputValue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual string? ConvertValueToText(T? value)
|
||||
{
|
||||
if (TextConverter is not null)
|
||||
{
|
||||
return TextConverter.ConvertBack(Value, typeof(int), null, CultureInfo.CurrentCulture)?.ToString();
|
||||
}
|
||||
|
||||
if (FormatString.Contains("{0"))
|
||||
{
|
||||
return string.Format(NumberFormat, FormatString, value);
|
||||
}
|
||||
|
||||
return ValueToString(Value);
|
||||
}
|
||||
|
||||
protected override void Increase()
|
||||
{
|
||||
T? value;
|
||||
if (Value is not null)
|
||||
{
|
||||
value = Add(Value.Value, Step);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = IsSet(MinimumProperty) ? Minimum : Zero;
|
||||
}
|
||||
SetCurrentValue(ValueProperty, Clamp(value, Maximum, Minimum));
|
||||
}
|
||||
|
||||
protected override void Decrease()
|
||||
{
|
||||
T? value;
|
||||
if (Value is not null)
|
||||
{
|
||||
value = Minus(Value.Value, Step);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = IsSet(MaximumProperty) ? Maximum : Zero;
|
||||
}
|
||||
|
||||
SetCurrentValue(ValueProperty, Clamp(value, Maximum, Minimum));
|
||||
}
|
||||
|
||||
protected abstract bool ParseText(string? text, out T number);
|
||||
protected abstract string? ValueToString(T? value);
|
||||
protected abstract T Zero { get; }
|
||||
protected abstract T? Add(T? a, T? b);
|
||||
protected abstract T? Minus(T? a, T? b);
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
SetCurrentValue(ValueProperty, EmptyInputValue);
|
||||
SyncTextAndValue(false, forceTextUpdate: true);
|
||||
}
|
||||
}
|
||||
15
src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs
Normal file
15
src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ValueChangedEventArgs<T>: RoutedEventArgs where T: struct, IComparable<T>
|
||||
{
|
||||
public ValueChangedEventArgs(RoutedEvent routedEvent, T? oldValue, T? newValue): base(routedEvent)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
public T? OldValue { get; }
|
||||
public T? NewValue { get; }
|
||||
|
||||
}
|
||||
11
src/Ursa/Controls/OverlayShared/DialogButton.cs
Normal file
11
src/Ursa/Controls/OverlayShared/DialogButton.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum DialogButton
|
||||
{
|
||||
None,
|
||||
OK,
|
||||
OKCancel,
|
||||
YesNo,
|
||||
YesNoCancel,
|
||||
}
|
||||
|
||||
11
src/Ursa/Controls/OverlayShared/DialogMode.cs
Normal file
11
src/Ursa/Controls/OverlayShared/DialogMode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum DialogMode
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Question,
|
||||
None,
|
||||
Success,
|
||||
}
|
||||
10
src/Ursa/Controls/OverlayShared/DialogResult.cs
Normal file
10
src/Ursa/Controls/OverlayShared/DialogResult.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum DialogResult
|
||||
{
|
||||
Cancel,
|
||||
No,
|
||||
None,
|
||||
OK,
|
||||
Yes,
|
||||
}
|
||||
310
src/Ursa/Controls/OverlayShared/OverlayDialogHost.Dialog.cs
Normal file
310
src/Ursa/Controls/OverlayShared/OverlayDialogHost.Dialog.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Utilities;
|
||||
using Irihi.Avalonia.Shared.Shapes;
|
||||
using Ursa.Controls.OverlayShared;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public partial class OverlayDialogHost
|
||||
{
|
||||
private Point _lastPoint;
|
||||
|
||||
public Thickness SnapThickness { get; set; } = new Thickness(0);
|
||||
|
||||
private static void ResetDialogPosition(DialogControlBase control, Size newSize)
|
||||
{
|
||||
if (control.IsFullScreen)
|
||||
{
|
||||
control.Width = newSize.Width;
|
||||
control.Height = newSize.Height;
|
||||
SetLeft(control, 0);
|
||||
SetTop(control, 0);
|
||||
return;
|
||||
}
|
||||
control.MaxWidth = newSize.Width;
|
||||
control.MaxHeight = newSize.Height;
|
||||
var width = newSize.Width - control.Bounds.Width;
|
||||
var height = newSize.Height - control.Bounds.Height;
|
||||
var newLeft = width * control.HorizontalOffsetRatio??0;
|
||||
var newTop = height * control.VerticalOffsetRatio??0;
|
||||
if(control.ActualHorizontalAnchor == HorizontalPosition.Left)
|
||||
{
|
||||
newLeft = 0;
|
||||
}
|
||||
if (control.ActualHorizontalAnchor == HorizontalPosition.Right)
|
||||
{
|
||||
newLeft = newSize.Width - control.Bounds.Width;
|
||||
}
|
||||
if (control.ActualVerticalAnchor == VerticalPosition.Top)
|
||||
{
|
||||
newTop = 0;
|
||||
}
|
||||
if (control.ActualVerticalAnchor == VerticalPosition.Bottom)
|
||||
{
|
||||
newTop = newSize.Height - control.Bounds.Height;
|
||||
}
|
||||
SetLeft(control, Math.Max(0.0, newLeft));
|
||||
SetTop(control, Math.Max(0.0, newTop));
|
||||
}
|
||||
|
||||
protected override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
if (e.Source is DialogControlBase item)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
var p = e.GetPosition(this);
|
||||
var left = p.X - _lastPoint.X;
|
||||
var top = p.Y - _lastPoint.Y;
|
||||
left = MathUtilities.Clamp(left, 0, Bounds.Width - item.Bounds.Width);
|
||||
top = MathUtilities.Clamp(top, 0, Bounds.Height - item.Bounds.Height);
|
||||
SetLeft(item, left);
|
||||
SetTop(item, top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.Source is DialogControlBase item)
|
||||
{
|
||||
_lastPoint = e.GetPosition(item);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
if (e.Source is DialogControlBase item)
|
||||
{
|
||||
AnchorAndUpdatePositionInfo(item);
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddDialog(DialogControlBase control)
|
||||
{
|
||||
PureRectangle? mask = null;
|
||||
if (control.CanLightDismiss)
|
||||
{
|
||||
mask = CreateOverlayMask(false, control.CanLightDismiss);
|
||||
}
|
||||
if (mask is not null)
|
||||
{
|
||||
Children.Add(mask);
|
||||
}
|
||||
this.Children.Add(control);
|
||||
_layers.Add(new DialogPair(mask, control, false));
|
||||
if (control.IsFullScreen)
|
||||
{
|
||||
control.Width = Bounds.Width;
|
||||
control.Height = Bounds.Height;
|
||||
}
|
||||
control.MaxWidth = Bounds.Width;
|
||||
control.MaxHeight = Bounds.Height;
|
||||
control.Measure(this.Bounds.Size);
|
||||
control.Arrange(new Rect(control.DesiredSize));
|
||||
SetToPosition(control);
|
||||
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
|
||||
control.AddHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
|
||||
ResetZIndices();
|
||||
}
|
||||
|
||||
private async void OnDialogControlClosing(object sender, object? e)
|
||||
{
|
||||
if (sender is DialogControlBase control)
|
||||
{
|
||||
var layer = _layers.FirstOrDefault(a => a.Element == control);
|
||||
if (layer is null) return;
|
||||
_layers.Remove(layer);
|
||||
|
||||
control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
|
||||
control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
|
||||
|
||||
Children.Remove(control);
|
||||
|
||||
if (layer.Mask is not null)
|
||||
{
|
||||
Children.Remove(layer.Mask);
|
||||
|
||||
if (layer.Modal)
|
||||
{
|
||||
_modalCount--;
|
||||
HasModal = _modalCount > 0;
|
||||
if (!IsAnimationDisabled)
|
||||
{
|
||||
await _maskDisappearAnimation.RunAsync(layer.Mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResetZIndices();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a dialog as a modal dialog to the host
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
internal void AddModalDialog(DialogControlBase control)
|
||||
{
|
||||
var mask = CreateOverlayMask(true, control.CanLightDismiss);
|
||||
_layers.Add(new DialogPair(mask, control));
|
||||
control.SetAsModal(true);
|
||||
ResetZIndices();
|
||||
this.Children.Add(mask);
|
||||
this.Children.Add(control);
|
||||
if (control.IsFullScreen)
|
||||
{
|
||||
control.Width = Bounds.Width;
|
||||
control.Height = Bounds.Height;
|
||||
}
|
||||
control.MaxWidth = Bounds.Width;
|
||||
control.MaxHeight = Bounds.Height;
|
||||
control.Measure(this.Bounds.Size);
|
||||
control.Arrange(new Rect(control.DesiredSize));
|
||||
SetToPosition(control);
|
||||
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
|
||||
control.AddHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
|
||||
if (!IsAnimationDisabled)
|
||||
{
|
||||
_maskAppearAnimation.RunAsync(mask);
|
||||
}
|
||||
_modalCount++;
|
||||
HasModal = _modalCount > 0;
|
||||
control.IsClosed = false;
|
||||
}
|
||||
|
||||
// Handle dialog layer change event
|
||||
private void OnDialogLayerChanged(object sender, DialogLayerChangeEventArgs e)
|
||||
{
|
||||
if (sender is not DialogControlBase control)
|
||||
return;
|
||||
var layer = _layers.FirstOrDefault(a => a.Element == control);
|
||||
if (layer is null) return;
|
||||
int index = _layers.IndexOf(layer);
|
||||
_layers.Remove(layer);
|
||||
int newIndex = index;
|
||||
switch (e.ChangeType)
|
||||
{
|
||||
case DialogLayerChangeType.BringForward:
|
||||
newIndex = MathUtilities.Clamp(index + 1, 0, _layers.Count);
|
||||
break;
|
||||
case DialogLayerChangeType.SendBackward:
|
||||
newIndex = MathUtilities.Clamp(index - 1, 0, _layers.Count);
|
||||
break;
|
||||
case DialogLayerChangeType.BringToFront:
|
||||
newIndex = _layers.Count;
|
||||
break;
|
||||
case DialogLayerChangeType.SendToBack:
|
||||
newIndex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
_layers.Insert(newIndex, layer);
|
||||
ResetZIndices();
|
||||
}
|
||||
|
||||
private void SetToPosition(DialogControlBase? control)
|
||||
{
|
||||
if (control is null) return;
|
||||
double left = GetLeftPosition(control);
|
||||
double top = GetTopPosition(control);
|
||||
SetLeft(control, left);
|
||||
SetTop(control, top);
|
||||
AnchorAndUpdatePositionInfo(control);
|
||||
}
|
||||
|
||||
private void AnchorAndUpdatePositionInfo(DialogControlBase control)
|
||||
{
|
||||
control.ActualHorizontalAnchor = HorizontalPosition.Center;
|
||||
control.ActualVerticalAnchor = VerticalPosition.Center;
|
||||
double left = GetLeft(control);
|
||||
double top = GetTop(control);
|
||||
double right = Bounds.Width - left - control.Bounds.Width;
|
||||
double bottom = Bounds.Height - top - control.Bounds.Height;
|
||||
if(top < SnapThickness.Top)
|
||||
{
|
||||
SetTop(control, 0);
|
||||
control.ActualVerticalAnchor = VerticalPosition.Top;
|
||||
control.VerticalOffsetRatio = 0;
|
||||
}
|
||||
if(bottom < SnapThickness.Bottom)
|
||||
{
|
||||
SetTop(control, Bounds.Height - control.Bounds.Height);
|
||||
control.ActualVerticalAnchor = VerticalPosition.Bottom;
|
||||
control.VerticalOffsetRatio = 1;
|
||||
}
|
||||
if(left < SnapThickness.Left)
|
||||
{
|
||||
SetLeft(control, 0);
|
||||
control.ActualHorizontalAnchor = HorizontalPosition.Left;
|
||||
control.HorizontalOffsetRatio = 0;
|
||||
}
|
||||
if(right < SnapThickness.Right)
|
||||
{
|
||||
SetLeft(control, Bounds.Width - control.Bounds.Width);
|
||||
control.ActualHorizontalAnchor = HorizontalPosition.Right;
|
||||
control.HorizontalOffsetRatio = 1;
|
||||
}
|
||||
left = GetLeft(control);
|
||||
top = GetTop(control);
|
||||
right = Bounds.Width - left - control.Bounds.Width;
|
||||
bottom = Bounds.Height - top - control.Bounds.Height;
|
||||
|
||||
control.HorizontalOffsetRatio = (left + right) == 0 ? 0 : left / (left + right);
|
||||
control.VerticalOffsetRatio = (top + bottom) == 0 ? 0 : top / (top + bottom);
|
||||
}
|
||||
|
||||
private double GetLeftPosition(DialogControlBase control)
|
||||
{
|
||||
double left = 0;
|
||||
double offset = Math.Max(0, control.HorizontalOffset ?? 0);
|
||||
left = this.Bounds.Width - control.Bounds.Width;
|
||||
if (control.HorizontalAnchor == HorizontalPosition.Center)
|
||||
{
|
||||
left *= 0.5;
|
||||
(double min, double max) = MathUtilities.GetMinMax(0, Bounds.Width * 0.5);
|
||||
left = MathUtilities.Clamp(left, min, max);
|
||||
}
|
||||
else if (control.HorizontalAnchor == HorizontalPosition.Left)
|
||||
{
|
||||
(double min, double max) = MathUtilities.GetMinMax(0, offset);
|
||||
left = MathUtilities.Clamp(left, min, max);
|
||||
}
|
||||
else if (control.HorizontalAnchor == HorizontalPosition.Right)
|
||||
{
|
||||
double leftOffset = Bounds.Width - control.Bounds.Width - offset;
|
||||
leftOffset = Math.Max(0, leftOffset);
|
||||
if(control.HorizontalOffset.HasValue)
|
||||
{
|
||||
left = MathUtilities.Clamp(left, 0, leftOffset);
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private double GetTopPosition(DialogControlBase control)
|
||||
{
|
||||
double top = 0;
|
||||
double offset = Math.Max(0, control.VerticalOffset ?? 0);
|
||||
top = this.Bounds.Height - control.Bounds.Height;
|
||||
if (control.VerticalAnchor == VerticalPosition.Center)
|
||||
{
|
||||
top *= 0.5;
|
||||
(double min, double max) = MathUtilities.GetMinMax(0, Bounds.Height * 0.5);
|
||||
top = MathUtilities.Clamp(top, min, max);
|
||||
}
|
||||
else if (control.VerticalAnchor == VerticalPosition.Top)
|
||||
{
|
||||
top = MathUtilities.Clamp(top, 0, offset);
|
||||
}
|
||||
else if (control.VerticalAnchor == VerticalPosition.Bottom)
|
||||
{
|
||||
var topOffset = Math.Max(0, Bounds.Height - control.Bounds.Height - offset);
|
||||
top = MathUtilities.Clamp(top, 0, topOffset);
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
186
src/Ursa/Controls/OverlayShared/OverlayDialogHost.Drawer.cs
Normal file
186
src/Ursa/Controls/OverlayShared/OverlayDialogHost.Drawer.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
using Irihi.Avalonia.Shared.Shapes;
|
||||
using Ursa.Common;
|
||||
using Ursa.Controls.OverlayShared;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public partial class OverlayDialogHost
|
||||
{
|
||||
internal async void AddDrawer(DrawerControlBase control)
|
||||
{
|
||||
PureRectangle? mask = null;
|
||||
if (control.CanLightDismiss)
|
||||
{
|
||||
mask = CreateOverlayMask(false, true);
|
||||
}
|
||||
_layers.Add(new DialogPair(mask, control));
|
||||
ResetZIndices();
|
||||
if(mask is not null)this.Children.Add(mask);
|
||||
this.Children.Add(control);
|
||||
control.Measure(this.Bounds.Size);
|
||||
control.Arrange(new Rect(control.DesiredSize));
|
||||
SetDrawerPosition(control);
|
||||
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDrawerControlClosing);
|
||||
var animation = CreateAnimation(control.Bounds.Size, control.Position, true);
|
||||
if (IsAnimationDisabled)
|
||||
{
|
||||
ResetDrawerPosition(control, this.Bounds.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mask is null)
|
||||
{
|
||||
await animation.RunAsync(control);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.WhenAll(animation.RunAsync(control), _maskAppearAnimation.RunAsync(mask));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async void AddModalDrawer(DrawerControlBase control)
|
||||
{
|
||||
PureRectangle? mask = CreateOverlayMask(true, control.CanLightDismiss);
|
||||
_layers.Add(new DialogPair(mask, control));
|
||||
this.Children.Add(mask);
|
||||
this.Children.Add(control);
|
||||
ResetZIndices();
|
||||
control.Measure(this.Bounds.Size);
|
||||
control.Arrange(new Rect(control.DesiredSize));
|
||||
SetDrawerPosition(control);
|
||||
_modalCount++;
|
||||
HasModal = _modalCount > 0;
|
||||
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDrawerControlClosing);
|
||||
var animation = CreateAnimation(control.Bounds.Size, control.Position);
|
||||
if (IsAnimationDisabled)
|
||||
{
|
||||
ResetDrawerPosition(control, this.Bounds.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.WhenAll(animation.RunAsync(control), _maskAppearAnimation.RunAsync(mask));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDrawerPosition(DrawerControlBase control)
|
||||
{
|
||||
if(control.Position is Position.Left or Position.Right)
|
||||
{
|
||||
control.Height = this.Bounds.Height;
|
||||
}
|
||||
if(control.Position is Position.Top or Position.Bottom)
|
||||
{
|
||||
control.Width = this.Bounds.Width;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetDrawerPosition(DrawerControlBase control, Size newSize)
|
||||
{
|
||||
if (control.Position == Position.Right)
|
||||
{
|
||||
control.Height = newSize.Height;
|
||||
SetLeft(control, newSize.Width - control.Bounds.Width);
|
||||
}
|
||||
else if (control.Position == Position.Left)
|
||||
{
|
||||
control.Height = newSize.Height;
|
||||
SetLeft(control, 0);
|
||||
}
|
||||
else if (control.Position == Position.Top)
|
||||
{
|
||||
control.Width = newSize.Width;
|
||||
SetTop(control, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.Width = newSize.Width;
|
||||
SetTop(control, newSize.Height-control.Bounds.Height);
|
||||
}
|
||||
}
|
||||
|
||||
private Animation CreateAnimation(Size elementBounds, Position position, bool appear = true)
|
||||
{
|
||||
// left or top.
|
||||
double source = 0;
|
||||
double target = 0;
|
||||
if (position == Position.Left)
|
||||
{
|
||||
source = appear ? -elementBounds.Width : 0;
|
||||
target = appear ? 0 : -elementBounds.Width;
|
||||
}
|
||||
|
||||
if (position == Position.Right)
|
||||
{
|
||||
source = appear ? Bounds.Width : Bounds.Width - elementBounds.Width;
|
||||
target = appear ? Bounds.Width - elementBounds.Width : Bounds.Width;
|
||||
}
|
||||
|
||||
if (position == Position.Top)
|
||||
{
|
||||
source = appear ? -elementBounds.Height : 0;
|
||||
target = appear ? 0 : -elementBounds.Height;
|
||||
}
|
||||
|
||||
if (position == Position.Bottom)
|
||||
{
|
||||
source = appear ? Bounds.Height : Bounds.Height - elementBounds.Height;
|
||||
target = appear ? Bounds.Height - elementBounds.Height : Bounds.Height;
|
||||
}
|
||||
|
||||
var targetProperty = position==Position.Left || position==Position.Right ? Canvas.LeftProperty : Canvas.TopProperty;
|
||||
var animation = new Animation();
|
||||
animation.Easing = new CubicEaseOut();
|
||||
animation.FillMode = FillMode.Forward;
|
||||
var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) };
|
||||
keyFrame1.Setters.Add(new Setter()
|
||||
{ Property = targetProperty, Value = source });
|
||||
var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) };
|
||||
keyFrame2.Setters.Add(new Setter()
|
||||
{ Property = targetProperty, Value = target });
|
||||
animation.Children.Add(keyFrame1);
|
||||
animation.Children.Add(keyFrame2);
|
||||
animation.Duration = TimeSpan.FromSeconds(0.3);
|
||||
return animation;
|
||||
}
|
||||
|
||||
private async void OnDrawerControlClosing(object sender, ResultEventArgs e)
|
||||
{
|
||||
if (sender is DrawerControlBase control)
|
||||
{
|
||||
var layer = _layers.FirstOrDefault(a => a.Element == control);
|
||||
if(layer is null) return;
|
||||
_layers.Remove(layer);
|
||||
control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
|
||||
control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
|
||||
if (layer.Mask is not null)
|
||||
{
|
||||
_modalCount--;
|
||||
HasModal = _modalCount > 0;
|
||||
layer.Mask.RemoveHandler(PointerPressedEvent, ClickMaskToCloseDialog);
|
||||
if (!IsAnimationDisabled)
|
||||
{
|
||||
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
|
||||
await Task.WhenAll(disappearAnimation.RunAsync(control), _maskDisappearAnimation.RunAsync(layer.Mask));
|
||||
}
|
||||
Children.Remove(layer.Mask);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!IsAnimationDisabled)
|
||||
{
|
||||
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
|
||||
await disappearAnimation.RunAsync(control);
|
||||
}
|
||||
}
|
||||
Children.Remove(control);
|
||||
ResetZIndices();
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/Ursa/Controls/OverlayShared/OverlayDialogHost.Shared.cs
Normal file
200
src/Ursa/Controls/OverlayShared/OverlayDialogHost.Shared.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Ursa.Controls.OverlayShared;
|
||||
using Avalonia.Styling;
|
||||
using Irihi.Avalonia.Shared.Shapes;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public partial class OverlayDialogHost: Canvas
|
||||
{
|
||||
private static readonly Animation _maskAppearAnimation;
|
||||
private static readonly Animation _maskDisappearAnimation;
|
||||
|
||||
private readonly List<DialogPair> _layers = new List<DialogPair>(10);
|
||||
|
||||
private class DialogPair
|
||||
{
|
||||
internal PureRectangle? Mask;
|
||||
internal OverlayFeedbackElement Element;
|
||||
internal bool Modal;
|
||||
|
||||
public DialogPair(PureRectangle? mask, OverlayFeedbackElement element, bool modal = true)
|
||||
{
|
||||
Mask = mask;
|
||||
Element = element;
|
||||
Modal = modal;
|
||||
}
|
||||
}
|
||||
|
||||
private int _modalCount = 0;
|
||||
|
||||
|
||||
|
||||
public static readonly DirectProperty<OverlayDialogHost, bool> HasModalProperty = AvaloniaProperty.RegisterDirect<OverlayDialogHost, bool>(
|
||||
nameof(HasModal), o => o.HasModal);
|
||||
private bool _hasModal;
|
||||
public bool HasModal
|
||||
{
|
||||
get => _hasModal;
|
||||
private set => SetAndRaise(HasModalProperty, ref _hasModal, value);
|
||||
}
|
||||
|
||||
public bool IsAnimationDisabled { get; set; }
|
||||
|
||||
static OverlayDialogHost()
|
||||
{
|
||||
ClipToBoundsProperty.OverrideDefaultValue<OverlayDialogHost>(true);
|
||||
_maskAppearAnimation = CreateOpacityAnimation(true);
|
||||
_maskDisappearAnimation = CreateOpacityAnimation(false);
|
||||
}
|
||||
|
||||
private static Animation CreateOpacityAnimation(bool appear)
|
||||
{
|
||||
var animation = new Animation();
|
||||
animation.FillMode = FillMode.Forward;
|
||||
var keyFrame1 = new KeyFrame{ Cue = new Cue(0.0) };
|
||||
keyFrame1.Setters.Add(new Setter() { Property = OpacityProperty, Value = appear ? 0.0 : 1.0 });
|
||||
var keyFrame2 = new KeyFrame{ Cue = new Cue(1.0) };
|
||||
keyFrame2.Setters.Add(new Setter() { Property = OpacityProperty, Value = appear ? 1.0 : 0.0 });
|
||||
animation.Children.Add(keyFrame1);
|
||||
animation.Children.Add(keyFrame2);
|
||||
animation.Duration = TimeSpan.FromSeconds(0.2);
|
||||
return animation;
|
||||
}
|
||||
|
||||
public string? HostId { get; set; }
|
||||
|
||||
public DataTemplates DialogDataTemplates { get; set; } = new DataTemplates();
|
||||
|
||||
public static readonly StyledProperty<IBrush?> OverlayMaskBrushProperty =
|
||||
AvaloniaProperty.Register<OverlayDialogHost, IBrush?>(
|
||||
nameof(OverlayMaskBrush));
|
||||
|
||||
public IBrush? OverlayMaskBrush
|
||||
{
|
||||
get => GetValue(OverlayMaskBrushProperty);
|
||||
set => SetValue(OverlayMaskBrushProperty, value);
|
||||
}
|
||||
|
||||
private PureRectangle CreateOverlayMask(bool modal, bool canCloseOnClick)
|
||||
{
|
||||
PureRectangle rec = new()
|
||||
{
|
||||
Width = this.Bounds.Width,
|
||||
Height = this.Bounds.Height,
|
||||
IsVisible = true,
|
||||
};
|
||||
if (modal)
|
||||
{
|
||||
rec[!PureRectangle.BackgroundProperty] = this[!OverlayMaskBrushProperty];
|
||||
}
|
||||
else if(canCloseOnClick)
|
||||
{
|
||||
rec.SetCurrentValue(PureRectangle.BackgroundProperty, Brushes.Transparent);
|
||||
}
|
||||
if (canCloseOnClick)
|
||||
{
|
||||
rec.AddHandler(PointerReleasedEvent, ClickMaskToCloseDialog);
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
private void ClickMaskToCloseDialog(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (sender is PureRectangle border)
|
||||
{
|
||||
var layer = _layers.FirstOrDefault(a => a.Mask == border);
|
||||
if (layer is not null)
|
||||
{
|
||||
layer.Element.Close();
|
||||
border.RemoveHandler(PointerReleasedEvent, ClickMaskToCloseDialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected sealed override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
OverlayDialogManager.RegisterHost(this, HostId);
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
while (_layers.Count>0)
|
||||
{
|
||||
_layers[0].Element.Close();
|
||||
}
|
||||
OverlayDialogManager.UnregisterHost(HostId);
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
}
|
||||
|
||||
|
||||
protected sealed override void OnSizeChanged(SizeChangedEventArgs e)
|
||||
{
|
||||
base.OnSizeChanged(e);
|
||||
for (int i = 0; i < _layers.Count; i++)
|
||||
{
|
||||
if (_layers[i].Mask is { } rect)
|
||||
{
|
||||
rect.Width = this.Bounds.Width;
|
||||
rect.Height = this.Bounds.Height;
|
||||
}
|
||||
if (_layers[i].Element is DialogControlBase d)
|
||||
{
|
||||
ResetDialogPosition(d, e.NewSize);
|
||||
}
|
||||
else if (_layers[i].Element is DrawerControlBase drawer)
|
||||
{
|
||||
ResetDrawerPosition(drawer, e.NewSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetZIndices()
|
||||
{
|
||||
int index = 0;
|
||||
for (int i = 0; i < _layers.Count; i++)
|
||||
{
|
||||
if(_layers[i].Mask is { } mask)
|
||||
{
|
||||
mask.ZIndex = index;
|
||||
index++;
|
||||
}
|
||||
if(_layers[i].Element is { } dialog)
|
||||
{
|
||||
dialog.ZIndex = index;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal IDataTemplate? GetDataTemplate(object? o)
|
||||
{
|
||||
if (o is null) return null;
|
||||
IDataTemplate? result = null;
|
||||
var templates = this.DialogDataTemplates;
|
||||
result = templates.FirstOrDefault(a => a.Match(o));
|
||||
if (result != null) return result;
|
||||
var keys = this.Resources.Keys;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (Resources.TryGetValue(key, out var value) && value is IDataTemplate t)
|
||||
{
|
||||
result = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal T? Recall<T>()
|
||||
{
|
||||
var element = _layers.LastOrDefault(a => a.Element.Content?.GetType() == typeof(T));
|
||||
return element?.Element.Content is T t ? t : default;
|
||||
}
|
||||
}
|
||||
42
src/Ursa/Controls/OverlayShared/OverlayDialogManager.cs
Normal file
42
src/Ursa/Controls/OverlayShared/OverlayDialogManager.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
internal static class OverlayDialogManager
|
||||
{
|
||||
private static OverlayDialogHost? _defaultHost;
|
||||
private static readonly ConcurrentDictionary<string, OverlayDialogHost> Hosts = new();
|
||||
|
||||
public static void RegisterHost(OverlayDialogHost host, string? id)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
if (_defaultHost != null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot register multiple OverlayDialogHost with empty HostId");
|
||||
}
|
||||
_defaultHost = host;
|
||||
return;
|
||||
}
|
||||
Hosts.TryAdd(id, host);
|
||||
}
|
||||
|
||||
public static void UnregisterHost(string? id)
|
||||
{
|
||||
if (id is null)
|
||||
{
|
||||
_defaultHost = null;
|
||||
return;
|
||||
}
|
||||
Hosts.TryRemove(id, out _);
|
||||
}
|
||||
|
||||
public static OverlayDialogHost? GetHost(string? id)
|
||||
{
|
||||
if (id is null)
|
||||
{
|
||||
return _defaultHost;
|
||||
}
|
||||
return Hosts.TryGetValue(id, out var host) ? host : null;
|
||||
}
|
||||
}
|
||||
89
src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs
Normal file
89
src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Ursa.EventArgs;
|
||||
|
||||
namespace Ursa.Controls.OverlayShared;
|
||||
|
||||
public abstract class OverlayFeedbackElement: ContentControl
|
||||
{
|
||||
public static readonly StyledProperty<bool> IsClosedProperty =
|
||||
AvaloniaProperty.Register<OverlayFeedbackElement, bool>(nameof(IsClosed), defaultValue: true);
|
||||
|
||||
public bool IsClosed
|
||||
{
|
||||
get => GetValue(IsClosedProperty);
|
||||
set => SetValue(IsClosedProperty, value);
|
||||
}
|
||||
|
||||
static OverlayFeedbackElement()
|
||||
{
|
||||
DataContextProperty.Changed.AddClassHandler<OverlayFeedbackElement, object?>((o, e) => o.OnDataContextChange(e));
|
||||
ClosedEvent.AddClassHandler<OverlayFeedbackElement>((o,e)=>o.OnClosed(e));
|
||||
}
|
||||
|
||||
private void OnClosed(ResultEventArgs arg2)
|
||||
{
|
||||
SetCurrentValue(IsClosedProperty,true);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<ResultEventArgs> ClosedEvent = RoutedEvent.Register<DrawerControlBase, ResultEventArgs>(
|
||||
nameof(Closed), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<ResultEventArgs> Closed
|
||||
{
|
||||
add => AddHandler(ClosedEvent, value);
|
||||
remove => RemoveHandler(ClosedEvent, value);
|
||||
}
|
||||
|
||||
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
if (args.OldValue.Value is IDialogContext oldContext)
|
||||
{
|
||||
oldContext.RequestClose -= OnContextRequestClose;
|
||||
}
|
||||
if (args.NewValue.Value is IDialogContext newContext)
|
||||
{
|
||||
newContext.RequestClose += OnContextRequestClose;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnElementClosing(object sender, object? args)
|
||||
{
|
||||
RaiseEvent(new ResultEventArgs(ClosedEvent, args));
|
||||
}
|
||||
|
||||
private void OnContextRequestClose(object sender, object? args)
|
||||
{
|
||||
RaiseEvent(new ResultEventArgs(ClosedEvent, args));
|
||||
}
|
||||
|
||||
public Task<T?> ShowAsync<T>(CancellationToken? token = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<T?>();
|
||||
token?.Register(() =>
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(Close);
|
||||
});
|
||||
|
||||
void OnCloseHandler(object sender, ResultEventArgs? args)
|
||||
{
|
||||
if (args?.Result is T result)
|
||||
{
|
||||
tcs.SetResult(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(default);
|
||||
}
|
||||
RemoveHandler(ClosedEvent, OnCloseHandler);
|
||||
}
|
||||
|
||||
AddHandler(ClosedEvent, OnCloseHandler);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public abstract void Close();
|
||||
}
|
||||
@@ -3,8 +3,11 @@ using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
@@ -16,44 +19,65 @@ namespace Ursa.Controls;
|
||||
[TemplatePart(PART_PreviousButton, typeof(PaginationButton))]
|
||||
[TemplatePart(PART_NextButton, typeof(PaginationButton))]
|
||||
[TemplatePart(PART_ButtonPanel, typeof(StackPanel))]
|
||||
[TemplatePart(PART_SizeChangerComboBox, typeof(ComboBox))]
|
||||
[TemplatePart(PART_QuickJumpInput, typeof(NumericIntUpDown))]
|
||||
public class Pagination: TemplatedControl
|
||||
{
|
||||
public const string PART_PreviousButton = "PART_PreviousButton";
|
||||
public const string PART_NextButton = "PART_NextButton";
|
||||
public const string PART_ButtonPanel = "PART_ButtonPanel";
|
||||
public const string PART_SizeChangerComboBox = "PART_SizeChangerComboBox";
|
||||
public const string PART_QuickJumpInput = "PART_QuickJumpInput";
|
||||
private PaginationButton? _previousButton;
|
||||
private PaginationButton? _nextButton;
|
||||
private StackPanel? _buttonPanel;
|
||||
private readonly PaginationButton[] _buttons = new PaginationButton[7];
|
||||
private ComboBox? _sizeChangerComboBox;
|
||||
private NumericIntUpDown? _quickJumpInput;
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_previousButton != null) _previousButton.Click -= OnButtonClick;
|
||||
if (_nextButton != null) _nextButton.Click -= OnButtonClick;
|
||||
_previousButton = e.NameScope.Find<PaginationButton>(PART_PreviousButton);
|
||||
_nextButton = e.NameScope.Find<PaginationButton>(PART_NextButton);
|
||||
_buttonPanel = e.NameScope.Find<StackPanel>(PART_ButtonPanel);
|
||||
_sizeChangerComboBox = e.NameScope.Find<ComboBox>(PART_SizeChangerComboBox);
|
||||
if (_previousButton != null) _previousButton.Click += OnButtonClick;
|
||||
if (_nextButton != null) _nextButton.Click += OnButtonClick;
|
||||
InitializePanelButtons();
|
||||
UpdateButtons(0);
|
||||
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<int> CurrentPageProperty = AvaloniaProperty.Register<Pagination, int>(
|
||||
public static readonly StyledProperty<int?> CurrentPageProperty = AvaloniaProperty.Register<Pagination, int?>(
|
||||
nameof(CurrentPage));
|
||||
|
||||
public int CurrentPage
|
||||
|
||||
public int? CurrentPage
|
||||
{
|
||||
get => GetValue(CurrentPageProperty);
|
||||
set => SetValue(CurrentPageProperty, value);
|
||||
}
|
||||
|
||||
private void OnCurrentPageChanged(AvaloniaPropertyChangedEventArgs<int?> args)
|
||||
{
|
||||
int? oldValue = args.GetOldValue<int?>();
|
||||
int? newValue = args.GetNewValue<int?>();
|
||||
var e = new ValueChangedEventArgs<int>(CurrentPageChangedEvent, oldValue, newValue);
|
||||
RaiseEvent(e);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<ValueChangedEventArgs<int>> CurrentPageChangedEvent =
|
||||
RoutedEvent.Register<Pagination, ValueChangedEventArgs<int>>(nameof(CurrentPageChanged), RoutingStrategies.Bubble);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the <see cref="CurrentPage"/> changes.
|
||||
/// </summary>
|
||||
public event EventHandler<ValueChangedEventArgs<int>>? CurrentPageChanged
|
||||
{
|
||||
add => AddHandler(CurrentPageChangedEvent, value);
|
||||
remove => RemoveHandler(CurrentPageChangedEvent, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ICommand?> CommandProperty = AvaloniaProperty.Register<Pagination, ICommand?>(
|
||||
nameof(Command));
|
||||
|
||||
public ICommand? Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> CommandParameterProperty = AvaloniaProperty.Register<Pagination, object?>(nameof(CommandParameter));
|
||||
|
||||
public object? CommandParameter
|
||||
{
|
||||
get => this.GetValue(CommandParameterProperty);
|
||||
set => this.SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<int> TotalCountProperty = AvaloniaProperty.Register<Pagination, int>(
|
||||
nameof(TotalCount));
|
||||
|
||||
@@ -68,7 +92,7 @@ public class Pagination: TemplatedControl
|
||||
|
||||
public static readonly StyledProperty<int> PageSizeProperty = AvaloniaProperty.Register<Pagination, int>(
|
||||
nameof(PageSize), defaultValue: 10);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Page size.
|
||||
/// </summary>
|
||||
@@ -128,33 +152,82 @@ public class Pagination: TemplatedControl
|
||||
set => SetValue(ShowQuickJumpProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
static Pagination()
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
// When page size is updated, the selected current page must be updated.
|
||||
if (change.Property == PageSizeProperty)
|
||||
PageSizeProperty.Changed.AddClassHandler<Pagination, int>((pagination, args) => pagination.OnPageSizeChanged(args));
|
||||
CurrentPageProperty.Changed.AddClassHandler<Pagination, int?>((pagination, args) =>
|
||||
pagination.UpdateButtonsByCurrentPage(args.NewValue.Value));
|
||||
CurrentPageProperty.Changed.AddClassHandler<Pagination, int?>((pagination, args) =>
|
||||
pagination.OnCurrentPageChanged(args));
|
||||
TotalCountProperty.Changed.AddClassHandler<Pagination, int>((pagination, args) =>
|
||||
pagination.UpdateButtonsByCurrentPage(pagination.CurrentPage));
|
||||
}
|
||||
|
||||
private void OnPageSizeChanged(AvaloniaPropertyChangedEventArgs<int> args)
|
||||
{
|
||||
int pageCount = TotalCount / args.NewValue.Value;
|
||||
int residue = TotalCount % args.NewValue.Value;
|
||||
if (residue > 0)
|
||||
{
|
||||
int oldPageSize = change.GetOldValue<int>();
|
||||
int index = oldPageSize * CurrentPage;
|
||||
UpdateButtons(index);
|
||||
pageCount++;
|
||||
}
|
||||
else if (change.Property == TotalCountProperty || change.Property == CurrentPageProperty)
|
||||
PageCount = pageCount;
|
||||
if (CurrentPage > PageCount)
|
||||
{
|
||||
int index = PageSize * CurrentPage;
|
||||
UpdateButtons(index);
|
||||
CurrentPage = null;
|
||||
}
|
||||
UpdateButtonsByCurrentPage(CurrentPage);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
Button.ClickEvent.AddHandler(OnButtonClick, _previousButton, _nextButton);
|
||||
_previousButton = e.NameScope.Find<PaginationButton>(PART_PreviousButton);
|
||||
_nextButton = e.NameScope.Find<PaginationButton>(PART_NextButton);
|
||||
_buttonPanel = e.NameScope.Find<StackPanel>(PART_ButtonPanel);
|
||||
Button.ClickEvent.AddHandler(OnButtonClick, _previousButton, _nextButton);
|
||||
|
||||
KeyDownEvent.RemoveHandler(OnQuickJumpInputKeyDown, _quickJumpInput);
|
||||
LostFocusEvent.RemoveHandler(OnQuickJumpInputLostFocus, _quickJumpInput);
|
||||
_quickJumpInput = e.NameScope.Find<NumericIntUpDown>(PART_QuickJumpInput);
|
||||
KeyDownEvent.AddHandler(OnQuickJumpInputKeyDown, _quickJumpInput);
|
||||
LostFocusEvent.AddHandler(OnQuickJumpInputLostFocus, _quickJumpInput);
|
||||
|
||||
InitializePanelButtons();
|
||||
UpdateButtonsByCurrentPage(0);
|
||||
}
|
||||
|
||||
private void OnQuickJumpInputKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key is Key.Enter or Key.Return)
|
||||
{
|
||||
SyncQuickJumperValue();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuickJumpInputLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SyncQuickJumperValue();
|
||||
}
|
||||
|
||||
private void SyncQuickJumperValue()
|
||||
{
|
||||
if (_quickJumpInput is null) return;
|
||||
var value = _quickJumpInput?.Value;
|
||||
if (value is null) return;
|
||||
value = Clamp(value.Value, 1, PageCount);
|
||||
SetCurrentValue(CurrentPageProperty, value);
|
||||
_quickJumpInput?.SetCurrentValue(NumericIntUpDown.ValueProperty, null);
|
||||
InvokeCommand();
|
||||
}
|
||||
|
||||
private void OnButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Equals(sender, _previousButton))
|
||||
{
|
||||
AddCurrentPage(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddCurrentPage(1);
|
||||
}
|
||||
var diff = Equals(sender, _previousButton) ? -1 : 1;
|
||||
AddCurrentPage(diff);
|
||||
InvokeCommand();
|
||||
}
|
||||
|
||||
private void InitializePanelButtons()
|
||||
@@ -166,7 +239,7 @@ public class Pagination: TemplatedControl
|
||||
var button = new PaginationButton() { Page = i, IsVisible = true };
|
||||
_buttonPanel.Children.Add(button);
|
||||
_buttons![i - 1] = button;
|
||||
button.Click+= OnPageButtonClick;
|
||||
Button.ClickEvent.AddHandler(OnPageButtonClick, button);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,34 +260,37 @@ public class Pagination: TemplatedControl
|
||||
CurrentPage = pageButton.Page;
|
||||
}
|
||||
}
|
||||
InvokeCommand();
|
||||
}
|
||||
|
||||
private void AddCurrentPage(int pageChange)
|
||||
{
|
||||
int newValue = CurrentPage + pageChange;
|
||||
if (newValue <= 0) newValue = 1;
|
||||
else if(newValue>=PageCount) newValue = PageCount;
|
||||
CurrentPage = newValue;
|
||||
int newValue = (CurrentPage ?? 0) + pageChange;
|
||||
newValue = Clamp(newValue, 1, PageCount);
|
||||
SetCurrentValue(CurrentPageProperty, newValue);
|
||||
}
|
||||
|
||||
private void UpdateButtons(int index)
|
||||
private int Clamp(int value, int min, int max)
|
||||
{
|
||||
return value < min ? min : value > max ? max : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update Button Content and Visibility by current page.
|
||||
/// </summary>
|
||||
/// <param name="page"></param>
|
||||
private void UpdateButtonsByCurrentPage(int? page)
|
||||
{
|
||||
if (_buttonPanel is null) return;
|
||||
if (PageSize == 0) return;
|
||||
|
||||
int currentIndex = index;
|
||||
|
||||
int? currentPage = CurrentPage;
|
||||
int pageCount = TotalCount / PageSize;
|
||||
int residue = TotalCount % PageSize;
|
||||
if (residue > 0)
|
||||
{
|
||||
pageCount++;
|
||||
}
|
||||
|
||||
PageCount = pageCount;
|
||||
|
||||
int currentPage = currentIndex/ PageSize;
|
||||
if (currentPage == 0) currentPage++;
|
||||
|
||||
if (pageCount <= 7)
|
||||
{
|
||||
@@ -223,7 +299,7 @@ public class Pagination: TemplatedControl
|
||||
if (i < pageCount)
|
||||
{
|
||||
_buttons[i].IsVisible = true;
|
||||
_buttons[i].SetStatus(i + 1, i+1 == CurrentPage, false, false);
|
||||
_buttons[i].SetStatus(i + 1, i + 1 == CurrentPage, false, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -237,29 +313,28 @@ public class Pagination: TemplatedControl
|
||||
{
|
||||
_buttons[i].IsVisible = true;
|
||||
}
|
||||
int mid = currentPage;
|
||||
if (mid < 4) mid = 4;
|
||||
else if (mid > pageCount - 3) mid = pageCount - 3;
|
||||
int mid = currentPage ?? 0;
|
||||
mid = Clamp(mid, 4, pageCount - 3);
|
||||
_buttons[3].Page = mid;
|
||||
_buttons[2].Page = mid - 1;
|
||||
_buttons[4].Page = mid + 1;
|
||||
_buttons[0].Page = 1;
|
||||
_buttons[6].Page = pageCount;
|
||||
if(mid>4)
|
||||
if (mid > 4)
|
||||
{
|
||||
_buttons[1].SetStatus(0, false, true, false);
|
||||
_buttons[1].SetStatus(-1, false, true, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_buttons[1].SetStatus(mid-2, false, false, false);
|
||||
_buttons[1].SetStatus(mid - 2, false, false, false);
|
||||
}
|
||||
if(mid<pageCount-3)
|
||||
if (mid < pageCount - 3)
|
||||
{
|
||||
_buttons[5].SetStatus(0, false, false, true);
|
||||
_buttons[5].SetStatus(-1, false, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_buttons[5].SetStatus(mid+2, false, false, false);
|
||||
_buttons[5].SetStatus(mid + 2, false, false, false);
|
||||
}
|
||||
|
||||
foreach (var button in _buttons)
|
||||
@@ -275,8 +350,16 @@ public class Pagination: TemplatedControl
|
||||
}
|
||||
}
|
||||
|
||||
CurrentPage = currentPage;
|
||||
if (_previousButton != null) _previousButton.IsEnabled = CurrentPage > 1;
|
||||
if( _nextButton!=null) _nextButton.IsEnabled = CurrentPage < PageCount;
|
||||
PageCount = pageCount;
|
||||
SetCurrentValue(CurrentPageProperty, currentPage);
|
||||
if (_previousButton != null) _previousButton.IsEnabled = (CurrentPage ?? int.MaxValue) > 1;
|
||||
if (_nextButton != null) _nextButton.IsEnabled = (CurrentPage ?? 0) < PageCount;
|
||||
}
|
||||
private void InvokeCommand()
|
||||
{
|
||||
if (this.Command != null && this.Command.CanExecute(this.CommandParameter))
|
||||
{
|
||||
this.Command.Execute(this.CommandParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
309
src/Ursa/Controls/RangeSlider/RangeSlider.cs
Normal file
309
src/Ursa/Controls/RangeSlider/RangeSlider.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_Track, typeof(RangeTrack))]
|
||||
[PseudoClasses(PC_Horizontal, PC_Vertical, PC_Pressed)]
|
||||
public class RangeSlider: TemplatedControl
|
||||
{
|
||||
public const string PART_Track = "PART_Track";
|
||||
private const string PC_Horizontal= ":horizontal";
|
||||
private const string PC_Vertical = ":vertical";
|
||||
private const string PC_Pressed = ":pressed";
|
||||
|
||||
private RangeTrack? _track;
|
||||
private bool _isDragging;
|
||||
private IDisposable? _pointerPressedDisposable;
|
||||
private IDisposable? _pointerMoveDisposable;
|
||||
private IDisposable? _pointerReleasedDisposable;
|
||||
|
||||
private const double Tolerance = 0.0001;
|
||||
|
||||
public static readonly StyledProperty<double> MinimumProperty = RangeTrack.MinimumProperty.AddOwner<RangeSlider>();
|
||||
public double Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MaximumProperty = RangeTrack.MaximumProperty.AddOwner<RangeSlider>();
|
||||
public double Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> LowerValueProperty = RangeTrack.LowerValueProperty.AddOwner<RangeSlider>();
|
||||
public double LowerValue
|
||||
{
|
||||
get => GetValue(LowerValueProperty);
|
||||
set => SetValue(LowerValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> UpperValueProperty = RangeTrack.UpperValueProperty.AddOwner<RangeSlider>();
|
||||
public double UpperValue
|
||||
{
|
||||
get => GetValue(UpperValueProperty);
|
||||
set => SetValue(UpperValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> TrackWidthProperty = AvaloniaProperty.Register<RangeSlider, double>(
|
||||
nameof(TrackWidth));
|
||||
|
||||
public double TrackWidth
|
||||
{
|
||||
get => GetValue(TrackWidthProperty);
|
||||
set => SetValue(TrackWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Orientation> OrientationProperty = RangeTrack.OrientationProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsDirectionReversedProperty =
|
||||
RangeTrack.IsDirectionReversedProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public bool IsDirectionReversed
|
||||
{
|
||||
get => GetValue(IsDirectionReversedProperty);
|
||||
set => SetValue(IsDirectionReversedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> TickFrequencyProperty = AvaloniaProperty.Register<RangeSlider, double>(
|
||||
nameof(TickFrequency));
|
||||
|
||||
public double TickFrequency
|
||||
{
|
||||
get => GetValue(TickFrequencyProperty);
|
||||
set => SetValue(TickFrequencyProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<AvaloniaList<double>?> TicksProperty =
|
||||
TickBar.TicksProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public AvaloniaList<double>? Ticks
|
||||
{
|
||||
get => GetValue(TicksProperty);
|
||||
set => SetValue(TicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
|
||||
Slider.TickPlacementProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public TickPlacement TickPlacement
|
||||
{
|
||||
get => GetValue(TickPlacementProperty);
|
||||
set => SetValue(TickPlacementProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsSnapToTickProperty = AvaloniaProperty.Register<RangeSlider, bool>(
|
||||
nameof(IsSnapToTick));
|
||||
|
||||
public bool IsSnapToTick
|
||||
{
|
||||
get => GetValue(IsSnapToTickProperty);
|
||||
set => SetValue(IsSnapToTickProperty, value);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
||||
RoutedEvent.Register<RangeSlider, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
||||
{
|
||||
add => AddHandler(ValueChangedEvent, value);
|
||||
remove => RemoveHandler(ValueChangedEvent, value);
|
||||
}
|
||||
|
||||
static RangeSlider()
|
||||
{
|
||||
PressedMixin.Attach<RangeSlider>();
|
||||
FocusableProperty.OverrideDefaultValue<RangeSlider>(true);
|
||||
IsHitTestVisibleProperty.OverrideDefaultValue<RangeSlider>(true);
|
||||
OrientationProperty.OverrideDefaultValue<RangeSlider>(Orientation.Horizontal);
|
||||
OrientationProperty.Changed.AddClassHandler<RangeSlider, Orientation>((o,e)=>o.OnOrientationChanged(e));
|
||||
MinimumProperty.OverrideDefaultValue<RangeSlider>(0);
|
||||
MaximumProperty.OverrideDefaultValue<RangeSlider>(100);
|
||||
LowerValueProperty.OverrideDefaultValue<RangeSlider>(0);
|
||||
UpperValueProperty.OverrideDefaultValue<RangeSlider>(100);
|
||||
LowerValueProperty.Changed.AddClassHandler<RangeSlider, double>((o, e) => o.OnValueChanged(e, true));
|
||||
UpperValueProperty.Changed.AddClassHandler<RangeSlider, double>((o, e) => o.OnValueChanged(e, false));
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> args, bool isLower)
|
||||
{
|
||||
var oldValue = args.OldValue.Value;
|
||||
var newValue = args.NewValue.Value;
|
||||
if (Math.Abs(oldValue - newValue) > Tolerance)
|
||||
{
|
||||
RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower));
|
||||
}
|
||||
}
|
||||
|
||||
public RangeSlider()
|
||||
{
|
||||
UpdatePseudoClasses(Orientation);
|
||||
}
|
||||
|
||||
private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs<Orientation> args)
|
||||
{
|
||||
var value = args.NewValue.Value;
|
||||
UpdatePseudoClasses(value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_pointerMoveDisposable?.Dispose();
|
||||
_pointerPressedDisposable?.Dispose();
|
||||
_pointerReleasedDisposable?.Dispose();
|
||||
_track = e.NameScope.Find<RangeTrack>(PART_Track);
|
||||
_pointerMoveDisposable = this.AddDisposableHandler(PointerMovedEvent, PointerMove, RoutingStrategies.Tunnel);
|
||||
_pointerPressedDisposable = this.AddDisposableHandler(PointerPressedEvent, PointerPress, RoutingStrategies.Tunnel);
|
||||
_pointerReleasedDisposable = this.AddDisposableHandler(PointerReleasedEvent, PointerRelease, RoutingStrategies.Tunnel);
|
||||
}
|
||||
|
||||
private Thumb? _currentThumb;
|
||||
|
||||
private void PointerPress(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
var point = e.GetCurrentPoint(_track);
|
||||
_currentThumb = GetThumbByPoint(point);
|
||||
MoveToPoint(point);
|
||||
_isDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void PointerMove(object sender, PointerEventArgs args)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
_isDragging = false;
|
||||
return;
|
||||
}
|
||||
if (_isDragging)
|
||||
{
|
||||
MoveToPoint(args.GetCurrentPoint(_track));
|
||||
}
|
||||
}
|
||||
|
||||
private void PointerRelease(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_isDragging = false;
|
||||
_currentThumb = null;
|
||||
}
|
||||
|
||||
private void MoveToPoint(PointerPoint posOnTrack)
|
||||
{
|
||||
if (_track is null) return;
|
||||
var value = GetValueByPoint(posOnTrack);
|
||||
var thumb = _isDragging ? _currentThumb : GetThumbByPoint(posOnTrack);
|
||||
if (_currentThumb !=null && _currentThumb != thumb) return;
|
||||
if (thumb is null) return;
|
||||
if (thumb == _track.LowerThumb)
|
||||
{
|
||||
if (UpperValue < value) SetCurrentValue(UpperValueProperty, IsSnapToTick ? SnapToTick(value) : value);
|
||||
SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LowerValue > value) SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value);
|
||||
SetCurrentValue(UpperValueProperty, IsSnapToTick ? SnapToTick(value) : value);
|
||||
}
|
||||
}
|
||||
|
||||
private double SnapToTick(double value)
|
||||
{
|
||||
if (IsSnapToTick)
|
||||
{
|
||||
var previous = Minimum;
|
||||
var next = Maximum;
|
||||
|
||||
var ticks = Ticks;
|
||||
|
||||
if (ticks != null && ticks.Count > 0)
|
||||
{
|
||||
foreach (var tick in ticks)
|
||||
{
|
||||
if (MathUtilities.AreClose(tick, value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous))
|
||||
{
|
||||
previous = tick;
|
||||
}
|
||||
else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next))
|
||||
{
|
||||
next = tick;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (MathUtilities.GreaterThan(TickFrequency, 0.0))
|
||||
{
|
||||
previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency;
|
||||
next = Math.Min(Maximum, previous + TickFrequency);
|
||||
}
|
||||
value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private Thumb? GetThumbByPoint(PointerPoint point)
|
||||
{
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var lowerThumbPosition = isHorizontal? _track?.LowerThumb?.Bounds.Center.X : _track?.LowerThumb?.Bounds.Center.Y;
|
||||
var upperThumbPosition = isHorizontal? _track?.UpperThumb?.Bounds.Center.X : _track?.UpperThumb?.Bounds.Center.Y;
|
||||
var mid = isHorizontal? _track?.Bounds.Center.X : _track?.Bounds.Center.Y;
|
||||
var pointerPosition = isHorizontal? point.Position.X : point.Position.Y;
|
||||
|
||||
var lowerDistance = Math.Abs((lowerThumbPosition ?? 0) - pointerPosition);
|
||||
var upperDistance = Math.Abs((upperThumbPosition ?? 0) - pointerPosition);
|
||||
|
||||
if (lowerDistance<upperDistance)
|
||||
{
|
||||
return _track?.LowerThumb;
|
||||
}
|
||||
if(lowerDistance>upperDistance)
|
||||
{
|
||||
return _track?.UpperThumb;
|
||||
}
|
||||
if (IsDirectionReversed) return pointerPosition < mid ? _track?.LowerThumb : _track?.UpperThumb;
|
||||
return pointerPosition > mid ? _track?.LowerThumb : _track?.UpperThumb;
|
||||
}
|
||||
|
||||
private double GetValueByPoint(PointerPoint point)
|
||||
{
|
||||
if (_track is null) return 0;
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
|
||||
var pointPosition = isHorizontal ? point.Position.X : point.Position.Y;
|
||||
var ratio = _track.GetRatioByPoint(pointPosition);
|
||||
var range = Maximum - Minimum;
|
||||
var finalValue = ratio * range + Minimum;
|
||||
return finalValue;
|
||||
}
|
||||
|
||||
private void UpdatePseudoClasses(Orientation o)
|
||||
{
|
||||
this.PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical);
|
||||
this.PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal);
|
||||
}
|
||||
}
|
||||
506
src/Ursa/Controls/RangeSlider/RangeTrack.cs
Normal file
506
src/Ursa/Controls/RangeSlider/RangeTrack.cs
Normal file
@@ -0,0 +1,506 @@
|
||||
using System.Diagnostics;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 1. Notice that this is not used in ScrollBar, so ViewportSize related feature is not necessary.
|
||||
/// 2. Maximum, Minimum, MaxValue and MinValue are coerced there.
|
||||
/// </summary>
|
||||
[PseudoClasses(PC_Horizontal, PC_Vertical)]
|
||||
public class RangeTrack: Control
|
||||
{
|
||||
public const string PC_Horizontal = ":horizontal";
|
||||
public const string PC_Vertical = ":vertical";
|
||||
private double _density;
|
||||
private Vector _lastDrag;
|
||||
|
||||
private const double Tolerance = 0.0001;
|
||||
|
||||
public static readonly StyledProperty<double> MinimumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay);
|
||||
|
||||
public double Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MaximumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public double Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> LowerValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public double LowerValue
|
||||
{
|
||||
get => GetValue(LowerValueProperty);
|
||||
set => SetValue(LowerValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> UpperValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public double UpperValue
|
||||
{
|
||||
get => GetValue(UpperValueProperty);
|
||||
set => SetValue(UpperValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<RangeTrack, Orientation>(
|
||||
nameof(Orientation));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> UpperSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(UpperSection));
|
||||
|
||||
public Control? UpperSection
|
||||
{
|
||||
get => GetValue(UpperSectionProperty);
|
||||
set => SetValue(UpperSectionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> LowerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(LowerSection));
|
||||
|
||||
public Control? LowerSection
|
||||
{
|
||||
get => GetValue(LowerSectionProperty);
|
||||
set => SetValue(LowerSectionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> InnerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(InnerSection));
|
||||
|
||||
public Control? InnerSection
|
||||
{
|
||||
get => GetValue(InnerSectionProperty);
|
||||
set => SetValue(InnerSectionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> TrackBackgroundProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(TrackBackground));
|
||||
|
||||
public Control? TrackBackground
|
||||
{
|
||||
get => GetValue(TrackBackgroundProperty);
|
||||
set => SetValue(TrackBackgroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Thumb?> UpperThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
||||
nameof(UpperThumb));
|
||||
|
||||
public Thumb? UpperThumb
|
||||
{
|
||||
get => GetValue(UpperThumbProperty);
|
||||
set => SetValue(UpperThumbProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Thumb?> LowerThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
||||
nameof(LowerThumb));
|
||||
|
||||
public Thumb? LowerThumb
|
||||
{
|
||||
get => GetValue(LowerThumbProperty);
|
||||
set => SetValue(LowerThumbProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsDirectionReversedProperty = AvaloniaProperty.Register<RangeTrack, bool>(
|
||||
nameof(IsDirectionReversed));
|
||||
|
||||
public bool IsDirectionReversed
|
||||
{
|
||||
get => GetValue(IsDirectionReversedProperty);
|
||||
set => SetValue(IsDirectionReversedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
||||
RoutedEvent.Register<RangeTrack, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
||||
{
|
||||
add => AddHandler(ValueChangedEvent, value);
|
||||
remove => RemoveHandler(ValueChangedEvent, value);
|
||||
}
|
||||
|
||||
static RangeTrack()
|
||||
{
|
||||
OrientationProperty.Changed.AddClassHandler<RangeTrack, Orientation>((o, e) => o.OnOrientationChanged(e));
|
||||
LowerThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
||||
UpperThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
||||
LowerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||
UpperSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||
InnerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||
MinimumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMinimumChanged(e));
|
||||
MaximumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMaximumChanged(e));
|
||||
LowerValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, true));
|
||||
UpperValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, false));
|
||||
AffectsArrange<RangeTrack>(
|
||||
MinimumProperty,
|
||||
MaximumProperty,
|
||||
LowerValueProperty,
|
||||
UpperValueProperty,
|
||||
OrientationProperty,
|
||||
IsDirectionReversedProperty);
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> args, bool isLower)
|
||||
{
|
||||
var oldValue = args.OldValue.Value;
|
||||
var newValue = args.NewValue.Value;
|
||||
if (Math.Abs(oldValue - newValue) > Tolerance)
|
||||
{
|
||||
RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMinimumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
CoerceValue(MaximumProperty);
|
||||
CoerceValue(LowerValueProperty);
|
||||
CoerceValue(UpperValueProperty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMaximumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
CoerceValue(LowerValueProperty);
|
||||
CoerceValue(UpperValueProperty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSectionChanged(AvaloniaPropertyChangedEventArgs<Control?> args)
|
||||
{
|
||||
var oldSection = args.OldValue.Value;
|
||||
var newSection = args.NewValue.Value;
|
||||
if (oldSection is not null)
|
||||
{
|
||||
LogicalChildren.Remove(oldSection);
|
||||
VisualChildren.Remove(oldSection);
|
||||
}
|
||||
if (newSection is not null)
|
||||
{
|
||||
LogicalChildren.Add(newSection);
|
||||
VisualChildren.Add(newSection);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThumbChanged(AvaloniaPropertyChangedEventArgs<Thumb?> args)
|
||||
{
|
||||
var oldThumb = args.OldValue.Value;
|
||||
var newThumb = args.NewValue.Value;
|
||||
if(oldThumb is not null)
|
||||
{
|
||||
LogicalChildren.Remove(oldThumb);
|
||||
VisualChildren.Remove(oldThumb);
|
||||
}
|
||||
if (newThumb is not null)
|
||||
{
|
||||
newThumb.ZIndex = 5;
|
||||
LogicalChildren.Add(newThumb);
|
||||
VisualChildren.Add(newThumb);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs<Orientation> args)
|
||||
{
|
||||
Orientation o = args.NewValue.Value;
|
||||
PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal);
|
||||
PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical);
|
||||
}
|
||||
|
||||
private static double CoerceMaximum(AvaloniaObject sender, double value)
|
||||
{
|
||||
return ValidateDouble(value)
|
||||
? Math.Max(value, sender.GetValue(MinimumProperty))
|
||||
: sender.GetValue(MaximumProperty);
|
||||
}
|
||||
|
||||
private static double CoerceMinimum(AvaloniaObject sender, double value)
|
||||
{
|
||||
return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty);
|
||||
}
|
||||
|
||||
private static double CoerceLowerValue(AvaloniaObject sender, double value)
|
||||
{
|
||||
if (!ValidateDouble(value)) return sender.GetValue(LowerValueProperty);
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty));
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(UpperValueProperty));
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double CoerceUpperValue(AvaloniaObject sender, double value)
|
||||
{
|
||||
if (!ValidateDouble(value)) return sender.GetValue(UpperValueProperty);
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty));
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(LowerValueProperty), sender.GetValue(MaximumProperty));
|
||||
return value;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
CoerceValue(MaximumProperty);
|
||||
CoerceValue(LowerValueProperty);
|
||||
CoerceValue(UpperValueProperty);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var desiredSize = new Size();
|
||||
if (LowerThumb is not null && UpperThumb is not null)
|
||||
{
|
||||
LowerThumb.Measure(availableSize);
|
||||
UpperThumb.Measure(availableSize);
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
desiredSize = new Size(LowerThumb.DesiredSize.Width + UpperThumb.DesiredSize.Width,
|
||||
Math.Max(LowerThumb.DesiredSize.Height, UpperThumb.DesiredSize.Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredSize = new Size(Math.Max(LowerThumb.DesiredSize.Width, UpperThumb.DesiredSize.Width),
|
||||
LowerThumb.DesiredSize.Height + UpperThumb.DesiredSize.Height);
|
||||
}
|
||||
}
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var vertical = Orientation == Orientation.Vertical;
|
||||
double lowerButtonLength, innerButtonLength, upperButtonLength, lowerThumbLength, upperThumbLength;
|
||||
ComputeSliderLengths(finalSize, vertical, out lowerButtonLength, out innerButtonLength, out upperButtonLength,
|
||||
out lowerThumbLength, out upperThumbLength);
|
||||
var offset = new Point();
|
||||
var pieceSize = finalSize;
|
||||
if (vertical)
|
||||
{
|
||||
CoerceLength(ref lowerButtonLength, finalSize.Height);
|
||||
CoerceLength(ref innerButtonLength, finalSize.Height);
|
||||
CoerceLength(ref upperButtonLength, finalSize.Height);
|
||||
CoerceLength(ref lowerThumbLength, finalSize.Height);
|
||||
CoerceLength(ref upperThumbLength, finalSize.Height);
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
offset = offset.WithY(lowerThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithHeight(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + lowerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(lowerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(lowerButtonLength + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = offset.WithY(upperThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithHeight(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + upperButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(upperButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(upperButtonLength + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CoerceLength(ref lowerButtonLength, finalSize.Width);
|
||||
CoerceLength(ref innerButtonLength, finalSize.Width);
|
||||
CoerceLength(ref upperButtonLength, finalSize.Width);
|
||||
CoerceLength(ref lowerThumbLength, finalSize.Width);
|
||||
CoerceLength(ref upperThumbLength, finalSize.Width);
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
offset = offset.WithX(upperThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithWidth(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + upperButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(upperButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(upperButtonLength+innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = offset.WithX(lowerThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithWidth(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + lowerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(lowerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(lowerButtonLength + innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
}
|
||||
}
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private void ComputeSliderLengths(
|
||||
Size arrangeSize,
|
||||
bool isVertical,
|
||||
out double lowerButtonLength,
|
||||
out double innerButtonLength,
|
||||
out double upperButtonLength,
|
||||
out double lowerThumbLength,
|
||||
out double upperThumbLength)
|
||||
{
|
||||
double range = Math.Max(0, Maximum - Minimum);
|
||||
range += double.Epsilon;
|
||||
double lowerOffset = Math.Min(range, LowerValue - Minimum);
|
||||
double upperOffset = Math.Min(range, UpperValue - Minimum);
|
||||
|
||||
double trackLength;
|
||||
if (isVertical)
|
||||
{
|
||||
trackLength = arrangeSize.Height;
|
||||
lowerThumbLength = LowerThumb?.DesiredSize.Height ?? 0;
|
||||
upperThumbLength = UpperThumb?.DesiredSize.Height ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
trackLength = arrangeSize.Width;
|
||||
lowerThumbLength = LowerThumb?.DesiredSize.Width ?? 0;
|
||||
upperThumbLength = UpperThumb?.DesiredSize.Width ?? 0;
|
||||
}
|
||||
|
||||
CoerceLength(ref lowerThumbLength, trackLength);
|
||||
CoerceLength(ref upperThumbLength, trackLength);
|
||||
|
||||
double remainingLength = trackLength - lowerThumbLength * 0.5 - upperThumbLength * 0.5;
|
||||
|
||||
lowerButtonLength = remainingLength * lowerOffset / range;
|
||||
upperButtonLength = remainingLength * (range-upperOffset) / range;
|
||||
innerButtonLength = remainingLength - lowerButtonLength - upperButtonLength;
|
||||
|
||||
_density = range / remainingLength;
|
||||
}
|
||||
|
||||
private static void CoerceLength(ref double componentLength, double trackLength)
|
||||
{
|
||||
if (componentLength < 0)
|
||||
{
|
||||
componentLength = 0.0;
|
||||
}
|
||||
else if (componentLength > trackLength || double.IsNaN(componentLength))
|
||||
{
|
||||
componentLength = trackLength;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ValidateDouble(double value)
|
||||
{
|
||||
return !double.IsInfinity(value) && !double.IsNaN(value);
|
||||
}
|
||||
|
||||
internal double GetRatioByPoint(double position)
|
||||
{
|
||||
bool isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var range = isHorizontal?
|
||||
LowerSection?.Bounds.Width + InnerSection?.Bounds.Width + UpperSection?.Bounds.Width ?? double.Epsilon
|
||||
: LowerSection?.Bounds.Height + InnerSection?.Bounds.Height + UpperSection?.Bounds.Height ?? double.Epsilon;
|
||||
if (isHorizontal)
|
||||
{
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
double trackStart = UpperThumb?.Bounds.Width/2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 1.0;
|
||||
if (position > trackEnd) return 0.0;
|
||||
double diff = trackEnd - position;
|
||||
return diff / range;
|
||||
}
|
||||
else
|
||||
{
|
||||
double trackStart = LowerThumb?.Bounds.Width/2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 0.0;
|
||||
if (position > trackEnd) return 1.0;
|
||||
double diff = position - trackStart;
|
||||
return diff / range;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
double trackStart = LowerThumb?.Bounds.Height / 2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 0.0;
|
||||
if (position > trackEnd) return 1.0;
|
||||
double diff = position - trackStart;
|
||||
return diff / range;
|
||||
}
|
||||
else
|
||||
{
|
||||
double trackStart = UpperThumb?.Bounds.Height / 2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 1.0;
|
||||
if (position > trackEnd) return 0.0;
|
||||
double diff = trackEnd - position;
|
||||
return diff / range;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs
Normal file
34
src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class RangeValueChangedEventArgs: RoutedEventArgs
|
||||
{
|
||||
public double OldValue { get; set; }
|
||||
public double NewValue { get; set; }
|
||||
public bool IsLower { get; set; }
|
||||
|
||||
public RangeValueChangedEventArgs(
|
||||
RoutedEvent routedEvent,
|
||||
object source,
|
||||
double oldValue,
|
||||
double newValue,
|
||||
bool isLower = true) : base(routedEvent, source)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
IsLower = isLower;
|
||||
}
|
||||
|
||||
public RangeValueChangedEventArgs(
|
||||
RoutedEvent routedEvent,
|
||||
double oldValue,
|
||||
double newValue,
|
||||
bool isLower = true) : base(routedEvent)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
IsLower = isLower;
|
||||
}
|
||||
|
||||
}
|
||||
60
src/Ursa/Controls/ScrollTo/ScrollTo.cs
Normal file
60
src/Ursa/Controls/ScrollTo/ScrollTo.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Styling;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ScrollTo
|
||||
{
|
||||
public static readonly AttachedProperty<Position?> DirectionProperty =
|
||||
AvaloniaProperty.RegisterAttached<ScrollTo, Control, Position?>("Direction");
|
||||
|
||||
public static void SetDirection(Control obj, Position value) => obj.SetValue(DirectionProperty, value);
|
||||
public static Position? GetDirection(Control obj) => obj.GetValue(DirectionProperty);
|
||||
|
||||
public static readonly AttachedProperty<ControlTheme?> ButtonThemeProperty =
|
||||
AvaloniaProperty.RegisterAttached<ScrollTo, Control, ControlTheme?>("ButtonTheme");
|
||||
|
||||
public static void SetButtonTheme(Control obj, ControlTheme? value) => obj.SetValue(ButtonThemeProperty, value);
|
||||
public static ControlTheme? GetButtonTheme(Control obj) => obj.GetValue(ButtonThemeProperty);
|
||||
|
||||
static ScrollTo()
|
||||
{
|
||||
DirectionProperty.Changed.AddClassHandler<Control, Position?>(OnDirectionChanged);
|
||||
ButtonThemeProperty.Changed.AddClassHandler<Control, ControlTheme?>(OnButtonThemeChanged);
|
||||
}
|
||||
|
||||
private static void OnButtonThemeChanged(Control arg1, AvaloniaPropertyChangedEventArgs<ControlTheme?> arg2)
|
||||
{
|
||||
var button = EnsureButtonInAdorner(arg1);
|
||||
if (button is null) return;
|
||||
button.SetCurrentValue(StyledElement.ThemeProperty, arg2.NewValue.Value);
|
||||
}
|
||||
|
||||
private static void OnDirectionChanged(Control control, AvaloniaPropertyChangedEventArgs<Position?> args)
|
||||
{
|
||||
if (args.NewValue.Value is null) return;
|
||||
var button = EnsureButtonInAdorner(control);
|
||||
if (button is null) return;
|
||||
button.SetCurrentValue(ScrollToButton.DirectionProperty, args.NewValue.Value);
|
||||
}
|
||||
|
||||
private static ScrollToButton? EnsureButtonInAdorner(Control control)
|
||||
{
|
||||
var adorner = AdornerLayer.GetAdorner(control);
|
||||
if (adorner is not ScrollToButton button)
|
||||
{
|
||||
button = new ScrollToButton();
|
||||
AdornerLayer.SetAdorner(control, button);
|
||||
}
|
||||
button.SetCurrentValue(ScrollToButton.TargetProperty, control);
|
||||
button.SetCurrentValue(ScrollToButton.DirectionProperty, GetDirection(control));
|
||||
if ( GetButtonTheme(control) is { } theme)
|
||||
{
|
||||
button.SetCurrentValue(StyledElement.ThemeProperty, theme);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
}
|
||||
125
src/Ursa/Controls/ScrollTo/ScrollToButton.cs
Normal file
125
src/Ursa/Controls/ScrollTo/ScrollToButton.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.VisualTree;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ScrollToButton: Button
|
||||
{
|
||||
private ScrollViewer? _scroll;
|
||||
private IDisposable? _disposable;
|
||||
|
||||
public static readonly StyledProperty<Control> TargetProperty = AvaloniaProperty.Register<ScrollToButton, Control>(
|
||||
nameof(Target));
|
||||
|
||||
public Control Target
|
||||
{
|
||||
get => GetValue(TargetProperty);
|
||||
set => SetValue(TargetProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Position> DirectionProperty = AvaloniaProperty.Register<ScrollToButton, Position>(
|
||||
nameof(Direction));
|
||||
|
||||
public Position Direction
|
||||
{
|
||||
get => GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
static ScrollToButton()
|
||||
{
|
||||
TargetProperty.Changed.AddClassHandler<ScrollToButton, Control>((o,e)=>o.OnTargetChanged(e));
|
||||
DirectionProperty.Changed.AddClassHandler<ScrollToButton, Position>((o,e)=>o.OnDirectionChanged(e));
|
||||
}
|
||||
|
||||
private void OnDirectionChanged(AvaloniaPropertyChangedEventArgs<Position> avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (_scroll is null) return;
|
||||
SetVisibility(avaloniaPropertyChangedEventArgs.NewValue.Value, _scroll.Offset);
|
||||
}
|
||||
|
||||
private void OnTargetChanged(AvaloniaPropertyChangedEventArgs<Control> arg2)
|
||||
{
|
||||
_disposable?.Dispose();
|
||||
if (arg2.NewValue.Value is { } newValue)
|
||||
{
|
||||
var scroll = newValue.GetSelfAndVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
|
||||
if (_scroll is not null)
|
||||
{
|
||||
_disposable?.Dispose();
|
||||
_scroll = null;
|
||||
}
|
||||
_scroll = scroll;
|
||||
|
||||
_disposable = ScrollViewer.OffsetProperty.Changed.AddClassHandler<ScrollViewer, Vector>(OnScrollChanged);
|
||||
SetVisibility(Direction, _scroll?.Offset);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnClick()
|
||||
{
|
||||
if (_scroll is null) return;
|
||||
var vector = Direction switch
|
||||
{
|
||||
Position.Top => new Vector(0, double.NegativeInfinity),
|
||||
Position.Bottom => new Vector(0, double.PositiveInfinity),
|
||||
Position.Left => new Vector(double.NegativeInfinity, 0),
|
||||
Position.Right => new Vector(double.PositiveInfinity, 0),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
_scroll.SetCurrentValue(ScrollViewer.OffsetProperty, vector);
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
var scroll = Target.GetSelfAndVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
|
||||
if (_scroll is not null)
|
||||
{
|
||||
_disposable?.Dispose();
|
||||
_scroll = null;
|
||||
}
|
||||
_scroll = scroll;
|
||||
_disposable = ScrollViewer.OffsetProperty.Changed.AddClassHandler<ScrollViewer, Vector>(OnScrollChanged);
|
||||
SetVisibility(Direction, _scroll?.Offset);
|
||||
}
|
||||
|
||||
private void OnScrollChanged(ScrollViewer arg1, AvaloniaPropertyChangedEventArgs<Vector> arg2)
|
||||
{
|
||||
if (arg1 != _scroll) return;
|
||||
SetVisibility(Direction, arg2.NewValue.Value);
|
||||
}
|
||||
|
||||
private void SetVisibility(Position direction, Vector? vector)
|
||||
{
|
||||
if (vector is null || _scroll is null) return;
|
||||
if (direction == Position.Bottom && vector.Value.Y < _scroll.Extent.Height - _scroll.Bounds.Height)
|
||||
{
|
||||
IsVisible = true;
|
||||
}
|
||||
else if (direction == Position.Top && vector.Value.Y > 0)
|
||||
{
|
||||
IsVisible = true;
|
||||
}
|
||||
else if (direction == Position.Left && vector.Value.X > 0)
|
||||
{
|
||||
IsVisible = true;
|
||||
}
|
||||
else if (direction == Position.Right && vector.Value.X < _scroll.Extent.Width - _scroll.Bounds.Width)
|
||||
{
|
||||
IsVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/Ursa/Controls/SelectionList/SelectionList.cs
Normal file
166
src/Ursa/Controls/SelectionList/SelectionList.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Selection;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Rendering.Composition;
|
||||
using Avalonia.Rendering.Composition.Animations;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_Indicator, typeof(ContentPresenter))]
|
||||
public class SelectionList: SelectingItemsControl
|
||||
{
|
||||
public const string PART_Indicator = "PART_Indicator";
|
||||
private static readonly FuncTemplate<Panel?> DefaultPanel = new(() => new StackPanel());
|
||||
|
||||
private ImplicitAnimationCollection? _implicitAnimations;
|
||||
private ContentPresenter? _indicator;
|
||||
|
||||
public static readonly StyledProperty<Control?> IndicatorProperty = AvaloniaProperty.Register<SelectionList, Control?>(
|
||||
nameof(Indicator));
|
||||
|
||||
public Control? Indicator
|
||||
{
|
||||
get => GetValue(IndicatorProperty);
|
||||
set => SetValue(IndicatorProperty, value);
|
||||
}
|
||||
|
||||
static SelectionList()
|
||||
{
|
||||
SelectionModeProperty.OverrideMetadata<SelectionList>(
|
||||
new StyledPropertyMetadata<SelectionMode>(
|
||||
defaultValue: SelectionMode.Single,
|
||||
coerce: (o, mode) => SelectionMode.Single)
|
||||
);
|
||||
SelectedItemProperty.Changed.AddClassHandler<SelectionList, object?>((list, args) =>
|
||||
list.OnSelectedItemChanged(args));
|
||||
}
|
||||
|
||||
private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
var newValue = args.NewValue.Value;
|
||||
if (newValue is null)
|
||||
{
|
||||
OpacityProperty.SetValue(0d, _indicator);
|
||||
return;
|
||||
}
|
||||
var container = ContainerFromItem(newValue);
|
||||
if (container is null)
|
||||
{
|
||||
OpacityProperty.SetValue(0d, _indicator);
|
||||
return;
|
||||
}
|
||||
OpacityProperty.SetValue(1d, _indicator);
|
||||
InvalidateMeasure();
|
||||
InvalidateArrange();
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var size = base.ArrangeOverride(finalSize);
|
||||
if(_indicator is not null && SelectedItem is not null)
|
||||
{
|
||||
var container = ContainerFromItem(SelectedItem);
|
||||
if (container is null) return size;
|
||||
_indicator.Arrange(container.Bounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a hack. The indicator is not visible, so we arrange it to a 1x1 rectangle
|
||||
_indicator?.Arrange(new Rect(new Point(), new Size(1, 1)));
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<SelectionListItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new SelectionListItem();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_indicator= e.NameScope.Find<ContentPresenter>(PART_Indicator);
|
||||
EnsureIndicatorAnimation();
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
if(_indicator is not null && SelectedItem is not null)
|
||||
{
|
||||
var container = ContainerFromItem(SelectedItem);
|
||||
if (container is null) return;
|
||||
_indicator.Opacity = 1;
|
||||
_indicator.Arrange(container.Bounds);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureIndicatorAnimation()
|
||||
{
|
||||
if (_indicator is not null)
|
||||
{
|
||||
_indicator.Opacity = 0;
|
||||
SetUpAnimation();
|
||||
if (ElementComposition.GetElementVisual(_indicator) is { } v)
|
||||
{
|
||||
v.ImplicitAnimations = _implicitAnimations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SelectByIndex(int index)
|
||||
{
|
||||
using var operation = Selection.BatchUpdate();
|
||||
Selection.Clear();
|
||||
Selection.Select(index);
|
||||
}
|
||||
|
||||
private void SetUpAnimation()
|
||||
{
|
||||
if (_implicitAnimations != null) return;
|
||||
var compositorVisual = ElementComposition.GetElementVisual(this);
|
||||
if (compositorVisual is null) return;
|
||||
var compositor = ElementComposition.GetElementVisual(this)!.Compositor;
|
||||
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
||||
offsetAnimation.Target = nameof(CompositionVisual.Offset);
|
||||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
|
||||
offsetAnimation.Duration = TimeSpan.FromSeconds(0.3);
|
||||
var sizeAnimation = compositor.CreateVector2KeyFrameAnimation();
|
||||
sizeAnimation.Target = nameof(CompositionVisual.Size);
|
||||
sizeAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
|
||||
sizeAnimation.Duration = TimeSpan.FromSeconds(0.3);
|
||||
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
opacityAnimation.Target = nameof(CompositionVisual.Opacity);
|
||||
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
|
||||
opacityAnimation.Duration = TimeSpan.FromSeconds(0.3);
|
||||
|
||||
_implicitAnimations = compositor.CreateImplicitAnimationCollection();
|
||||
_implicitAnimations[nameof(CompositionVisual.Offset)] = offsetAnimation;
|
||||
_implicitAnimations[nameof(CompositionVisual.Size)] = sizeAnimation;
|
||||
_implicitAnimations[nameof(CompositionVisual.Opacity)] = opacityAnimation;
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
var hotkeys = Application.Current!.PlatformSettings?.HotkeyConfiguration;
|
||||
|
||||
if (e.Key.ToNavigationDirection() is { } direction && direction.IsDirectional())
|
||||
{
|
||||
e.Handled |= MoveSelection(direction, WrapSelection);
|
||||
}
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
38
src/Ursa/Controls/SelectionList/SelectionListItem.cs
Normal file
38
src/Ursa/Controls/SelectionList/SelectionListItem.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class SelectionListItem: ContentControl, ISelectable
|
||||
{
|
||||
static SelectionListItem()
|
||||
{
|
||||
SelectableMixin.Attach<SelectionListItem>(IsSelectedProperty);
|
||||
PressedMixin.Attach<SelectionListItem>();
|
||||
FocusableProperty.OverrideDefaultValue<SelectionListItem>(true);
|
||||
}
|
||||
|
||||
private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
|
||||
private Point _pointerDownPoint = s_invalidPoint;
|
||||
|
||||
public static readonly StyledProperty<bool> IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => GetValue(IsSelectedProperty);
|
||||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
if (ItemsControl.ItemsControlFromItemContaner(this) is SelectionList list)
|
||||
{
|
||||
int index = list.IndexFromContainer(this);
|
||||
list.SelectByIndex(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Ursa/Controls/Skeleton.cs
Normal file
29
src/Ursa/Controls/Skeleton.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ursa.Controls
|
||||
{
|
||||
public class Skeleton : ContentControl
|
||||
{
|
||||
|
||||
public static readonly StyledProperty<bool> IsActiveProperty =
|
||||
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsActive));
|
||||
public bool IsActive
|
||||
{
|
||||
get { return GetValue(IsActiveProperty); }
|
||||
set { SetValue(IsActiveProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsLoadingProperty =
|
||||
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsLoading));
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => GetValue(IsLoadingProperty);
|
||||
set => SetValue(IsLoadingProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/Ursa/Controls/TagInput/LostFocusBehavior.cs
Normal file
8
src/Ursa/Controls/TagInput/LostFocusBehavior.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum LostFocusBehavior
|
||||
{
|
||||
None,
|
||||
Add,
|
||||
Clear,
|
||||
}
|
||||
@@ -49,11 +49,28 @@ public class TagInput : TemplatedControl
|
||||
public TagInput()
|
||||
{
|
||||
_textBox = new TextBox();
|
||||
_textBox.AddHandler(InputElement.KeyDownEvent, OnTextBoxKeyDown, RoutingStrategies.Tunnel);
|
||||
Items = new AvaloniaList<object>();
|
||||
_textBox.AddHandler(KeyDownEvent, OnTextBoxKeyDown, RoutingStrategies.Tunnel);
|
||||
_textBox.AddHandler(LostFocusEvent, OnTextBox_LostFocus, RoutingStrategies.Bubble);
|
||||
Items = new AvaloniaList<object>
|
||||
{
|
||||
_textBox
|
||||
};
|
||||
Tags = new ObservableCollection<string>();
|
||||
}
|
||||
|
||||
private void OnTextBox_LostFocus(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
switch (LostFocusBehavior)
|
||||
{
|
||||
case LostFocusBehavior.Add:
|
||||
AddTags();
|
||||
break;
|
||||
case LostFocusBehavior.Clear:
|
||||
_textBox.Text = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ControlTheme> InputThemeProperty =
|
||||
AvaloniaProperty.Register<TagInput, ControlTheme>(
|
||||
nameof(InputTheme));
|
||||
@@ -83,6 +100,16 @@ public class TagInput : TemplatedControl
|
||||
set => SetValue(SeparatorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<LostFocusBehavior> LostFocusBehaviorProperty = AvaloniaProperty.Register<TagInput, LostFocusBehavior>(
|
||||
nameof(LostFocusBehavior));
|
||||
|
||||
public LostFocusBehavior LostFocusBehavior
|
||||
{
|
||||
get => GetValue(LostFocusBehaviorProperty);
|
||||
set => SetValue(LostFocusBehaviorProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly StyledProperty<bool> AllowDuplicatesProperty = AvaloniaProperty.Register<TagInput, bool>(
|
||||
nameof(AllowDuplicates), defaultValue: true);
|
||||
|
||||
@@ -123,7 +150,6 @@ public class TagInput : TemplatedControl
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_itemsControl = e.NameScope.Find<ItemsControl>(PART_ItemsControl);
|
||||
Items.Add(_textBox);
|
||||
}
|
||||
|
||||
private void OnInputThemePropertyChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
@@ -139,16 +165,24 @@ public class TagInput : TemplatedControl
|
||||
{
|
||||
var newTags = args.GetNewValue<IList<string>>();
|
||||
var oldTags = args.GetOldValue<IList<string>>();
|
||||
for (int i = 0; i < Items.Count - 1; i++)
|
||||
|
||||
if (Items is AvaloniaList<object> avaloniaList)
|
||||
{
|
||||
Items.RemoveAt(Items.Count - 1);
|
||||
avaloniaList.RemoveRange(0, avaloniaList.Count - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < newTags.Count; i++)
|
||||
else if (Items.Count != 0)
|
||||
{
|
||||
Items.Insert(Items.Count - 1, newTags[i]);
|
||||
Items.Clear();
|
||||
Items.Add(_textBox);
|
||||
}
|
||||
|
||||
|
||||
if (newTags != null)
|
||||
{
|
||||
for (int i = 0; i < newTags.Count; i++)
|
||||
{
|
||||
Items.Insert(Items.Count - 1, newTags[i]);
|
||||
}
|
||||
}
|
||||
if (oldTags is INotifyCollectionChanged inccold)
|
||||
{
|
||||
inccold.CollectionChanged-= OnCollectionChanged;
|
||||
@@ -187,6 +221,12 @@ public class TagInput : TemplatedControl
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
Items.Clear();
|
||||
Items.Add(_textBox);
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -194,37 +234,11 @@ public class TagInput : TemplatedControl
|
||||
{
|
||||
if (args.Key == Key.Enter)
|
||||
{
|
||||
if (_textBox.Text?.Length > 0)
|
||||
{
|
||||
string[] values;
|
||||
if (!string.IsNullOrEmpty(Separator))
|
||||
{
|
||||
values = _textBox.Text.Split(new string[] { Separator },
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
else
|
||||
{
|
||||
values = new[] { _textBox.Text };
|
||||
}
|
||||
|
||||
if (!AllowDuplicates)
|
||||
{
|
||||
values = values.Distinct().Except(Tags).ToArray();
|
||||
}
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
int index = Items.Count - 1;
|
||||
// Items.Insert(index, values[i]);
|
||||
Tags.Insert(index, values[i]);
|
||||
}
|
||||
|
||||
_textBox.Text = "";
|
||||
}
|
||||
AddTags();
|
||||
}
|
||||
else if (args.Key == Key.Delete || args.Key == Key.Back)
|
||||
{
|
||||
if (_textBox.Text?.Length == 0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_textBox.Text)||_textBox.Text?.Length == 0)
|
||||
{
|
||||
if (Tags.Count == 0)
|
||||
{
|
||||
@@ -236,6 +250,33 @@ public class TagInput : TemplatedControl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTags()
|
||||
{
|
||||
if (!(_textBox.Text?.Length > 0)) return;
|
||||
string[] values;
|
||||
if (!string.IsNullOrEmpty(Separator))
|
||||
{
|
||||
values = _textBox.Text.Split(new string[] { Separator },
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
else
|
||||
{
|
||||
values = new[] { _textBox.Text };
|
||||
}
|
||||
|
||||
if (!AllowDuplicates && Tags != null)
|
||||
values = values.Distinct().Except(Tags).ToArray();
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
int index = Items.Count - 1;
|
||||
// Items.Insert(index, values[i]);
|
||||
Tags?.Insert(index, values[i]);
|
||||
}
|
||||
|
||||
_textBox.Text = "";
|
||||
}
|
||||
|
||||
public void Close(object o)
|
||||
{
|
||||
|
||||
155
src/Ursa/Controls/ThemeSelector/ThemeSelectorBase.cs
Normal file
155
src/Ursa/Controls/ThemeSelector/ThemeSelectorBase.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Styling;
|
||||
using Ursa.Common;
|
||||
using System;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public abstract class ThemeSelectorBase: TemplatedControl
|
||||
{
|
||||
private bool _syncFromScope;
|
||||
private Application? _application;
|
||||
private ThemeVariantScope? _scope;
|
||||
|
||||
public static readonly StyledProperty<ThemeVariant?> SelectedThemeProperty = AvaloniaProperty.Register<ThemeSelectorBase, ThemeVariant?>(
|
||||
nameof(SelectedTheme));
|
||||
|
||||
public ThemeVariant? SelectedTheme
|
||||
{
|
||||
get => GetValue(SelectedThemeProperty);
|
||||
set => SetValue(SelectedThemeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ThemeSelectorMode> ModeProperty = AvaloniaProperty.Register<ThemeSelectorBase, ThemeSelectorMode>(
|
||||
nameof(Mode));
|
||||
|
||||
public ThemeSelectorMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ThemeVariantScope?> TargetScopeProperty =
|
||||
AvaloniaProperty.Register<ThemeSelectorBase, ThemeVariantScope?>(
|
||||
nameof(TargetScope));
|
||||
|
||||
public ThemeVariantScope? TargetScope
|
||||
{
|
||||
get => GetValue(TargetScopeProperty);
|
||||
set => SetValue(TargetScopeProperty, value);
|
||||
}
|
||||
|
||||
static ThemeSelectorBase()
|
||||
{
|
||||
SelectedThemeProperty.Changed.AddClassHandler<ThemeSelectorBase, ThemeVariant?>((s, e) => s.OnSelectedThemeChanged(e));
|
||||
TargetScopeProperty.Changed.AddClassHandler<ThemeSelectorBase, ThemeVariantScope?>((s, e) => s.OnTargetScopeChanged(e));
|
||||
}
|
||||
|
||||
private void OnTargetScopeChanged(AvaloniaPropertyChangedEventArgs<ThemeVariantScope?> args)
|
||||
{
|
||||
if (args.OldValue.Value is { } oldTarget)
|
||||
{
|
||||
oldTarget.ActualThemeVariantChanged -= OnScopeThemeChanged;
|
||||
}
|
||||
if (args.NewValue.Value is { } newTarget)
|
||||
{
|
||||
newTarget.ActualThemeVariantChanged += OnScopeThemeChanged;
|
||||
SyncThemeFromScope(newTarget.ActualThemeVariant);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScopeThemeChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
_syncFromScope = true;
|
||||
if (this.TargetScope is { } target)
|
||||
{
|
||||
SyncThemeFromScope(Mode == ThemeSelectorMode.Controller
|
||||
? target.RequestedThemeVariant
|
||||
: target.ActualThemeVariant);
|
||||
}
|
||||
else if (this._scope is { } scope)
|
||||
{
|
||||
SyncThemeFromScope(Mode == ThemeSelectorMode.Controller
|
||||
? scope.RequestedThemeVariant
|
||||
: scope.ActualThemeVariant);
|
||||
}
|
||||
else if (_application is { } app)
|
||||
{
|
||||
SyncThemeFromScope(Mode == ThemeSelectorMode.Controller
|
||||
? app.RequestedThemeVariant
|
||||
: app.ActualThemeVariant);
|
||||
}
|
||||
_syncFromScope = false;
|
||||
}
|
||||
|
||||
protected virtual void SyncThemeFromScope(ThemeVariant? theme)
|
||||
{
|
||||
this.SelectedTheme = theme;
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_application = Application.Current;
|
||||
_syncFromScope = true;
|
||||
if (_application is not null)
|
||||
{
|
||||
_application.ActualThemeVariantChanged += OnScopeThemeChanged;
|
||||
SyncThemeFromScope(Mode == ThemeSelectorMode.Controller
|
||||
? _application.RequestedThemeVariant
|
||||
: _application.ActualThemeVariant);
|
||||
}
|
||||
_scope = this.GetLogicalAncestors().FirstOrDefault(a => a is ThemeVariantScope) as ThemeVariantScope;
|
||||
if (_scope is not null)
|
||||
{
|
||||
_scope.ActualThemeVariantChanged += OnScopeThemeChanged;
|
||||
SyncThemeFromScope(Mode == ThemeSelectorMode.Controller
|
||||
? _scope.RequestedThemeVariant
|
||||
: _scope.ActualThemeVariant);
|
||||
}
|
||||
if (TargetScope is not null)
|
||||
{
|
||||
SyncThemeFromScope(Mode == ThemeSelectorMode.Controller
|
||||
? TargetScope.RequestedThemeVariant
|
||||
: TargetScope.ActualThemeVariant);
|
||||
}
|
||||
_syncFromScope = false;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
if (_application is not null)
|
||||
{
|
||||
_application.ActualThemeVariantChanged -= OnScopeThemeChanged;
|
||||
}
|
||||
if (_scope is not null)
|
||||
{
|
||||
_scope.ActualThemeVariantChanged -= OnScopeThemeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnSelectedThemeChanged(AvaloniaPropertyChangedEventArgs<ThemeVariant?> args)
|
||||
{
|
||||
if (_syncFromScope) return;
|
||||
ThemeVariant? newTheme = args.NewValue.Value;
|
||||
if (TargetScope is not null)
|
||||
{
|
||||
TargetScope.RequestedThemeVariant = newTheme;
|
||||
return;
|
||||
}
|
||||
if (_scope is not null)
|
||||
{
|
||||
_scope.RequestedThemeVariant = newTheme;
|
||||
return;
|
||||
}
|
||||
if (_application is not null)
|
||||
{
|
||||
_application.RequestedThemeVariant = newTheme;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/Ursa/Controls/ThemeSelector/ThemeSelectorMode.cs
Normal file
7
src/Ursa/Controls/ThemeSelector/ThemeSelectorMode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum ThemeSelectorMode
|
||||
{
|
||||
Controller,
|
||||
Indicator,
|
||||
}
|
||||
98
src/Ursa/Controls/ThemeSelector/ThemeToggleButton.cs
Normal file
98
src/Ursa/Controls/ThemeSelector/ThemeToggleButton.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_ThemeButton, typeof(Button))]
|
||||
[PseudoClasses(PC_Dark, PC_Light, PC_Default)]
|
||||
public class ThemeToggleButton: ThemeSelectorBase
|
||||
{
|
||||
public const string PART_ThemeButton = "PART_ThemeButton";
|
||||
|
||||
public const string PC_Light = ":light";
|
||||
public const string PC_Dark = ":dark";
|
||||
public const string PC_Default = ":default";
|
||||
|
||||
private Button? _button;
|
||||
private bool? _state;
|
||||
|
||||
public static readonly StyledProperty<bool> IsThreeStateProperty = AvaloniaProperty.Register<ThemeToggleButton, bool>(
|
||||
nameof(IsThreeState));
|
||||
|
||||
public bool IsThreeState
|
||||
{
|
||||
get => GetValue(IsThreeStateProperty);
|
||||
set => SetValue(IsThreeStateProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(OnButtonClicked, _button);
|
||||
_button = e.NameScope.Get<Button>(PART_ThemeButton);
|
||||
Button.ClickEvent.AddHandler(OnButtonClicked, _button);
|
||||
// ToggleButton.IsCheckedProperty.SetValue(_currentTheme == ThemeVariant.Light, _button);
|
||||
}
|
||||
|
||||
private void OnButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool? currentState = _state;
|
||||
if (IsThreeState)
|
||||
{
|
||||
_state = currentState switch
|
||||
{
|
||||
true => false,
|
||||
false => null,
|
||||
null => true,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = currentState switch
|
||||
{
|
||||
true => false,
|
||||
false => true,
|
||||
null => true,
|
||||
};
|
||||
}
|
||||
if (_state == true)
|
||||
{
|
||||
SelectedTheme = ThemeVariant.Light;
|
||||
}
|
||||
else if (_state == false)
|
||||
{
|
||||
SelectedTheme = ThemeVariant.Dark;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedTheme = ThemeVariant.Default;
|
||||
}
|
||||
|
||||
if (Mode == ThemeSelectorMode.Controller)
|
||||
{
|
||||
PseudoClasses.Set(PC_Light, SelectedTheme == ThemeVariant.Light);
|
||||
PseudoClasses.Set(PC_Dark, SelectedTheme == ThemeVariant.Dark);
|
||||
PseudoClasses.Set(PC_Default, SelectedTheme == null || SelectedTheme == ThemeVariant.Default);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SyncThemeFromScope(ThemeVariant? theme)
|
||||
{
|
||||
base.SyncThemeFromScope(theme);
|
||||
if (Mode == ThemeSelectorMode.Indicator)
|
||||
{
|
||||
PseudoClasses.Set(PC_Light, theme == ThemeVariant.Light);
|
||||
PseudoClasses.Set(PC_Dark, theme == ThemeVariant.Dark);
|
||||
PseudoClasses.Set(PC_Default, theme == null || SelectedTheme == ThemeVariant.Default);
|
||||
if (theme == ThemeVariant.Dark) _state = false;
|
||||
else if (theme == ThemeVariant.Light) _state = true;
|
||||
else _state = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
713
src/Ursa/Controls/TimeBox.cs
Normal file
713
src/Ursa/Controls/TimeBox.cs
Normal file
@@ -0,0 +1,713 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum TimeBoxInputMode
|
||||
{
|
||||
Normal,
|
||||
|
||||
// In fast mode, automatically move to next session after 2 digits input.
|
||||
Fast,
|
||||
}
|
||||
|
||||
public enum TimeBoxDragOrientation
|
||||
{
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
[TemplatePart(PART_HoursTextPresenter, typeof(TextPresenter))]
|
||||
[TemplatePart(PART_MinuteTextPresenter, typeof(TextPresenter))]
|
||||
[TemplatePart(PART_SecondTextPresenter, typeof(TextPresenter))]
|
||||
[TemplatePart(PART_MillisecondTextPresenter, typeof(TextPresenter))]
|
||||
[TemplatePart(PART_HourBorder, typeof(Border))]
|
||||
[TemplatePart(PART_MinuteBorder, typeof(Border))]
|
||||
[TemplatePart(PART_SecondBorder, typeof(Border))]
|
||||
[TemplatePart(PART_MilliSecondBorder, typeof(Border))]
|
||||
[TemplatePart(PART_HourDragPanel, typeof(Panel))]
|
||||
[TemplatePart(PART_MinuteDragPanel, typeof(Panel))]
|
||||
[TemplatePart(PART_SecondDragPanel, typeof(Panel))]
|
||||
[TemplatePart(PART_MilliSecondDragPanel, typeof(Panel))]
|
||||
public class TimeBox : TemplatedControl
|
||||
{
|
||||
public const string PART_HoursTextPresenter = "PART_HourTextPresenter";
|
||||
public const string PART_MinuteTextPresenter = "PART_MinuteTextPresenter";
|
||||
public const string PART_SecondTextPresenter = "PART_SecondTextPresenter";
|
||||
public const string PART_MillisecondTextPresenter = "PART_MillisecondTextPresenter";
|
||||
public const string PART_HourBorder = "PART_HourBorder";
|
||||
public const string PART_MinuteBorder = "PART_MinuteBorder";
|
||||
public const string PART_SecondBorder = "PART_SecondBorder";
|
||||
public const string PART_MilliSecondBorder = "PART_MilliSecondBorder";
|
||||
public const string PART_HourDragPanel = "PART_HourDragPanel";
|
||||
public const string PART_MinuteDragPanel = "PART_MinuteDragPanel";
|
||||
public const string PART_SecondDragPanel = "PART_SecondDragPanel";
|
||||
public const string PART_MilliSecondDragPanel = "PART_MilliSecondDragPanel";
|
||||
private TextPresenter? _hourText;
|
||||
private TextPresenter? _minuteText;
|
||||
private TextPresenter? _secondText;
|
||||
private TextPresenter? _milliSecondText;
|
||||
private Border? _hourBorder;
|
||||
private Border? _minuteBorder;
|
||||
private Border? _secondBorder;
|
||||
private Border? _milliSecondBorder;
|
||||
private Panel? _hourDragPanel;
|
||||
private Panel? _minuteDragPanel;
|
||||
private Panel? _secondDragPanel;
|
||||
private Panel? _milliSecondDragPanel;
|
||||
private readonly TextPresenter[] _presenters = new TextPresenter[4];
|
||||
private readonly Border[] _borders = new Border[4];
|
||||
private readonly Panel[] _dragPanels = new Panel[4];
|
||||
private readonly int[] _limits = new[] { 24, 60, 60, 1000 };
|
||||
private readonly int[] _values = new[] { 0, 0, 0, 0 };
|
||||
private readonly int[] _sectionLength = new[] { 2, 2, 2, 3 };
|
||||
private readonly bool[] _isShowedCaret = new[] { false, false, false, false };
|
||||
private int? _currentActiveSectionIndex;
|
||||
private bool _isDragging;
|
||||
private Point _pressedPosition;
|
||||
private Point? _lastDragPoint;
|
||||
|
||||
public static readonly StyledProperty<TimeSpan?> TimeProperty = AvaloniaProperty.Register<TimeBox, TimeSpan?>(
|
||||
nameof(Time), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public TimeSpan? Time
|
||||
{
|
||||
get => GetValue(TimeProperty);
|
||||
set => SetValue(TimeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
|
||||
TextBox.TextAlignmentProperty.AddOwner<TimeBox>();
|
||||
|
||||
public TextAlignment TextAlignment
|
||||
{
|
||||
get => GetValue(TextAlignmentProperty);
|
||||
set => SetValue(TextAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
|
||||
TextBox.SelectionBrushProperty.AddOwner<TimeBox>();
|
||||
|
||||
public IBrush? SelectionBrush
|
||||
{
|
||||
get => GetValue(SelectionBrushProperty);
|
||||
set => SetValue(SelectionBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
|
||||
TextBox.SelectionForegroundBrushProperty.AddOwner<TimeBox>();
|
||||
|
||||
public IBrush? SelectionForegroundBrush
|
||||
{
|
||||
get => GetValue(SelectionForegroundBrushProperty);
|
||||
set => SetValue(SelectionForegroundBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> CaretBrushProperty = TextBox.CaretBrushProperty.AddOwner<TimeBox>();
|
||||
|
||||
public IBrush? CaretBrush
|
||||
{
|
||||
get => GetValue(CaretBrushProperty);
|
||||
set => SetValue(CaretBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowLeadingZeroProperty = AvaloniaProperty.Register<TimeBox, bool>(
|
||||
nameof(ShowLeadingZero));
|
||||
|
||||
public bool ShowLeadingZero
|
||||
{
|
||||
get => GetValue(ShowLeadingZeroProperty);
|
||||
set => SetValue(ShowLeadingZeroProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TimeBoxInputMode> InputModeProperty =
|
||||
AvaloniaProperty.Register<TimeBox, TimeBoxInputMode>(
|
||||
nameof(InputMode));
|
||||
|
||||
public TimeBoxInputMode InputMode
|
||||
{
|
||||
get => GetValue(InputModeProperty);
|
||||
set => SetValue(InputModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> AllowDragProperty = AvaloniaProperty.Register<TimeBox, bool>(
|
||||
nameof(AllowDrag), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool AllowDrag
|
||||
{
|
||||
get => GetValue(AllowDragProperty);
|
||||
set => SetValue(AllowDragProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TimeBoxDragOrientation> DragOrientationProperty
|
||||
= AvaloniaProperty.Register<TimeBox, TimeBoxDragOrientation>(nameof(DragOrientation),
|
||||
defaultValue: TimeBoxDragOrientation.Horizontal);
|
||||
|
||||
public TimeBoxDragOrientation DragOrientation
|
||||
{
|
||||
get => GetValue(DragOrientationProperty);
|
||||
set => SetValue(DragOrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsTimeLoopProperty = AvaloniaProperty.Register<TimeBox, bool>(
|
||||
nameof(IsTimeLoop), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool IsTimeLoop
|
||||
{
|
||||
get => GetValue(IsTimeLoopProperty);
|
||||
set => SetValue(IsTimeLoopProperty, value);
|
||||
}
|
||||
|
||||
static TimeBox()
|
||||
{
|
||||
ShowLeadingZeroProperty.Changed.AddClassHandler<TimeBox>((o, e) => o.OnFormatChange(e));
|
||||
TimeProperty.Changed.AddClassHandler<TimeBox>((o, e) => o.OnTimeChanged(e));
|
||||
AllowDragProperty.Changed.AddClassHandler<TimeBox, bool>((o, e) => o.OnAllowDragChanged(e));
|
||||
}
|
||||
|
||||
#region Overrides
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_hourText = e.NameScope.Get<TextPresenter>(PART_HoursTextPresenter);
|
||||
_minuteText = e.NameScope.Get<TextPresenter>(PART_MinuteTextPresenter);
|
||||
_secondText = e.NameScope.Get<TextPresenter>(PART_SecondTextPresenter);
|
||||
_milliSecondText = e.NameScope.Get<TextPresenter>(PART_MillisecondTextPresenter);
|
||||
_hourBorder = e.NameScope.Get<Border>(PART_HourBorder);
|
||||
_minuteBorder = e.NameScope.Get<Border>(PART_MinuteBorder);
|
||||
_secondBorder = e.NameScope.Get<Border>(PART_SecondBorder);
|
||||
_milliSecondBorder = e.NameScope.Get<Border>(PART_MilliSecondBorder);
|
||||
_hourDragPanel = e.NameScope.Get<Panel>(PART_HourDragPanel);
|
||||
_minuteDragPanel = e.NameScope.Get<Panel>(PART_MinuteDragPanel);
|
||||
_secondDragPanel = e.NameScope.Get<Panel>(PART_SecondDragPanel);
|
||||
_milliSecondDragPanel = e.NameScope.Get<Panel>(PART_MilliSecondDragPanel);
|
||||
|
||||
_presenters[0] = _hourText;
|
||||
_presenters[1] = _minuteText;
|
||||
_presenters[2] = _secondText;
|
||||
_presenters[3] = _milliSecondText;
|
||||
_borders[0] = _hourBorder;
|
||||
_borders[1] = _minuteBorder;
|
||||
_borders[2] = _secondBorder;
|
||||
_borders[3] = _milliSecondBorder;
|
||||
_dragPanels[0] = _hourDragPanel;
|
||||
_dragPanels[1] = _minuteDragPanel;
|
||||
_dragPanels[2] = _secondDragPanel;
|
||||
_dragPanels[3] = _milliSecondDragPanel;
|
||||
IsVisibleProperty.SetValue(AllowDrag, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]);
|
||||
|
||||
_hourText.Text = Time != null ? Time.Value.Hours.ToString() : "0";
|
||||
_minuteText.Text = Time != null ? Time.Value.Minutes.ToString() : "0";
|
||||
_secondText.Text = Time != null ? Time.Value.Seconds.ToString() : "0";
|
||||
//_milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0";
|
||||
_milliSecondText.Text = Time != null ? Time.Value.Milliseconds.ToString() : "0";
|
||||
ParseTimeSpan(ShowLeadingZero);
|
||||
|
||||
PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels[0], _dragPanels[1], _dragPanels[2],
|
||||
_dragPanels[3]);
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
var keymap = TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration;
|
||||
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
|
||||
if (keymap is not null && Match(keymap.SelectAll))
|
||||
{
|
||||
_presenters[_currentActiveSectionIndex.Value].SelectionStart = 0;
|
||||
_presenters[_currentActiveSectionIndex.Value].SelectionEnd =
|
||||
_presenters[_currentActiveSectionIndex.Value].Text?.Length ?? 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key is Key.Enter or Key.Return)
|
||||
{
|
||||
ParseTimeSpan(ShowLeadingZero);
|
||||
SetTimeSpanInternal();
|
||||
base.OnKeyDown(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Tab)
|
||||
{
|
||||
if (_currentActiveSectionIndex.Value == 3)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
return;
|
||||
}
|
||||
|
||||
MoveToNextSection(_currentActiveSectionIndex.Value);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Back)
|
||||
{
|
||||
DeleteImplementation(_currentActiveSectionIndex.Value);
|
||||
}
|
||||
else if (e.Key == Key.Right)
|
||||
{
|
||||
OnPressRightKey();
|
||||
}
|
||||
else if (e.Key == Key.Left)
|
||||
{
|
||||
OnPressLeftKey();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTextInput(TextInputEventArgs e)
|
||||
{
|
||||
if (e.Handled) return;
|
||||
string? s = e.Text;
|
||||
if (string.IsNullOrEmpty(s)) return;
|
||||
if (!char.IsNumber(s![0])) return;
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
|
||||
int caretIndex = Math.Min(_presenters[_currentActiveSectionIndex.Value].CaretIndex
|
||||
, _presenters[_currentActiveSectionIndex.Value].Text?.Length ?? 0);
|
||||
|
||||
if (_presenters[_currentActiveSectionIndex.Value].Text is null)
|
||||
{
|
||||
_presenters[_currentActiveSectionIndex.Value].Text = s;
|
||||
_presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal();
|
||||
}
|
||||
else
|
||||
{
|
||||
_presenters[_currentActiveSectionIndex.Value].DeleteSelection();
|
||||
_presenters[_currentActiveSectionIndex.Value].ClearSelection();
|
||||
string oldText = _presenters[_currentActiveSectionIndex.Value].Text ?? string.Empty;
|
||||
string newText = oldText.Length == 0
|
||||
? s
|
||||
: oldText.Substring(0, caretIndex) + s + oldText.Substring(Math.Min(caretIndex, oldText.Length));
|
||||
|
||||
// Limit the maximum number of input digits
|
||||
if (newText.Length > _sectionLength[_currentActiveSectionIndex.Value])
|
||||
{
|
||||
newText = newText.Substring(0, _sectionLength[_currentActiveSectionIndex.Value]);
|
||||
}
|
||||
|
||||
_presenters[_currentActiveSectionIndex.Value].Text = newText;
|
||||
_presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal();
|
||||
if (_presenters[_currentActiveSectionIndex.Value].CaretIndex == 2 && InputMode == TimeBoxInputMode.Fast)
|
||||
{
|
||||
MoveToNextSection(_currentActiveSectionIndex.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
_pressedPosition = e.GetPosition(_hourBorder);
|
||||
_lastDragPoint = _pressedPosition;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
if (!_borders[i].Bounds.Contains(_pressedPosition))
|
||||
{
|
||||
LeaveSection(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
_currentActiveSectionIndex = i;
|
||||
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
EnterSection(_currentActiveSectionIndex.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_dragPanels[_currentActiveSectionIndex.Value].IsVisible)
|
||||
{
|
||||
MoveCaret(_currentActiveSectionIndex.Value);
|
||||
}
|
||||
}
|
||||
e.Pointer.Capture(_presenters[_currentActiveSectionIndex.Value]);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
if (_isDragging)
|
||||
{
|
||||
_isDragging = false;
|
||||
_lastDragPoint = null;
|
||||
return;
|
||||
}
|
||||
if(_dragPanels[_currentActiveSectionIndex.Value].IsVisible)
|
||||
{
|
||||
EnterSection(_currentActiveSectionIndex.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLostFocus(RoutedEventArgs e)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
LeaveSection(i);
|
||||
}
|
||||
|
||||
_currentActiveSectionIndex = null;
|
||||
ParseTimeSpan(ShowLeadingZero);
|
||||
SetTimeSpanInternal();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnFormatChange(AvaloniaPropertyChangedEventArgs arg)
|
||||
{
|
||||
// this function will be call ahead of OnApplyTemplate() if Set ShowLeadingZero in axaml, so that _xxxText could be null
|
||||
if (_hourText is null || _minuteText is null || _secondText is null || _milliSecondText is null)
|
||||
return;
|
||||
|
||||
bool showLeadingZero = arg.GetNewValue<bool>();
|
||||
ParseTimeSpan(showLeadingZero);
|
||||
}
|
||||
|
||||
private void OnAllowDragChanged(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]);
|
||||
}
|
||||
|
||||
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs arg)
|
||||
{
|
||||
// this function will be call ahead of OnApplyTemplate() if bind Time in axaml, so that _xxxText could be null
|
||||
if (_hourText is null || _minuteText is null || _secondText is null || _milliSecondText is null)
|
||||
return;
|
||||
|
||||
TimeSpan? timeSpan = arg.GetNewValue<TimeSpan?>();
|
||||
if (timeSpan is null)
|
||||
{
|
||||
_hourText.Text = String.Empty;
|
||||
_minuteText.Text = String.Empty;
|
||||
_secondText.Text = String.Empty;
|
||||
_milliSecondText.Text = String.Empty;
|
||||
ParseTimeSpan(ShowLeadingZero);
|
||||
}
|
||||
else
|
||||
{
|
||||
_hourText.Text = timeSpan.Value.Hours.ToString();
|
||||
_minuteText.Text = timeSpan.Value.Minutes.ToString();
|
||||
_secondText.Text = timeSpan.Value.Seconds.ToString();
|
||||
//_milliSecondText.Text = ClampMilliSecond(timeSpan.Value.Milliseconds).ToString();
|
||||
_milliSecondText.Text = timeSpan.Value.Milliseconds.ToString();
|
||||
ParseTimeSpan(ShowLeadingZero);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseTimeSpan(bool showLeadingZero, bool skipParseFromText = false)
|
||||
{
|
||||
string format = showLeadingZero ? "D2" : "";
|
||||
string millisecondformat = showLeadingZero ? "D3" : "";
|
||||
|
||||
if (!skipParseFromText)
|
||||
{
|
||||
_values[0] = int.TryParse(_hourText?.Text, out int hour) ? hour : 0;
|
||||
_values[1] = int.TryParse(_minuteText?.Text, out int minute) ? minute : 0;
|
||||
_values[2] = int.TryParse(_secondText?.Text, out int second) ? second : 0;
|
||||
_values[3] = int.TryParse(_milliSecondText?.Text, out int millisecond) ? millisecond : 0;
|
||||
}
|
||||
|
||||
VerifyTimeValue();
|
||||
|
||||
_hourText?.SetValue(TextPresenter.TextProperty, _values[0].ToString(format));
|
||||
_minuteText?.SetValue(TextPresenter.TextProperty, _values[1].ToString(format));
|
||||
_secondText?.SetValue(TextPresenter.TextProperty, _values[2].ToString(format));
|
||||
_milliSecondText?.SetValue(TextPresenter.TextProperty, _values[3].ToString(millisecondformat));
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (!AllowDrag) return;
|
||||
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
|
||||
var point = e.GetPosition(this);
|
||||
var delta = point - _lastDragPoint;
|
||||
if (delta is null) return;
|
||||
int d = GetDelta(delta.Value);
|
||||
if (d > 0)
|
||||
{
|
||||
Increase();
|
||||
_isDragging = true;
|
||||
}
|
||||
else if (d < 0)
|
||||
{
|
||||
Decrease();
|
||||
_isDragging = true;
|
||||
}
|
||||
|
||||
_lastDragPoint = point;
|
||||
}
|
||||
|
||||
private int GetDelta(Point point)
|
||||
{
|
||||
switch (DragOrientation)
|
||||
{
|
||||
case TimeBoxDragOrientation.Horizontal:
|
||||
return point.X switch
|
||||
{
|
||||
> 0 => 1,
|
||||
< 0 => -1,
|
||||
_ => 0
|
||||
};
|
||||
case TimeBoxDragOrientation.Vertical:
|
||||
return point.Y switch
|
||||
{
|
||||
> 0 => -1,
|
||||
< 0 => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set dragPanel IsVisible to false if AllowDrag is true, and select all text in the section
|
||||
/// </summary>
|
||||
/// <param name="index">The index of section that will be enter</param>
|
||||
private void EnterSection(int index)
|
||||
{
|
||||
if (index < 0 || index > 3) return;
|
||||
|
||||
if (AllowDrag)
|
||||
_dragPanels[index].IsVisible = false;
|
||||
ShowCaretInteral(index);
|
||||
_presenters[index].SelectAll();
|
||||
}
|
||||
|
||||
private void MoveCaret(int index)
|
||||
{
|
||||
if (!_isShowedCaret[index])
|
||||
{
|
||||
ShowCaretInteral(index);
|
||||
}
|
||||
_presenters[index].ClearSelection();
|
||||
var caretPosition =
|
||||
_pressedPosition.WithX(_pressedPosition.X - _borders[index].Bounds.X);
|
||||
_presenters[index].MoveCaretToPoint(caretPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set dragPanel IsVisible to true if AllowDrag is true, and clear selection in the section
|
||||
/// </summary>
|
||||
/// <param name="index">The index of section that will be leave</param>
|
||||
private void LeaveSection(int index)
|
||||
{
|
||||
if (index < 0 || index > 3) return;
|
||||
_presenters[index].ClearSelection();
|
||||
if (_isShowedCaret[index])
|
||||
{
|
||||
HideCaretInteral(index);
|
||||
}
|
||||
|
||||
if (AllowDrag)
|
||||
_dragPanels[index].IsVisible = true;
|
||||
}
|
||||
|
||||
private bool MoveToNextSection(int index)
|
||||
{
|
||||
if (index < 0 || index >= 3) return false;
|
||||
LeaveSection(index);
|
||||
_currentActiveSectionIndex = index + 1;
|
||||
EnterSection(_currentActiveSectionIndex.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MoveToPreviousSection(int index)
|
||||
{
|
||||
if (index <= 0 || index > 3) return false;
|
||||
LeaveSection(index);
|
||||
_currentActiveSectionIndex = index - 1;
|
||||
EnterSection(_currentActiveSectionIndex.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPressRightKey()
|
||||
{
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
var index = _currentActiveSectionIndex.Value;
|
||||
if (_presenters[index].IsTextSelected())
|
||||
{
|
||||
int end = _presenters[index].SelectionEnd;
|
||||
_presenters[index].ClearSelection();
|
||||
_presenters[index].MoveCaretToTextPosition(end);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_presenters[index].CaretIndex >= _presenters[index].Text?.Length)
|
||||
{
|
||||
MoveToNextSection(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
_presenters[index].ClearSelection();
|
||||
_presenters[index].CaretIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPressLeftKey()
|
||||
{
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
var index = _currentActiveSectionIndex.Value;
|
||||
if (_presenters[index].IsTextSelected())
|
||||
{
|
||||
int start = _presenters[index].SelectionStart;
|
||||
_presenters[index].ClearSelection();
|
||||
_presenters[index].MoveCaretToTextPosition(start);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_presenters[index].CaretIndex == 0)
|
||||
{
|
||||
MoveToPreviousSection(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
_presenters[index].ClearSelection();
|
||||
_presenters[index].CaretIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTimeSpanInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
//Time = new TimeSpan(0, _values[0], _values[1], _values[2], _values[3] * 10);
|
||||
Time = new TimeSpan(0, _values[0], _values[1], _values[2], _values[3]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Time = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteImplementation(int index)
|
||||
{
|
||||
if (index < 0 || index > 3) return;
|
||||
var oldText = _presenters[index].Text;
|
||||
if (_presenters[index].SelectionStart != _presenters[index].SelectionEnd)
|
||||
{
|
||||
_presenters[index].DeleteSelection();
|
||||
_presenters[index].ClearSelection();
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(oldText) || _presenters[index].CaretIndex == 0)
|
||||
{
|
||||
MoveToPreviousSection(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
int caretIndex = _presenters[index].CaretIndex;
|
||||
string newText = oldText?.Substring(0, caretIndex - 1) +
|
||||
oldText?.Substring(Math.Min(caretIndex, oldText.Length));
|
||||
_presenters[index].MoveCaretHorizontal(LogicalDirection.Backward);
|
||||
_presenters[index].Text = newText;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandlingCarry(int index, int lowerCarry = 0)
|
||||
{
|
||||
if (index < 0)
|
||||
return IsTimeLoop;
|
||||
_values[index] += lowerCarry;
|
||||
int carry = _values[index] >= 0 ? _values[index] / _limits[index] : -1 + (_values[index] / _limits[index]);
|
||||
if (carry == 0) return true;
|
||||
bool success = false;
|
||||
if (carry > 0)
|
||||
{
|
||||
success = HandlingCarry(index - 1, carry);
|
||||
if (success)
|
||||
{
|
||||
_values[index] %= _limits[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
_values[index] = _limits[index] - 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = HandlingCarry(index - 1, carry);
|
||||
if (success)
|
||||
{
|
||||
_values[index] += _limits[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
_values[index] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void VerifyTimeValue()
|
||||
{
|
||||
for (int i = 3; i >= 0; --i)
|
||||
{
|
||||
HandlingCarry(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void Increase()
|
||||
{
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
if (_currentActiveSectionIndex.Value == 0)
|
||||
_values[0] += 1;
|
||||
else if (_currentActiveSectionIndex.Value == 1)
|
||||
_values[1] += 1;
|
||||
else if (_currentActiveSectionIndex.Value == 2)
|
||||
_values[2] += 1;
|
||||
else if (_currentActiveSectionIndex.Value == 3)
|
||||
_values[3] += 1;
|
||||
ParseTimeSpan(ShowLeadingZero, true);
|
||||
//SetTimeSpanInternal();
|
||||
}
|
||||
|
||||
private void Decrease()
|
||||
{
|
||||
if (_currentActiveSectionIndex is null) return;
|
||||
if (_currentActiveSectionIndex.Value == 0)
|
||||
_values[0] -= 1;
|
||||
else if (_currentActiveSectionIndex.Value == 1)
|
||||
_values[1] -= 1;
|
||||
else if (_currentActiveSectionIndex.Value == 2)
|
||||
_values[2] -= 1;
|
||||
else if (_currentActiveSectionIndex.Value == 3)
|
||||
_values[3] -= 1;
|
||||
ParseTimeSpan(ShowLeadingZero, true);
|
||||
//SetTimeSpanInternal();
|
||||
}
|
||||
|
||||
private int ClampMilliSecond(int milliSecond)
|
||||
{
|
||||
while (milliSecond % 100 != milliSecond)
|
||||
{
|
||||
milliSecond /= 10;
|
||||
}
|
||||
|
||||
return milliSecond;
|
||||
}
|
||||
|
||||
private void ShowCaretInteral(int index)
|
||||
{
|
||||
_presenters[index].ShowCaret();
|
||||
_isShowedCaret[index] = true;
|
||||
}
|
||||
|
||||
private void HideCaretInteral(int index)
|
||||
{
|
||||
_presenters[index].HideCaret();
|
||||
_isShowedCaret[index] = false;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,220 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Generators;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class Timeline: ItemsControl
|
||||
{
|
||||
private static readonly FuncTemplate<Panel?> DefaultPanel = new((Func<Panel>)(() => new TimelinePanel()));
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> ItemDescriptionTemplateProperty = AvaloniaProperty.Register<Timeline, IDataTemplate?>(
|
||||
nameof(ItemDescriptionTemplate));
|
||||
public static readonly StyledProperty<IBinding?> IconMemberBindingProperty = AvaloniaProperty.Register<Timeline, IBinding?>(
|
||||
nameof(IconMemberBinding));
|
||||
|
||||
public IDataTemplate? ItemDescriptionTemplate
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? IconMemberBinding
|
||||
{
|
||||
get => GetValue(ItemDescriptionTemplateProperty);
|
||||
set => SetValue(ItemDescriptionTemplateProperty, value);
|
||||
get => GetValue(IconMemberBindingProperty);
|
||||
set => SetValue(IconMemberBindingProperty, value);
|
||||
}
|
||||
|
||||
public Timeline()
|
||||
public static readonly StyledProperty<IBinding?> HeaderMemberBindingProperty = AvaloniaProperty.Register<Timeline, IBinding?>(
|
||||
nameof(HeaderMemberBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? HeaderMemberBinding
|
||||
{
|
||||
ItemsView.CollectionChanged+=ItemsViewOnCollectionChanged;
|
||||
get => GetValue(HeaderMemberBindingProperty);
|
||||
set => SetValue(HeaderMemberBindingProperty, value);
|
||||
}
|
||||
|
||||
private void ItemsViewOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
public static readonly StyledProperty<IBinding?> ContentMemberBindingProperty = AvaloniaProperty.Register<Timeline, IBinding?>(
|
||||
nameof(ContentMemberBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? ContentMemberBinding
|
||||
{
|
||||
RefreshTimelineItems();
|
||||
get => GetValue(ContentMemberBindingProperty);
|
||||
set => SetValue(ContentMemberBindingProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<Timeline, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
public static readonly StyledProperty<IDataTemplate?> DescriptionTemplateProperty = AvaloniaProperty.Register<Timeline, IDataTemplate?>(
|
||||
nameof(DescriptionTemplate));
|
||||
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IDataTemplate? DescriptionTemplate
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
RefreshTimelineItems();
|
||||
get => GetValue(DescriptionTemplateProperty);
|
||||
set => SetValue(DescriptionTemplateProperty, value);
|
||||
}
|
||||
|
||||
private void RefreshTimelineItems()
|
||||
public static readonly StyledProperty<IBinding?> TimeMemberBindingProperty = AvaloniaProperty.Register<Timeline, IBinding?>(
|
||||
nameof(TimeMemberBinding));
|
||||
|
||||
[AssignBinding]
|
||||
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||
public IBinding? TimeMemberBinding
|
||||
{
|
||||
for (int i = 0; i < this.LogicalChildren.Count; i++)
|
||||
get => GetValue(TimeMemberBindingProperty);
|
||||
set => SetValue(TimeMemberBindingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> TimeFormatProperty = AvaloniaProperty.Register<Timeline, string?>(
|
||||
nameof(TimeFormat), defaultValue:"yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public string? TimeFormat
|
||||
{
|
||||
get => GetValue(TimeFormatProperty);
|
||||
set => SetValue(TimeFormatProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly StyledProperty<TimelineDisplayMode> ModeProperty = AvaloniaProperty.Register<Timeline, TimelineDisplayMode>(
|
||||
nameof(Mode));
|
||||
|
||||
public TimelineDisplayMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
static Timeline()
|
||||
{
|
||||
ItemsPanelProperty.OverrideDefaultValue<Timeline>(DefaultPanel);
|
||||
ModeProperty.Changed.AddClassHandler<Timeline, TimelineDisplayMode>((t, e) => { t.OnDisplayModeChanged(e); });
|
||||
}
|
||||
|
||||
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs<TimelineDisplayMode> e)
|
||||
{
|
||||
if (this.ItemsPanelRoot is TimelinePanel panel)
|
||||
{
|
||||
if (this.LogicalChildren[i] is TimelineItem t)
|
||||
panel.Mode = e.NewValue.Value;
|
||||
SetItemMode();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
recycleKey = null;
|
||||
return item is not TimelineItem;
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
if (item is TimelineItem t) return t;
|
||||
return new TimelineItem();
|
||||
}
|
||||
|
||||
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||
{
|
||||
base.PrepareContainerForItemOverride(container, item, index);
|
||||
if (container is TimelineItem t)
|
||||
{
|
||||
bool start = index == 0;
|
||||
bool end = index == ItemCount - 1;
|
||||
t.SetEnd(start, end);
|
||||
if (IconMemberBinding is not null)
|
||||
{
|
||||
t.SetIndex(i == 0, i == this.LogicalChildren.Count - 1);
|
||||
t.Bind(TimelineItem.IconProperty, IconMemberBinding);
|
||||
}
|
||||
else if (this.LogicalChildren[i] is ContentPresenter { Child: TimelineItem t2 })
|
||||
if (HeaderMemberBinding != null)
|
||||
{
|
||||
t2.SetIndex(i == 0, i == this.LogicalChildren.Count - 1);
|
||||
t.Bind(HeaderedContentControl.HeaderProperty, HeaderMemberBinding);
|
||||
}
|
||||
if (ContentMemberBinding != null)
|
||||
{
|
||||
t.Bind(ContentControl.ContentProperty, ContentMemberBinding);
|
||||
}
|
||||
if (TimeMemberBinding != null)
|
||||
{
|
||||
t.Bind(TimelineItem.TimeProperty, TimeMemberBinding);
|
||||
}
|
||||
|
||||
t.SetIfUnset(TimelineItem.TimeFormatProperty, TimeFormat);
|
||||
t.SetIfUnset(TimelineItem.IconTemplateProperty, IconTemplate);
|
||||
t.SetIfUnset(HeaderedContentControl.HeaderTemplateProperty, ItemTemplate);
|
||||
t.SetIfUnset(ContentControl.ContentTemplateProperty, DescriptionTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var panel = this.ItemsPanelRoot as TimelinePanel;
|
||||
panel.Mode = this.Mode;
|
||||
SetItemMode();
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
|
||||
private void SetItemMode()
|
||||
{
|
||||
if (ItemsPanelRoot is TimelinePanel panel)
|
||||
{
|
||||
var items = panel.Children.OfType<TimelineItem>();
|
||||
if (Mode == TimelineDisplayMode.Left)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Left);
|
||||
}
|
||||
}
|
||||
else if (Mode == TimelineDisplayMode.Right)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Right);
|
||||
}
|
||||
}
|
||||
else if (Mode == TimelineDisplayMode.Center)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Separate);
|
||||
}
|
||||
}
|
||||
else if (Mode == TimelineDisplayMode.Alternate)
|
||||
{
|
||||
bool left = false;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (left)
|
||||
{
|
||||
SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Left);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Right);
|
||||
}
|
||||
left = !left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetIfUnset<T>(AvaloniaObject target, StyledProperty<T> property, T value)
|
||||
{
|
||||
if (!target.IsSet(property))
|
||||
target.SetCurrentValue(property, value);
|
||||
}
|
||||
}
|
||||
22
src/Ursa/Controls/Timeline/TimelineDisplayMode.cs
Normal file
22
src/Ursa/Controls/Timeline/TimelineDisplayMode.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum TimelineDisplayMode
|
||||
{
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
Alternate,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placement of timeline.
|
||||
/// Left means line is placed left to TimelineItem content.
|
||||
/// Right means line is placed right to TimelineItem content.
|
||||
/// Separate means line is placed between TimelineItem content and time.
|
||||
/// </summary>
|
||||
public enum TimelineItemPosition
|
||||
{
|
||||
Left,
|
||||
Right,
|
||||
Separate,
|
||||
}
|
||||
@@ -1,45 +1,106 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_First, PC_Last, PC_Default, PC_Ongoing, PC_Success, PC_Warning, PC_Error, PC_None)]
|
||||
public class TimelineItem: ContentControl
|
||||
[PseudoClasses(PC_First, PC_Last, PC_EmptyIcon, PC_AllLeft, PC_AllRight, PC_Separate)]
|
||||
[TemplatePart(PART_Header, typeof(ContentPresenter))]
|
||||
[TemplatePart(PART_Icon, typeof(Panel))]
|
||||
[TemplatePart(PART_Content, typeof(ContentPresenter))]
|
||||
[TemplatePart(PART_Time, typeof(TextBlock))]
|
||||
[TemplatePart(PART_RootGrid, typeof(Grid))]
|
||||
public class TimelineItem: HeaderedContentControl
|
||||
{
|
||||
private const string PC_First = ":first";
|
||||
private const string PC_Last = ":last";
|
||||
private const string PC_Default = ":default";
|
||||
private const string PC_Ongoing = ":ongoing";
|
||||
private const string PC_Success = ":success";
|
||||
private const string PC_Warning = ":warning";
|
||||
private const string PC_Error = ":error";
|
||||
private const string PC_None = ":none";
|
||||
public const string PC_First = ":first";
|
||||
public const string PC_Last = ":last";
|
||||
public const string PC_EmptyIcon = ":empty-icon";
|
||||
public const string PC_AllLeft=":all-left";
|
||||
public const string PC_AllRight=":all-right";
|
||||
public const string PC_Separate = ":separate";
|
||||
public const string PART_Header = "PART_Header";
|
||||
public const string PART_Icon = "PART_Icon";
|
||||
public const string PART_Content = "PART_Content";
|
||||
public const string PART_Time = "PART_Time";
|
||||
public const string PART_RootGrid = "PART_RootGrid";
|
||||
|
||||
private ContentPresenter? _headerPresenter;
|
||||
private Panel? _iconPresenter;
|
||||
private ContentPresenter? _contentPresenter;
|
||||
private TextBlock? _timePresenter;
|
||||
private Grid? _rootGrid;
|
||||
|
||||
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<TimelineItem, object?>(
|
||||
nameof(Icon));
|
||||
|
||||
private static readonly IReadOnlyDictionary<TimelineItemType, string> _itemTypeMapping = new Dictionary<TimelineItemType, string>
|
||||
public object? Icon
|
||||
{
|
||||
{TimelineItemType.Default, PC_Default},
|
||||
{TimelineItemType.Ongoing, PC_Ongoing},
|
||||
{TimelineItemType.Success, PC_Success},
|
||||
{TimelineItemType.Warning, PC_Warning},
|
||||
{TimelineItemType.Error, PC_Error},
|
||||
};
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush> IconForegroundProperty =
|
||||
AvaloniaProperty.Register<TimelineItem, IBrush>(nameof(IconForeground));
|
||||
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty = AvaloniaProperty.Register<TimelineItem, IDataTemplate?>(
|
||||
nameof(IconTemplate));
|
||||
|
||||
public IBrush IconForeground
|
||||
public IDataTemplate? IconTemplate
|
||||
{
|
||||
get => GetValue(IconForegroundProperty);
|
||||
set => SetValue(IconForegroundProperty, value);
|
||||
get => GetValue(IconTemplateProperty);
|
||||
set => SetValue(IconTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TimelineItemType> TypeProperty = AvaloniaProperty.Register<TimelineItem, TimelineItemType>(
|
||||
nameof(Type));
|
||||
|
||||
public TimelineItemType Type
|
||||
{
|
||||
get => GetValue(TypeProperty);
|
||||
set => SetValue(TypeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TimelineItemPosition> PositionProperty = AvaloniaProperty.Register<TimelineItem, TimelineItemPosition>(
|
||||
nameof(Position), defaultValue: TimelineItemPosition.Right);
|
||||
|
||||
public TimelineItemPosition Position
|
||||
{
|
||||
get => GetValue(PositionProperty);
|
||||
set => SetValue(PositionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<TimelineItem, double> LeftWidthProperty = AvaloniaProperty.RegisterDirect<TimelineItem, double>(
|
||||
nameof(LeftWidth), o => o.LeftWidth, (o, v) => o.LeftWidth = v);
|
||||
private double _leftWidth;
|
||||
public double LeftWidth
|
||||
{
|
||||
get => _leftWidth;
|
||||
set => SetAndRaise(LeftWidthProperty, ref _leftWidth, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<TimelineItem, double> IconWidthProperty = AvaloniaProperty.RegisterDirect<TimelineItem, double>(
|
||||
nameof(IconWidth), o => o.IconWidth, (o, v) => o.IconWidth = v);
|
||||
private double _iconWidth;
|
||||
public double IconWidth
|
||||
{
|
||||
get => _iconWidth;
|
||||
set => SetAndRaise(IconWidthProperty, ref _iconWidth, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<TimelineItem, double> RightWidthProperty = AvaloniaProperty.RegisterDirect<TimelineItem, double>(
|
||||
nameof(RightWidth), o => o.RightWidth, (o, v) => o.RightWidth = v);
|
||||
private double _rightWidth;
|
||||
public double RightWidth
|
||||
{
|
||||
get => _rightWidth;
|
||||
set => SetAndRaise(RightWidthProperty, ref _rightWidth, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<DateTime> TimeProperty = AvaloniaProperty.Register<TimelineItem, DateTime>(
|
||||
nameof(Time));
|
||||
|
||||
public DateTime Time
|
||||
{
|
||||
get => GetValue(TimeProperty);
|
||||
@@ -47,7 +108,7 @@ public class TimelineItem: ContentControl
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> TimeFormatProperty = AvaloniaProperty.Register<TimelineItem, string?>(
|
||||
nameof(TimeFormat), defaultValue:CultureInfo.CurrentUICulture.DateTimeFormat.ShortDatePattern);
|
||||
nameof(TimeFormat));
|
||||
|
||||
public string? TimeFormat
|
||||
{
|
||||
@@ -55,47 +116,86 @@ public class TimelineItem: ContentControl
|
||||
set => SetValue(TimeFormatProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate> DescriptionTemplateProperty = AvaloniaProperty.Register<TimelineItem, IDataTemplate>(
|
||||
nameof(DescriptionTemplate));
|
||||
|
||||
public IDataTemplate DescriptionTemplate
|
||||
{
|
||||
get => GetValue(DescriptionTemplateProperty);
|
||||
set => SetValue(DescriptionTemplateProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TimelineItemType> ItemTypeProperty = AvaloniaProperty.Register<TimelineItem, TimelineItemType>(
|
||||
nameof(ItemType));
|
||||
|
||||
public TimelineItemType ItemType
|
||||
{
|
||||
get => GetValue(ItemTypeProperty);
|
||||
set => SetValue(ItemTypeProperty, value);
|
||||
}
|
||||
|
||||
internal void SetIndex(bool isFirst, bool isLast)
|
||||
{
|
||||
PseudoClasses.Set(PC_First, isFirst);
|
||||
PseudoClasses.Set(PC_Last, isLast);
|
||||
}
|
||||
|
||||
static TimelineItem()
|
||||
{
|
||||
ItemTypeProperty.Changed.AddClassHandler<TimelineItem>((o, e) => { o.OnItemTypeChanged(e); });
|
||||
IconForegroundProperty.Changed.AddClassHandler<TimelineItem>((o, e) => { o.OnIconForegroundChanged(e); });
|
||||
IconProperty.Changed.AddClassHandler<TimelineItem, object?>((item, args) => { item.OnIconChanged(args); });
|
||||
PositionProperty.Changed.AddClassHandler<TimelineItem, TimelineItemPosition>((item, args) => { item.OnModeChanged(args); });
|
||||
AffectsMeasure<TimelineItem>(LeftWidthProperty, RightWidthProperty, IconWidthProperty);
|
||||
}
|
||||
|
||||
private void OnItemTypeChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
private void OnModeChanged(AvaloniaPropertyChangedEventArgs<TimelineItemPosition> args)
|
||||
{
|
||||
var oldValue = args.GetOldValue<TimelineItemType>();
|
||||
var newValue = args.GetNewValue<TimelineItemType>();
|
||||
PseudoClasses.Set(_itemTypeMapping[oldValue], false);
|
||||
PseudoClasses.Set(_itemTypeMapping[newValue], true);
|
||||
SetMode(args.NewValue.Value);
|
||||
}
|
||||
|
||||
private void OnIconForegroundChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
private void SetMode(TimelineItemPosition mode)
|
||||
{
|
||||
IBrush? newValue = args.GetOldValue<IBrush?>();
|
||||
PseudoClasses.Set(PC_None, newValue is null);
|
||||
PseudoClasses.Set(PC_AllLeft, mode == TimelineItemPosition.Left);
|
||||
PseudoClasses.Set(PC_AllRight, mode == TimelineItemPosition.Right);
|
||||
PseudoClasses.Set(PC_Separate, mode == TimelineItemPosition.Separate);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_rootGrid = e.NameScope.Find<Grid>(PART_RootGrid);
|
||||
_headerPresenter = e.NameScope.Find<ContentPresenter>(PART_Header);
|
||||
_iconPresenter = e.NameScope.Find<Panel>(PART_Icon);
|
||||
_contentPresenter = e.NameScope.Find<ContentPresenter>(PART_Content);
|
||||
_timePresenter = e.NameScope.Find<TextBlock>(PART_Time);
|
||||
PseudoClasses.Set(PC_EmptyIcon, Icon is null);
|
||||
SetMode(Position);
|
||||
}
|
||||
|
||||
private void OnIconChanged(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
PseudoClasses.Set(PC_EmptyIcon, args.NewValue.Value is null);
|
||||
}
|
||||
|
||||
internal void SetEnd(bool start, bool end)
|
||||
{
|
||||
PseudoClasses.Set(PC_First, start);
|
||||
PseudoClasses.Set(PC_Last, end);
|
||||
}
|
||||
|
||||
internal (double left, double mid, double right) GetWidth()
|
||||
{
|
||||
if (_headerPresenter is null) return new ValueTuple<double, double, double>(0, 0, 0);
|
||||
double header = _headerPresenter?.DesiredSize.Width ?? 0;
|
||||
double icon = _iconPresenter?.DesiredSize.Width ?? 0;
|
||||
double content = _contentPresenter?.DesiredSize.Width ?? 0;
|
||||
double time = _timePresenter?.DesiredSize.Width ?? 0;
|
||||
double max = Math.Max(header, content);
|
||||
if (Position == TimelineItemPosition.Left)
|
||||
{
|
||||
max = Math.Max(max, time);
|
||||
return (0, icon, max);
|
||||
}
|
||||
if (Position == TimelineItemPosition.Right)
|
||||
{
|
||||
max = Math.Max(max, time);
|
||||
return (max , icon, 0);
|
||||
}
|
||||
if (Position == TimelineItemPosition.Separate)
|
||||
{
|
||||
return (time, icon, max);
|
||||
}
|
||||
return new ValueTuple<double, double, double>(0, 0, 0);
|
||||
}
|
||||
|
||||
internal void SetWidth(double? left, double? mid, double? right)
|
||||
{
|
||||
if (_rootGrid is null) return;
|
||||
_rootGrid.ColumnDefinitions[0].Width = new GridLength(left??0);
|
||||
_rootGrid.ColumnDefinitions[1].Width = new GridLength(mid??0);
|
||||
_rootGrid.ColumnDefinitions[2].Width = new GridLength(right??0);
|
||||
}
|
||||
|
||||
internal void SetIfUnset<T>(AvaloniaProperty<T> property, T value)
|
||||
{
|
||||
if (!IsSet(property))
|
||||
{
|
||||
SetCurrentValue(property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/Ursa/Controls/Timeline/TimelinePanel.cs
Normal file
75
src/Ursa/Controls/Timeline/TimelinePanel.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class TimelinePanel: Panel
|
||||
{
|
||||
public static readonly StyledProperty<TimelineDisplayMode> ModeProperty =
|
||||
Timeline.ModeProperty.AddOwner<TimelinePanel>();
|
||||
|
||||
public TimelineDisplayMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
static TimelinePanel()
|
||||
{
|
||||
AffectsMeasure<TimelinePanel>(ModeProperty);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
double left = 0;
|
||||
double right = 0;
|
||||
double icon = 0;
|
||||
double height = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Measure(availableSize);
|
||||
if (child is TimelineItem t)
|
||||
{
|
||||
var doubles = t.GetWidth();
|
||||
left = Math.Max(left, doubles.left);
|
||||
icon = Math.Max(icon, doubles.mid);
|
||||
right = Math.Max(right, doubles.right);
|
||||
}
|
||||
height+=child.DesiredSize.Height;
|
||||
}
|
||||
return new Size(left+icon+right, height);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
|
||||
double left = 0, mid = 0, right = 0;
|
||||
double height = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child is TimelineItem t)
|
||||
{
|
||||
var doubles = t.GetWidth();
|
||||
left = Math.Max(left, doubles.left);
|
||||
mid = Math.Max(mid, doubles.mid);
|
||||
right = Math.Max(right, doubles.right);
|
||||
}
|
||||
}
|
||||
|
||||
Rect rect = new Rect(0, 0, left + mid + right, 0);
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child is TimelineItem t)
|
||||
{
|
||||
t.SetWidth(left, mid, right);
|
||||
t.InvalidateArrange();
|
||||
rect = rect.WithHeight(t.DesiredSize.Height);
|
||||
child.Arrange(rect);
|
||||
rect = rect.WithY(rect.Y + t.DesiredSize.Height);
|
||||
height+=t.DesiredSize.Height;
|
||||
}
|
||||
}
|
||||
return new Size(left + mid + right, height);
|
||||
}
|
||||
}
|
||||
8
src/Ursa/Controls/ToolBar/OverflowMode.cs
Normal file
8
src/Ursa/Controls/ToolBar/OverflowMode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public enum OverflowMode
|
||||
{
|
||||
AsNeeded,
|
||||
Always,
|
||||
Never
|
||||
}
|
||||
101
src/Ursa/Controls/ToolBar/ToolBar.cs
Normal file
101
src/Ursa/Controls/ToolBar/ToolBar.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Markup.Xaml.Templates;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Overflow)]
|
||||
[TemplatePart(PART_OverflowPanel, typeof(Panel))]
|
||||
public class ToolBar: HeaderedItemsControl
|
||||
{
|
||||
public const string PART_OverflowPanel = "PART_OverflowPanel";
|
||||
public const string PC_Overflow = ":overflow";
|
||||
|
||||
internal Panel? OverflowPanel { get; private set; }
|
||||
|
||||
private static readonly ITemplate<Panel?> DefaultTemplate =
|
||||
new FuncTemplate<Panel?>(() => new ToolBarPanel() { Orientation = Orientation.Horizontal });
|
||||
|
||||
public static readonly StyledProperty<Orientation> OrientationProperty =
|
||||
StackPanel.OrientationProperty.AddOwner<ToolBar>();
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<PlacementMode> PopupPlacementProperty =
|
||||
Popup.PlacementProperty.AddOwner<ToolBar>();
|
||||
|
||||
public PlacementMode PopupPlacement
|
||||
{
|
||||
get => GetValue(PopupPlacementProperty);
|
||||
set => SetValue(PopupPlacementProperty, value);
|
||||
}
|
||||
|
||||
public static readonly AttachedProperty<OverflowMode> OverflowModeProperty =
|
||||
AvaloniaProperty.RegisterAttached<ToolBar, Control, OverflowMode>("OverflowMode");
|
||||
|
||||
public static void SetOverflowMode(Control obj, OverflowMode value) => obj.SetValue(OverflowModeProperty, value);
|
||||
public static OverflowMode GetOverflowMode(Control obj) => obj.GetValue(OverflowModeProperty);
|
||||
|
||||
internal static readonly AttachedProperty<bool> IsOverflowItemProperty =
|
||||
AvaloniaProperty.RegisterAttached<ToolBar, Control, bool>("IsOverflowItem");
|
||||
|
||||
internal static void SetIsOverflowItem(Control obj, bool value) => obj.SetValue(IsOverflowItemProperty, value);
|
||||
internal static bool GetIsOverflowItem(Control obj) => obj.GetValue(IsOverflowItemProperty);
|
||||
|
||||
private bool _hasOverflowItems;
|
||||
internal bool HasOverflowItems
|
||||
{
|
||||
get => _hasOverflowItems;
|
||||
set
|
||||
{
|
||||
_hasOverflowItems = value;
|
||||
PseudoClasses.Set(PC_Overflow, value);
|
||||
}
|
||||
}
|
||||
|
||||
static ToolBar()
|
||||
{
|
||||
IsTabStopProperty.OverrideDefaultValue<ToolBar>(false);
|
||||
ItemsPanelProperty.OverrideDefaultValue<ToolBar>(DefaultTemplate);
|
||||
OrientationProperty.OverrideDefaultValue<ToolBar>(Orientation.Horizontal);
|
||||
// TODO: use helper method after merged and upgrade helper dependency.
|
||||
IsOverflowItemProperty.Changed.AddClassHandler<Control, bool>((o, e) =>
|
||||
{
|
||||
PseudolassesExtensions.Set(o.Classes, PC_Overflow, e.NewValue.Value);
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<Control>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
if(item is Control c)
|
||||
{
|
||||
return c;
|
||||
}
|
||||
if(ItemTemplate is not null && ItemTemplate.Match(item))
|
||||
{
|
||||
return ItemTemplate.Build(item)?? new ContentPresenter();
|
||||
}
|
||||
return new ContentPresenter();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
OverflowPanel = e.NameScope.Find<Panel>(PART_OverflowPanel);
|
||||
}
|
||||
}
|
||||
164
src/Ursa/Controls/ToolBar/ToolBarPanel.cs
Normal file
164
src/Ursa/Controls/ToolBar/ToolBarPanel.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.LogicalTree;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ToolBarPanel: StackPanel
|
||||
{
|
||||
private ToolBar? _parent;
|
||||
private Panel? _overflowPanel;
|
||||
|
||||
private Panel? OverflowPanel => _overflowPanel ??= _parent?.OverflowPanel;
|
||||
internal ToolBar? ParentToolBar => _parent ??= this.TemplatedParent as ToolBar;
|
||||
|
||||
static ToolBarPanel()
|
||||
{
|
||||
OrientationProperty.OverrideDefaultValue<ToolBarPanel>(Orientation.Horizontal);
|
||||
}
|
||||
|
||||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToLogicalTree(e);
|
||||
_parent = this.TemplatedParent as ToolBar;
|
||||
if (_parent is null) return;
|
||||
this[!OrientationProperty] = _parent[!OrientationProperty];
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var logicalChildren = _parent?.GetLogicalChildren().OfType<Control>().ToList();
|
||||
Size size = new Size();
|
||||
double spacing = 0;
|
||||
Size measureSize = availableSize;
|
||||
bool horizontal = Orientation == Orientation.Horizontal;
|
||||
bool hasVisibleChildren = false;
|
||||
if (logicalChildren is null) return size;
|
||||
for (int i = 0; i < logicalChildren.Count; i++)
|
||||
{
|
||||
Control control = logicalChildren[i];
|
||||
var mode = ToolBar.GetOverflowMode(control);
|
||||
control.Measure(measureSize);
|
||||
if (mode == OverflowMode.Always)
|
||||
{
|
||||
ToolBar.SetIsOverflowItem(control, true);
|
||||
}
|
||||
else if (mode == OverflowMode.Never)
|
||||
{
|
||||
if (control.IsVisible)
|
||||
{
|
||||
hasVisibleChildren = true;
|
||||
size = horizontal
|
||||
? size.WithWidth(size.Width + control.DesiredSize.Width + spacing)
|
||||
.WithHeight(Math.Max(size.Height, control.DesiredSize.Height))
|
||||
: size.WithHeight(size.Height + control.DesiredSize.Height + spacing)
|
||||
.WithWidth(Math.Max(size.Width, control.DesiredSize.Width));
|
||||
ToolBar.SetIsOverflowItem(control, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool isOverflow = false;
|
||||
for(int i = 0; i < logicalChildren.Count; i++)
|
||||
{
|
||||
Control control = logicalChildren[i];
|
||||
var mode = ToolBar.GetOverflowMode(control);
|
||||
if (mode != OverflowMode.AsNeeded) continue;
|
||||
//Always keeps the order of display. It's very un reasonable to display the second but short control
|
||||
//and push the first control to the overflow panel. So once a control is marked as overflow, the following
|
||||
//controls will be marked as overflow too.
|
||||
if (isOverflow)
|
||||
{
|
||||
ToolBar.SetIsOverflowItem(control, isOverflow);
|
||||
continue;
|
||||
}
|
||||
bool isFit = horizontal
|
||||
? (size.Width + control.DesiredSize.Width <= availableSize.Width)
|
||||
: (size.Height + control.DesiredSize.Height <= availableSize.Height);
|
||||
if (isFit)
|
||||
{
|
||||
ToolBar.SetIsOverflowItem(control, false);
|
||||
size = horizontal
|
||||
? size.WithWidth(size.Width + control.DesiredSize.Width + spacing)
|
||||
.WithHeight(Math.Max(size.Height, control.DesiredSize.Height))
|
||||
: size.WithHeight(size.Height + control.DesiredSize.Height + spacing)
|
||||
.WithWidth(Math.Max(size.Width, control.DesiredSize.Width));
|
||||
}
|
||||
else
|
||||
{
|
||||
isOverflow = true;
|
||||
ToolBar.SetIsOverflowItem(control, isOverflow);
|
||||
}
|
||||
}
|
||||
if (hasVisibleChildren)
|
||||
{
|
||||
size = horizontal ? size.WithWidth(size.Width - spacing) : size.WithHeight(size.Height - spacing);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var logicalChildren = _parent?.GetLogicalChildren().OfType<Control>().ToList();
|
||||
if(logicalChildren is null) return finalSize;
|
||||
bool overflow = false;
|
||||
for (int i = 0; i < logicalChildren.Count; i++)
|
||||
{
|
||||
var child = logicalChildren[i];
|
||||
if(child is ToolBarSeparator s) s.IsVisible = true;
|
||||
var isItemOverflow = ToolBar.GetIsOverflowItem(child);
|
||||
if(isItemOverflow) overflow = true;
|
||||
if (Children?.Contains(child) == true)
|
||||
{
|
||||
if (isItemOverflow)
|
||||
{
|
||||
Children.Remove(child);
|
||||
var overflowIndex = -1;
|
||||
if (i > 1)
|
||||
{
|
||||
var last = logicalChildren[i - 1];
|
||||
overflowIndex = OverflowPanel?.Children?.IndexOf(last) ?? -1;
|
||||
}
|
||||
OverflowPanel?.Children?.Insert(overflowIndex + 1, child);
|
||||
}
|
||||
}
|
||||
else if (OverflowPanel?.Children?.Contains(child) == true)
|
||||
{
|
||||
if (!isItemOverflow)
|
||||
{
|
||||
OverflowPanel.Children.Remove(child);
|
||||
var index = -1;
|
||||
if (i > 1)
|
||||
{
|
||||
var last = logicalChildren[i - 1];
|
||||
index = Children?.IndexOf(last) ?? -1;
|
||||
}
|
||||
Children?.Insert(index + 1, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.Children?.LastOrDefault() is ToolBarSeparator s2)
|
||||
{
|
||||
s2.IsVisible = false;
|
||||
}
|
||||
if (OverflowPanel?.Children?.FirstOrDefault() is ToolBarSeparator s3)
|
||||
{
|
||||
s3.IsVisible = false;
|
||||
}
|
||||
if (_parent != null) _parent.HasOverflowItems = overflow;
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
var list = OverflowPanel?.Children.ToList();
|
||||
if (list is not null)
|
||||
{
|
||||
OverflowPanel?.Children.Clear();
|
||||
Children.AddRange(list);
|
||||
}
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
}
|
||||
}
|
||||
41
src/Ursa/Controls/ToolBar/ToolBarSeparator.cs
Normal file
41
src/Ursa/Controls/ToolBar/ToolBarSeparator.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.LogicalTree;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Vertical)]
|
||||
public class ToolBarSeparator: TemplatedControl
|
||||
{
|
||||
public const string PC_Vertical = ":vertical";
|
||||
|
||||
public static readonly StyledProperty<Orientation> OrientationProperty =
|
||||
ToolBar.OrientationProperty.AddOwner<ToolBarSeparator>();
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
static ToolBarSeparator()
|
||||
{
|
||||
OrientationProperty.OverrideDefaultValue<ToolBarSeparator>(Orientation.Horizontal);
|
||||
OrientationProperty.Changed.AddClassHandler<ToolBarSeparator, Orientation>((separator, args) =>
|
||||
{
|
||||
separator.PseudoClasses.Set(PC_Vertical, args.NewValue.Value == Orientation.Vertical);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToLogicalTree(e);
|
||||
var ancestor = this.GetLogicalAncestors().OfType<ToolBar>().FirstOrDefault();
|
||||
if (ancestor is null) return;
|
||||
this[!OrientationProperty] = ancestor[!ToolBar.OrientationProperty];
|
||||
}
|
||||
}
|
||||
175
src/Ursa/Controls/VerificationCode/VerificationCode.cs
Normal file
175
src/Ursa/Controls/VerificationCode/VerificationCode.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Utilities;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_ItemsControl, typeof(ItemsControl))]
|
||||
public class VerificationCode: TemplatedControl
|
||||
{
|
||||
public const string PART_ItemsControl = "PART_ItemsControl";
|
||||
private ItemsControl? _itemsControl;
|
||||
private int _currentIndex = 0;
|
||||
|
||||
public static readonly StyledProperty<ICommand?> CompleteCommandProperty = AvaloniaProperty.Register<VerificationCode, ICommand?>(
|
||||
nameof(CompleteCommand));
|
||||
|
||||
public ICommand? CompleteCommand
|
||||
{
|
||||
get => GetValue(CompleteCommandProperty);
|
||||
set => SetValue(CompleteCommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<int> CountProperty = AvaloniaProperty.Register<VerificationCode, int>(
|
||||
nameof(Count));
|
||||
|
||||
public int Count
|
||||
{
|
||||
get => GetValue(CountProperty);
|
||||
set => SetValue(CountProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<char> PasswordCharProperty =
|
||||
AvaloniaProperty.Register<VerificationCode, char>(
|
||||
nameof(PasswordChar));
|
||||
|
||||
public char PasswordChar
|
||||
{
|
||||
get => GetValue(PasswordCharProperty);
|
||||
set => SetValue(PasswordCharProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<VerificationCodeMode> ModeProperty =
|
||||
AvaloniaProperty.Register<VerificationCode, VerificationCodeMode>(
|
||||
nameof(Mode), defaultValue: VerificationCodeMode.Digit | VerificationCodeMode.Letter);
|
||||
|
||||
public VerificationCodeMode Mode
|
||||
{
|
||||
get => GetValue(ModeProperty);
|
||||
set => SetValue(ModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<VerificationCode, IList<string>> DigitsProperty = AvaloniaProperty.RegisterDirect<VerificationCode, IList<string>>(
|
||||
nameof(Digits), o => o.Digits, (o, v) => o.Digits = v);
|
||||
|
||||
private IList<string> _digits = [];
|
||||
internal IList<string> Digits
|
||||
{
|
||||
get => _digits;
|
||||
set => SetAndRaise(DigitsProperty, ref _digits, value);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<VerificationCodeCompleteEventArgs> CompleteEvent =
|
||||
RoutedEvent.Register<VerificationCode, VerificationCodeCompleteEventArgs>(
|
||||
nameof(Complete), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<VerificationCodeCompleteEventArgs> Complete
|
||||
{
|
||||
add => AddHandler(CompleteEvent, value);
|
||||
remove => RemoveHandler(CompleteEvent, value);
|
||||
}
|
||||
|
||||
static VerificationCode()
|
||||
{
|
||||
CountProperty.Changed.AddClassHandler<VerificationCode, int>((code, args) => code.OnCountOfDigitChanged(args));
|
||||
FocusableProperty.OverrideDefaultValue<VerificationCode>(true);
|
||||
}
|
||||
|
||||
public VerificationCode()
|
||||
{
|
||||
InputMethod.SetIsInputMethodEnabled(this, false);
|
||||
}
|
||||
|
||||
private void OnCountOfDigitChanged(AvaloniaPropertyChangedEventArgs<int> args)
|
||||
{
|
||||
var newValue = args.NewValue.Value;
|
||||
if (newValue > 0)
|
||||
{
|
||||
Digits = new List<string>(Enumerable.Repeat(string.Empty, newValue));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_itemsControl = e.NameScope.Get<ItemsControl>(PART_ItemsControl);
|
||||
PointerPressedEvent.AddHandler(OnControlPressed, RoutingStrategies.Tunnel, false, this);
|
||||
}
|
||||
|
||||
private void OnControlPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.Source is Control t)
|
||||
{
|
||||
/*
|
||||
var item = t.FindLogicalAncestorOfType<VerificationCodeItem>();
|
||||
if (item != null)
|
||||
{
|
||||
item.Focus();
|
||||
_currentIndex = _itemsControl?.IndexFromContainer(item) ?? 0;
|
||||
}
|
||||
*/
|
||||
_currentIndex = MathUtilities.Clamp(_currentIndex, 0, Count - 1);
|
||||
_itemsControl?.ContainerFromIndex(_currentIndex)?.Focus();
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnTextInput(TextInputEventArgs e)
|
||||
{
|
||||
base.OnTextInput(e);
|
||||
if (e.Text?.Length == 1 && _currentIndex < Count)
|
||||
{
|
||||
var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as VerificationCodeItem;
|
||||
if (presenter is null) return;
|
||||
char c = e.Text[0];
|
||||
if (!Valid(c, this.Mode)) return;
|
||||
presenter.Text = e.Text;
|
||||
Digits[_currentIndex] = e.Text;
|
||||
_currentIndex++;
|
||||
_itemsControl?.ContainerFromIndex(_currentIndex)?.Focus();
|
||||
if (_currentIndex == Count)
|
||||
{
|
||||
CompleteCommand?.Execute(Digits);
|
||||
RaiseEvent(new VerificationCodeCompleteEventArgs(Digits, CompleteEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool Valid(char c, VerificationCodeMode mode)
|
||||
{
|
||||
bool isDigit = char.IsDigit(c);
|
||||
bool isLetter = char.IsLetter(c);
|
||||
return mode switch
|
||||
{
|
||||
VerificationCodeMode.Digit => isDigit,
|
||||
VerificationCodeMode.Letter => isLetter,
|
||||
VerificationCodeMode.Digit | VerificationCodeMode.Letter => isDigit || isLetter,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
if (e.Key == Key.Back && _currentIndex >= 0)
|
||||
{
|
||||
_currentIndex = MathUtilities.Clamp(_currentIndex, 0, Count - 1);
|
||||
var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as VerificationCodeItem;
|
||||
if (presenter is null) return;
|
||||
Digits[_currentIndex] = string.Empty;
|
||||
presenter.Text = string.Empty;
|
||||
if (_currentIndex == 0) return;
|
||||
_currentIndex--;
|
||||
_itemsControl?.ContainerFromIndex(_currentIndex)?.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class VerificationCodeCollection: ItemsControl
|
||||
{
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<VerificationCodeItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return new VerificationCodeItem()
|
||||
{
|
||||
[InputMethod.IsInputMethodEnabledProperty] = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class VerificationCodeCompleteEventArgs(IList<string> code, RoutedEvent? @event) : RoutedEventArgs(@event)
|
||||
{
|
||||
public IList<string> Code { get; } = code;
|
||||
}
|
||||
26
src/Ursa/Controls/VerificationCode/VerificationCodeItem.cs
Normal file
26
src/Ursa/Controls/VerificationCode/VerificationCodeItem.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class VerificationCodeItem: TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<VerificationCodeItem, string>(
|
||||
nameof(Text), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<char> PasswordCharProperty = AvaloniaProperty.Register<VerificationCodeItem, char>(
|
||||
nameof(PasswordChar), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public char PasswordChar
|
||||
{
|
||||
get => GetValue(PasswordCharProperty);
|
||||
set => SetValue(PasswordCharProperty, value);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user