Files
Ursa.Avalonia/src/Ursa/Controls/ComboBox/TreeComboBox.cs
2024-04-18 22:11:36 +08:00

301 lines
10 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.VisualTree;
using Irihi.Avalonia.Shared.Common;
using Irihi.Avalonia.Shared.Contracts;
using Size = Avalonia.Size;
namespace Ursa.Controls;
[TemplatePart(PartNames.PART_Popup, typeof(Popup))]
public class TreeComboBox: ItemsControl, IClearControl, IInnerContentControl, IPopupInnerContent
{
private Popup? _popup;
private static readonly FuncTemplate<Panel?> DefaultPanel =
new FuncTemplate<Panel?>(() => new VirtualizingStackPanel());
public static readonly StyledProperty<double> MaxDropDownHeightProperty =
ComboBox.MaxDropDownHeightProperty.AddOwner<TreeComboBox>();
public double MaxDropDownHeight
{
get => GetValue(MaxDropDownHeightProperty);
set => SetValue(MaxDropDownHeightProperty, value);
}
public static readonly StyledProperty<string?> WatermarkProperty =
TextBox.WatermarkProperty.AddOwner<TreeComboBox>();
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
ComboBox.IsDropDownOpenProperty.AddOwner<TreeComboBox>();
public bool IsDropDownOpen
{
get => GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner<TreeComboBox>();
public HorizontalAlignment HorizontalContentAlignment
{
get => GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<TreeComboBox>();
public VerticalAlignment VerticalContentAlignment
{
get => GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> SelectedItemTemplateProperty =
AvaloniaProperty.Register<TreeComboBox, IDataTemplate?>(nameof(SelectedItemTemplate));
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate? SelectedItemTemplate
{
get => GetValue(SelectedItemTemplateProperty);
set => SetValue(SelectedItemTemplateProperty, value);
}
public static readonly DirectProperty<TreeComboBox, object?> SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect<TreeComboBox, object?>(
nameof(SelectionBoxItem), o => o.SelectionBoxItem);
private object? _selectionBoxItem;
public object? SelectionBoxItem
{
get => _selectionBoxItem;
protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
}
private object? _selectedItem;
public static readonly DirectProperty<TreeComboBox, object?> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<TreeComboBox, object?>(
nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay);
public object? SelectedItem
{
get => _selectedItem;
set => SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
}
static TreeComboBox()
{
ItemsPanelProperty.OverrideDefaultValue<TreeComboBox>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<TreeComboBox>(true);
SelectedItemProperty.Changed.AddClassHandler<TreeComboBox, object?>((box, args) => box.OnSelectedItemChanged(args));
}
private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs<object?> args)
{
MarkContainerSelection(args.OldValue.Value, false);
MarkContainerSelection(args.NewValue.Value, true);
UpdateSelectionBoxItem(args.NewValue.Value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_popup = e.NameScope.Find<Popup>(PartNames.PART_Popup);
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TreeComboBoxItem>(item, out recycleKey);
}
internal bool NeedsContainerInternal(object? item, int index, out object? recycleKey)
{
return NeedsContainerOverride(item, index, out recycleKey);
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TreeComboBoxItem();
}
internal Control CreateContainerForItemInternal(object? item, int index, object? recycleKey)
{
return CreateContainerForItemOverride(item, index, recycleKey);
}
internal void ContainerForItemPreparedInternal(Control container, object? item, int index)
{
ContainerForItemPreparedOverride(container, item, index);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (e is { InitialPressMouseButton: MouseButton.Left, Source: Visual source })
{
if (_popup is not null && _popup.IsOpen && _popup.IsInsidePopup(source))
{
var container = GetContainerFromEventSource(source);
if (container is null) return;
var item = TreeItemFromContainer(container);
if (item is null) return;
if (SelectedItem is not null)
{
var selectedContainer = TreeContainerFromItem(SelectedItem);
if(selectedContainer is TreeComboBoxItem selectedTreeComboBoxItem)
{
selectedTreeComboBoxItem.IsSelected = false;
}
}
this.SelectedItem = item;
container.IsSelected = true;
IsDropDownOpen = false;
}
else
{
IsDropDownOpen = !IsDropDownOpen;
}
}
}
private void UpdateSelectionBoxItem(object? item)
{
if(item is null) SelectionBoxItem = null;
if (item is ContentControl contentControl)
{
item = contentControl.Content;
}
else if(item is HeaderedItemsControl headeredItemsControl)
{
item = headeredItemsControl.Header;
}
if (item is Control control)
{
if (VisualRoot == null) return;
control.Measure(Size.Infinity);
SelectionBoxItem = new Rectangle
{
Width = control.DesiredSize.Width,
Height = control.DesiredSize.Height,
Fill = new VisualBrush
{
Visual = control,
Stretch = Stretch.None,
AlignmentX = AlignmentX.Left,
}
};
// TODO: Implement flow direction udpate
}
else
{
if (ItemTemplate is null && DisplayMemberBinding is { } binding)
{
var template = new FuncDataTemplate<object?>((a,_) => new TextBlock
{
[DataContextProperty] = a,
[!TextBlock.TextProperty] = binding,
});
var textBlock = template.Build(item);
SelectionBoxItem = textBlock;
}
else
{
SelectionBoxItem = item;
}
}
}
private TreeComboBoxItem? GetContainerFromEventSource(object eventSource)
{
if (eventSource is Visual visual)
{
var item = visual.GetSelfAndVisualAncestors().OfType<TreeComboBoxItem>().FirstOrDefault();
return item?.Owner == this ? item : null!;
}
return null;
}
private object? TreeItemFromContainer(Control container)
{
return TreeItemFromContainer(this, container);
}
private Control? TreeContainerFromItem(object item)
{
return TreeContainerFromItem(this, item);
}
private static Control? TreeContainerFromItem(ItemsControl itemsControl, object item)
{
if (itemsControl.ContainerFromItem(item) is { } container)
{
return container;
}
foreach (var child in itemsControl.GetRealizedContainers())
{
if(child is ItemsControl childItemsControl && TreeContainerFromItem(childItemsControl, item) is { } childContainer)
{
return childContainer;
}
}
return null;
}
private static object? TreeItemFromContainer(ItemsControl itemsControl, Control container)
{
if (itemsControl.ItemFromContainer(container) is { } item)
{
return item;
}
foreach (var child in itemsControl.GetRealizedContainers())
{
if(child is ItemsControl childItemsControl && TreeItemFromContainer(childItemsControl, container) is { } childItem)
{
return childItem;
}
}
return null;
}
private void MarkContainerSelection(object? item, bool selected)
{
if (item is null) return;
var container = TreeContainerFromItem(item);
if (container is TreeComboBoxItem treeComboBoxItem)
{
treeComboBoxItem.IsSelected = selected;
}
}
public void Clear()
{
SelectedItem = null;
}
public object? InnerLeftContent { get; set; }
public object? InnerRightContent { get; set; }
public object? PopupInnerTopContent { get; set; }
public object? PopupInnerBottomContent { get; set; }
}