Merge branch 'main' into main

This commit is contained in:
Dong Bin
2024-02-26 01:29:30 +08:00
committed by GitHub
241 changed files with 14789 additions and 1427 deletions

View File

@@ -3,3 +3,4 @@ using Avalonia.Metadata;
[assembly:XmlnsPrefix("https://irihi.tech/ursa", "u")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls.Shapes")]

View File

@@ -0,0 +1,9 @@
namespace Ursa.Common;
public enum Position
{
Left,
Top,
Right,
Bottom,
}

View File

@@ -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()
{

View File

@@ -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();
}
}

View 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]);
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}
}
}

View File

@@ -0,0 +1,187 @@
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)]
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";
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; }
internal bool CanDragMove { get; set; }
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);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_titleArea = e.NameScope.Find<Panel>(PART_TitleArea);
if (CanDragMove)
{
_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);
}
}

View 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
}

View 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);
}
}

View 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;
}

View File

@@ -0,0 +1,48 @@
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 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;
}

View File

@@ -0,0 +1,216 @@
using Avalonia;
using Avalonia.Controls;
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.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;
control.CanDragMove = options.CanDragMove;
}
private static void ConfigureDefaultDialogControl(DefaultDialogControl control, OverlayDialogOptions? options)
{
if (options is null) options = new OverlayDialogOptions();
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;
control.CanDragMove = options.CanDragMove;
}
}

View 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);
}
}

View File

@@ -0,0 +1,55 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Ursa.Controls.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 Border()
{
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);
}
}
}

View 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);
}
}
}

View 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));
}
}
}

View 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;
}
}
}

View 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));
}
}
}

View 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;
}

View 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;
}
}

View 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],
};
}
}

View 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],
};
}
}

View 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();
}
}
}

View 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);
}
}

View File

@@ -0,0 +1,271 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
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);
}
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);
}
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));
}
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)
{
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);
}
private void OnStretchChanged(AvaloniaPropertyChangedEventArgs args)
{
var stretch = args.GetNewValue<Stretch>();
Scale = GetScaleRatio(Width / _image!.Width, Height / _image!.Height, stretch);
}
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 (Source is { } i)
{
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);
}
if (Overlayer is { } c)
{
AdornerLayer.SetAdorner(this, c);
}
}
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 < 0.1) scale = 0.1;
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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls.Primitives;
namespace Ursa.Controls.Layout;
public class DefaultDialogLayout: TemplatedControl
{
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
namespace Ursa.Controls;
public enum MessageBoxButton
{
OK,
OKCancel,
YesNo,
YesNoCancel,
}

View 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);
}
}

View 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,
}

View File

@@ -0,0 +1,10 @@
namespace Ursa.Controls;
public enum MessageBoxResult
{
Cancel,
No,
None,
OK,
Yes,
}

View 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);
}
}

View File

@@ -0,0 +1,276 @@
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 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();
}
}
}
}

View File

@@ -0,0 +1,340 @@
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.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;
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>(":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 (this.ItemCount == 0)
{
SelectItem(this);
}
else
{
if (!IsHorizontalCollapsed)
{
SetCurrentValue(IsVerticalCollapsedProperty, !IsVerticalCollapsed);
}
else
{
if (_popup is not null)
{
if (_popup.IsOpen)
{
_popup.Close();
}
else
{
_popup.Open();
}
}
}
}
Command?.Execute(CommandParameter);
e.Handled = true;
}
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);
}
}
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;
}
}
}
}
}

View 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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,11 +0,0 @@
using Avalonia.Input;
namespace Ursa.Controls;
public class NavigationMenuSeparator: NavigationMenuItem
{
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
e.Handled = true;
}
}

View 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);
}
}

View 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);
}

View File

@@ -0,0 +1,236 @@
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) => value?.ToString(FormatString, NumberFormat);
protected override int Zero => 0;
protected override int? Add(int? a, int? b) => a + b;
protected override int? Minus(int? a, int? b) => a - b;
}
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) => (byte?) (a + b);
protected override byte? Minus(byte? a, byte? b) => (byte?) (a - b);
}
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) => (sbyte?) (a + b);
protected override sbyte? Minus(sbyte? a, sbyte? b) => (sbyte?) (a - b);
}
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) => (short?) (a + b);
protected override short? Minus(short? a, short? b) => (short?) (a - b);
}
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) => (ushort?) (a + b);
protected override ushort? Minus(ushort? a, ushort? b) => (ushort?) (a - b);
}
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) => a + b;
protected override long? Minus(long? a, long? b) => a - b;
}
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;
}

View File

@@ -0,0 +1,623 @@
using System.Diagnostics;
using System.Globalization;
using System.Net.Mime;
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 Irihi.Avalonia.Shared.Contracts;
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;
public static readonly StyledProperty<bool> AllowDragProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
nameof(AllowDrag), defaultValue: false);
public bool AllowDrag
{
get => GetValue(AllowDragProperty);
set => SetValue(AllowDragProperty, value);
}
public static readonly StyledProperty<bool> IsReadOnlyProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
nameof(IsReadOnly));
public bool IsReadOnly
{
get => GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, 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 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>((o,e)=>o.ChangeToSetSpinDirection(e));
TextConverterProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
}
protected void ChangeToSetSpinDirection(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs, bool afterInitialization = false)
{
if (afterInitialization)
{
if (IsInitialized)
{
SetValidSpinDirection();
}
}
else
{
SetValidSpinDirection();
}
}
protected virtual void OnFormatChange(AvaloniaPropertyChangedEventArgs arg)
{
if (IsInitialized)
{
SyncTextAndValue(false, null);
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if(_spinner is not null)
{
_spinner.Spin -= OnSpin;
}
if (_dragPanel is not null)
{
_dragPanel.PointerPressed -= OnDragPanelPointerPressed;
_dragPanel.PointerMoved -= OnDragPanelPointerMoved;
_dragPanel.PointerReleased -= OnDragPanelPointerReleased;
}
_spinner = e.NameScope.Find<ButtonSpinner>(PART_Spinner);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
_dragPanel = e.NameScope.Find<Panel>(PART_DragPanel);
if (_spinner is not null)
{
_spinner.Spin += OnSpin;
}
if (_dragPanel is not null)
{
_dragPanel.PointerPressed+= OnDragPanelPointerPressed;
_dragPanel.PointerMoved += OnDragPanelPointerMoved;
_dragPanel.PointerReleased += OnDragPanelPointerReleased;
}
}
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();
}
}
}
private void OnDragPanelPointerPressed(object sender, PointerPressedEventArgs e)
{
_point = e.GetPosition(this);
if (e.ClickCount == 2 && _dragPanel is not null && AllowDrag)
{
_dragPanel.IsVisible = false;
_textBox.IsReadOnly = false;
}
else
{
_textBox?.Focus();
_textBox!.IsReadOnly = true;
}
}
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)
{
Increase();
}
else if (d < 0)
{
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>
{
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);
}
/// <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);
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;
if (!IsReadOnly)
{
if (Value is null)
{
validDirection = ValidSpinDirections.Increase | ValidSpinDirections.Decrease;
}
if (Value.HasValue && Value.Value.CompareTo(Maximum) < 0)
{
validDirection |= ValidSpinDirections.Increase;
}
if (Value.HasValue && Value.Value.CompareTo(Minimum) > 0)
{
validDirection |= ValidSpinDirections.Decrease;
}
}
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))
{
SetCurrentValue(ValueProperty, newValue);
}
}
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
{
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);
}
}

View 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; }
}

View File

@@ -0,0 +1,11 @@
namespace Ursa.Controls;
public enum DialogButton
{
None,
OK,
OKCancel,
YesNo,
YesNoCancel,
}

View File

@@ -0,0 +1,11 @@
namespace Ursa.Controls;
public enum DialogMode
{
Info,
Warning,
Error,
Question,
None,
Success,
}

View File

@@ -0,0 +1,10 @@
namespace Ursa.Controls;
public enum DialogResult
{
Cancel,
No,
None,
OK,
Yes,
}

View File

@@ -0,0 +1,289 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Utilities;
using Ursa.Controls.OverlayShared;
using Ursa.Controls.Shapes;
using Ursa.EventArgs;
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)
{
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));
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;
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);
control.Measure(this.Bounds.Size);
control.Arrange(new Rect(control.DesiredSize));
SetToPosition(control);
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.AddHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
_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;
}
}

View File

@@ -0,0 +1,162 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Styling;
using Ursa.Common;
using Ursa.Controls.OverlayShared;
using Ursa.Controls.Shapes;
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 (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);
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDrawerControlClosing);
var animation = CreateAnimation(control.Bounds.Size, control.Position);
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)
{
layer.Mask.RemoveHandler(PointerPressedEvent, ClickMaskToCloseDialog);
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
await Task.WhenAll(disappearAnimation.RunAsync(control), _maskDisappearAnimation.RunAsync(layer.Mask));
Children.Remove(layer.Mask);
}
else
{
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
await disappearAnimation.RunAsync(control);
}
Children.Remove(control);
ResetZIndices();
}
}
}

View File

@@ -0,0 +1,195 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Media;
using Ursa.Controls.OverlayShared;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
using Avalonia.Styling;
using Ursa.Controls.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>();
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);
}
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;
}
}

View 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;
}
}

View 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<CustomDialogControl, 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();
}

View File

@@ -0,0 +1,304 @@
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 = GetThumbByPoint(posOnTrack);
if (_currentThumb !=null && _currentThumb != thumb) return;
if (thumb is null) return;
if (thumb == _track.LowerThumb)
{
SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value);
}
else
{
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 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;
}
else
{
return _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);
}
}

View 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;
}
}
}
}

View 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;
}
}

View File

@@ -0,0 +1,153 @@
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.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();
}
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);
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,45 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
namespace Ursa.Controls.Shapes;
public class PureCircle: Control
{
public static readonly StyledProperty<IBrush?> BackgroundProperty =
TemplatedControl.BackgroundProperty.AddOwner<PureCircle>();
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<double> DiameterProperty = AvaloniaProperty.Register<PureCircle, double>(
nameof(Diameter));
public double Diameter
{
get => GetValue(DiameterProperty);
set => SetValue(DiameterProperty, value);
}
static PureCircle()
{
FocusableProperty.OverrideDefaultValue<PureCircle>(false);
AffectsMeasure<PureCircle>(DiameterProperty);
AffectsRender<PureCircle>(DiameterProperty, BackgroundProperty);
}
protected override Size MeasureOverride(Size availableSize)
{
return new Size(Diameter, Diameter);
}
public override void Render(DrawingContext context)
{
double value = Diameter / 2;
context.DrawEllipse(Background, null, new(value, value), value, value);
}
}

View File

@@ -0,0 +1,33 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
namespace Ursa.Controls.Shapes;
/// <summary>
/// A rectangle, with no corner radius.
/// </summary>
public class PureRectangle: Control
{
public static readonly StyledProperty<IBrush?> BackgroundProperty =
TemplatedControl.BackgroundProperty.AddOwner<PureRectangle>();
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
static PureRectangle()
{
FocusableProperty.OverrideDefaultValue<PureRectangle>(false);
AffectsRender<PureRectangle>(BackgroundProperty);
}
public override void Render(DrawingContext context)
{
context.DrawRectangle(Background, null, new Rect(Bounds.Size));
}
}

View File

@@ -190,6 +190,12 @@ public class TagInput : TemplatedControl
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
Items.Clear();
Items.Add(_textBox);
InvalidateVisual();
}
}

View File

@@ -0,0 +1,133 @@
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<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(target.ActualThemeVariant);
}
else if (this._scope is { } scope)
{
SyncThemeFromScope(scope.ActualThemeVariant);
}
else if (_application is { } app)
{
SyncThemeFromScope(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;
if (_application is not null)
{
_application.ActualThemeVariantChanged += OnScopeThemeChanged;
SyncThemeFromScope(_application.ActualThemeVariant);
}
_scope = this.GetLogicalAncestors().FirstOrDefault(a => a is ThemeVariantScope) as ThemeVariantScope;
if (_scope is not null)
{
_scope.ActualThemeVariantChanged += OnScopeThemeChanged;
SyncThemeFromScope(_scope.ActualThemeVariant);
}
if (TargetScope is not null)
{
SyncThemeFromScope(TargetScope.ActualThemeVariant);
}
}
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 (newTheme is null) return;
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;
}
}
}

View File

@@ -0,0 +1,50 @@
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_ThemeToggleButton, typeof(ToggleButton))]
public class ThemeToggleButton: ThemeSelectorBase
{
public const string PART_ThemeToggleButton = "PART_ThemeToggleButton";
/// <summary>
/// This button IsChecked=true means ThemeVariant.Light, IsChecked=false means ThemeVariant.Dark.
/// </summary>
private ToggleButton? _button;
private ThemeVariant? _currentTheme;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_currentTheme = this.ActualThemeVariant;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
Button.ClickEvent.RemoveHandler(OnButtonClickedChanged, _button);
_button = e.NameScope.Get<ToggleButton>(PART_ThemeToggleButton);
Button.ClickEvent.AddHandler(OnButtonClickedChanged, _button);
ToggleButton.IsCheckedProperty.SetValue(_currentTheme == ThemeVariant.Light, _button);
}
private void OnButtonClickedChanged(object sender, RoutedEventArgs e)
{
var newTheme = (sender as ToggleButton)!.IsChecked;
if (newTheme is null) return;
SetCurrentValue(SelectedThemeProperty, newTheme.Value ? ThemeVariant.Light : ThemeVariant.Dark);
}
protected override void SyncThemeFromScope(ThemeVariant? theme)
{
base.SyncThemeFromScope(theme);
ToggleButton.IsCheckedProperty.SetValue(theme == ThemeVariant.Light, _button);
}
}

View File

@@ -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);
}
}

View 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,
}

View File

@@ -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);
}
}
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
namespace Ursa.Controls;
public enum OverflowMode
{
AsNeeded,
Always,
Never
}

View 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);
}
}

View File

@@ -0,0 +1,151 @@
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)
{
Children.Clear();
OverflowPanel?.Children.Clear();
var logicalChildren = _parent?.GetLogicalChildren().OfType<Control>().ToList();
if(logicalChildren is null) return finalSize;
bool overflow = false;
foreach (var child in logicalChildren)
{
if (ToolBar.GetIsOverflowItem(child))
{
OverflowPanel?.Children.Add(child);
overflow = true;
}
else
{
Children.Add(child);
}
if (child is ToolBarSeparator s)
{
s.IsVisible = true;
}
}
var thisLast = this.Children.LastOrDefault();
if (thisLast is ToolBarSeparator s2)
{
s2.IsVisible = false;
}
var thatFirst = OverflowPanel?.Children.FirstOrDefault();
if (thatFirst 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);
}
}

View 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];
}
}

View File

@@ -0,0 +1,18 @@
using Avalonia.Interactivity;
namespace Ursa.EventArgs;
public class ResultEventArgs: RoutedEventArgs
{
public object? Result { get; set; }
public ResultEventArgs(object? result)
{
Result = result;
}
public ResultEventArgs(RoutedEvent routedEvent, object? result): base(routedEvent)
{
Result = result;
}
}

View File

@@ -7,13 +7,18 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<Version>0.1.0-beta20230702</Version>
<Version>0.2.0-beta20240213</Version>
<Authors>IRIHI Technology Co., Ltd.</Authors>
<PackageId>Irihi.Ursa</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Irihi.Avalonia.Shared" Version="0.1.4" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\Panels\" />
</ItemGroup>
</Project>