using System.Collections.Specialized; using System.Reactive.Disposables; using AtomUI.Controls; using AtomUI.Controls.Utils; using AtomUI.Data; using AtomUI.Desktop.Controls.Themes; using AtomUI.MotionScene; using Avalonia; using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.VisualTree; namespace AtomUI.Desktop.Controls; using AvaloniaTreeItem = Avalonia.Controls.TreeViewItem; [PseudoClasses(TreeViewPseudoClass.NodeToggleTypeCheckBox, TreeViewPseudoClass.NodeToggleTypeRadio)] public class TreeViewItem : AvaloniaTreeItem, IRadioButton, ITreeViewItemData { #region 公共属性定义 public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); public static readonly StyledProperty IsCheckedProperty = AvaloniaProperty.Register(nameof(IsChecked), false); public static readonly StyledProperty SwitcherExpandIconProperty = AvaloniaProperty.Register(nameof(SwitcherExpandIcon)); public static readonly StyledProperty SwitcherCollapseIconProperty = AvaloniaProperty.Register(nameof(SwitcherCollapseIcon)); public static readonly StyledProperty SwitcherRotationIconProperty = AvaloniaProperty.Register(nameof(SwitcherRotationIcon)); public static readonly StyledProperty SwitcherLoadingIconProperty = AvaloniaProperty.Register(nameof(SwitcherRotationIcon)); public static readonly StyledProperty SwitcherLeafIconProperty = AvaloniaProperty.Register(nameof(SwitcherLeafIcon)); public static readonly DirectProperty IsLeafProperty = AvaloniaProperty.RegisterDirect(nameof(IsLeaf), o => o.IsLeaf); public static readonly StyledProperty IsLoadingProperty = AvaloniaProperty.Register(nameof(IsLoading), false); public static readonly StyledProperty GroupNameProperty = RadioButton.GroupNameProperty.AddOwner(); public static readonly StyledProperty IsIndicatorEnabledProperty = AvaloniaProperty.Register(nameof(IsIndicatorEnabled), true); public PathIcon? Icon { get => GetValue(IconProperty); set => SetValue(IconProperty, value); } public PathIcon? SwitcherExpandIcon { get => GetValue(SwitcherExpandIconProperty); set => SetValue(SwitcherExpandIconProperty, value); } public PathIcon? SwitcherCollapseIcon { get => GetValue(SwitcherCollapseIconProperty); set => SetValue(SwitcherCollapseIconProperty, value); } public PathIcon? SwitcherRotationIcon { get => GetValue(SwitcherRotationIconProperty); set => SetValue(SwitcherRotationIconProperty, value); } public PathIcon? SwitcherLoadingIcon { get => GetValue(SwitcherLoadingIconProperty); set => SetValue(SwitcherLoadingIconProperty, value); } public PathIcon? SwitcherLeafIcon { get => GetValue(SwitcherLeafIconProperty); set => SetValue(SwitcherLeafIconProperty, value); } public bool? IsChecked { get => GetValue(IsCheckedProperty); set => SetValue(IsCheckedProperty, value); } private bool _isLeaf; public bool IsLeaf { get => _isLeaf; internal set => SetAndRaise(IsLeafProperty, ref _isLeaf, value); } public bool IsLoading { get => GetValue(IsLoadingProperty); set => SetValue(IsLoadingProperty, value); } public string? GroupName { get => GetValue(GroupNameProperty); set => SetValue(GroupNameProperty, value); } public bool IsIndicatorEnabled { get => GetValue(IsIndicatorEnabledProperty); set => SetValue(IsIndicatorEnabledProperty, value); } IList ITreeNode.Children => Items.OfType().ToList(); ITreeNode? ITreeNode.ParentNode => Parent as ITreeNode; public TreeNodeKey? ItemKey { get; set; } #endregion #region 公共事件定义 public static readonly RoutedEvent ClickEvent = RoutedEvent.Register( nameof(Click), RoutingStrategies.Bubble); public event EventHandler? Click { add => AddHandler(ClickEvent, value); remove => RemoveHandler(ClickEvent, value); } #endregion #region 内部属性定义 internal static readonly DirectProperty NodeHoverModeProperty = AvaloniaProperty.RegisterDirect(nameof(NodeHoverMode), o => o.NodeHoverMode, (o, v) => o.NodeHoverMode = v); internal static readonly DirectProperty IsShowLineProperty = AvaloniaProperty.RegisterDirect(nameof(IsShowLine), o => o.IsShowLine, (o, v) => o.IsShowLine = v); internal static readonly DirectProperty IsShowIconProperty = AvaloniaProperty.RegisterDirect(nameof(IsShowIcon), o => o.IsShowIcon, (o, v) => o.IsShowIcon = v); internal static readonly DirectProperty IsDraggingProperty = AvaloniaProperty.RegisterDirect(nameof(IsDragging), o => o.IsDragging, (o, v) => o.IsDragging = v); internal static readonly DirectProperty IsDragOverProperty = AvaloniaProperty.RegisterDirect(nameof(IsDragOver), o => o.IsDragOver, (o, v) => o.IsDragOver = v); internal static readonly StyledProperty IsMotionEnabledProperty = MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); internal static readonly DirectProperty IsSwitcherRotationProperty = AvaloniaProperty.RegisterDirect( nameof(IsSwitcherRotation), o => o.IsSwitcherRotation, (o, v) => o.IsSwitcherRotation = v); internal static readonly StyledProperty ToggleTypeProperty = TreeView.ToggleTypeProperty.AddOwner(); internal static readonly StyledProperty IsShowLeafIconProperty = AvaloniaProperty.Register(nameof(IsShowLeafIcon)); private TreeItemHoverMode _nodeHoverMode; internal TreeItemHoverMode NodeHoverMode { get => _nodeHoverMode; set => SetAndRaise(NodeHoverModeProperty, ref _nodeHoverMode, value); } private bool _isShowLine; internal bool IsShowLine { get => _isShowLine; set => SetAndRaise(IsShowLineProperty, ref _isShowLine, value); } private bool _isShowIcon = true; internal bool IsShowIcon { get => _isShowIcon; set => SetAndRaise(IsShowIconProperty, ref _isShowIcon, value); } private bool _isDragging; internal bool IsDragging { get => _isDragging; set => SetAndRaise(IsDraggingProperty, ref _isDragging, value); } private bool _isDragOver; internal bool IsDragOver { get => _isDragOver; set => SetAndRaise(IsDragOverProperty, ref _isDragOver, value); } internal bool IsMotionEnabled { get => GetValue(IsMotionEnabledProperty); set => SetValue(IsMotionEnabledProperty, value); } private bool _isSwitcherRotation; internal bool IsSwitcherRotation { get => _isSwitcherRotation; set => SetAndRaise(IsSwitcherRotationProperty, ref _isSwitcherRotation, value); } public ItemToggleType ToggleType { get => GetValue(ToggleTypeProperty); set => SetValue(ToggleTypeProperty, value); } internal bool IsShowLeafIcon { get => GetValue(IsShowLeafIconProperty); set => SetValue(IsShowLeafIconProperty, value); } internal TreeView? OwnerTreeView { get; set; } private ITreeViewInteractionHandler? TreeViewInteractionHandler => this.FindLogicalAncestorOfType()?.InteractionHandler; #endregion private bool _animating; private BaseMotionActor? _itemsPresenterMotionActor; private readonly BorderRenderHelper _borderRenderHelper; private TreeViewItemHeader? _header; private readonly Dictionary _itemsBindingDisposables = new(); static TreeViewItem() { AffectsRender( IsShowLineProperty, IsShowLeafIconProperty, IsDraggingProperty, IsDragOverProperty, BorderBrushProperty, BorderThicknessProperty, NodeHoverModeProperty, BackgroundProperty); } public TreeViewItem() { _borderRenderHelper = new BorderRenderHelper(); LogicalChildren.CollectionChanged += HandleItemsCollectionChanged; } private void HandleItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Remove) { if (e.OldItems != null) { foreach (var item in e.OldItems) { if (item is TreeViewItem treeViewItem) { if (_itemsBindingDisposables.TryGetValue(treeViewItem, out var disposable)) { disposable.Dispose(); } _itemsBindingDisposables.Remove(treeViewItem); } } } } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); OwnerTreeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ItemCountProperty) { IsLeaf = ItemCount == 0; } else if (change.Property == GroupNameProperty) { HandleGroupNameChanged(change); } else if (change.Property == IsCheckedProperty) { HandleIsCheckedChanged(change); } else if (change.Property == ToggleTypeProperty) { HandleToggleTypeChanged(change); } if (this.IsAttachedToVisualTree()) { if (change.Property == IsExpandedProperty) { HandleExpandedChanged(); } } } private void HandleExpandedChanged() { if (IsExpanded) { ExpandChildren(); } else { CollapseChildren(); } } private void HandleGroupNameChanged(AvaloniaPropertyChangedEventArgs change) { (TreeViewInteractionHandler as DefaultTreeViewInteractionHandler)?.OnGroupOrTypeChanged(this, change.GetOldValue()); } private void HandleToggleTypeChanged(AvaloniaPropertyChangedEventArgs change) { var newValue = change.GetNewValue(); PseudoClasses.Set(TreeViewPseudoClass.NodeToggleTypeRadio, newValue == ItemToggleType.Radio); PseudoClasses.Set(TreeViewPseudoClass.NodeToggleTypeCheckBox, newValue == ItemToggleType.CheckBox); (TreeViewInteractionHandler as DefaultTreeViewInteractionHandler)?.OnGroupOrTypeChanged(this, GroupName); } private void HandleIsCheckedChanged(AvaloniaPropertyChangedEventArgs change) { var newValue = change.GetNewValue(); PseudoClasses.Set(StdPseudoClass.Checked, newValue == true); (TreeViewInteractionHandler as DefaultTreeViewInteractionHandler)?.OnCheckedChanged(this); } private void ExpandChildren() { if (_itemsPresenterMotionActor is null || _animating || OwnerTreeView is null) { return; } if (!IsMotionEnabled || OwnerTreeView.IsExpandAllProcess) { _itemsPresenterMotionActor.Opacity = 1.0; _itemsPresenterMotionActor.IsVisible = true; return; } _animating = true; _header?.NotifyAnimating(true); var motion = OwnerTreeView.OpenMotion ?? new ExpandMotion(Direction.Top, null, new CubicEaseOut()); motion.Duration = OwnerTreeView.MotionDuration; motion.Run(_itemsPresenterMotionActor, () => { _itemsPresenterMotionActor.IsVisible = true; }, () => { _animating = false; _header?.NotifyAnimating(false); }); } private void CollapseChildren() { if (_itemsPresenterMotionActor is null || _animating || OwnerTreeView is null) { return; } if (!IsMotionEnabled || OwnerTreeView.IsExpandAllProcess) { _itemsPresenterMotionActor.Opacity = 0.0; _itemsPresenterMotionActor.IsVisible = false; return; } _animating = true; _header?.NotifyAnimating(true); var motion = OwnerTreeView.CloseMotion ?? new CollapseMotion(Direction.Top, null, new CubicEaseIn()); motion.Duration = OwnerTreeView.MotionDuration; motion.Run(_itemsPresenterMotionActor, null, () => { _itemsPresenterMotionActor.IsVisible = false; _animating = false; _header?.NotifyAnimating(false); }); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _header = e.NameScope.Find(TreeViewItemThemeConstants.HeaderPart); _itemsPresenterMotionActor = e.NameScope.Find(TreeViewItemThemeConstants.ItemsPresenterMotionActorPart); IsLeaf = ItemCount == 0; var originIsMotionEnabled = IsMotionEnabled; try { SetCurrentValue(IsMotionEnabledProperty, false); HandleExpandedChanged(); } finally { SetCurrentValue(IsMotionEnabledProperty, originIsMotionEnabled); } } internal bool IsEffectiveCheckable() { if (!IsEnabled || !IsIndicatorEnabled || ToggleType == ItemToggleType.None) { return false; } return true; } public override void Render(DrawingContext context) { if (NodeHoverMode == TreeItemHoverMode.WholeLine && _header != null) { using var state = context.PushTransform(Matrix.CreateTranslation(_header.Bounds.X, 0)); _borderRenderHelper.Render(context, _header.Bounds.Size, new Thickness(), CornerRadius, BackgroundSizing.InnerBorderEdge, Background, null); } if (IsShowLine) { RenderTreeNodeLine(context); } } private void RenderTreeNodeLine(DrawingContext context) { if (_header is null) { return; } var penWidth = BorderThickness.Top; using var state = context.PushRenderOptions(new RenderOptions { EdgeMode = EdgeMode.Aliased }); var isLastChild = false; if (Parent is ItemsControl parentTreeItem) { if (parentTreeItem.ContainerFromIndex(parentTreeItem.ItemCount - 1) == this) { isLastChild = true; } } var switcherButtonRect = _header.SwitcherButtonRect(this); if (!IsLeaf && !isLastChild && _itemsPresenterMotionActor?.IsVisible == true) { var switcherMiddleBottom = new Point(switcherButtonRect.X + switcherButtonRect.Width / 2, switcherButtonRect.Bottom); var blockStartPoint = new Point(switcherMiddleBottom.X, switcherMiddleBottom.Y); var blockEndPoint = new Point(blockStartPoint.X, DesiredSize.Height); context.DrawLine(new Pen(BorderBrush, penWidth), blockStartPoint, blockEndPoint); } // 画孩子线条 if (!IsShowLeafIcon && IsLeaf) { { // 纵向 var childStartPoint = new Point(switcherButtonRect.X + switcherButtonRect.Width / 2, switcherButtonRect.Top); var childEndPoint = new Point(switcherButtonRect.X + switcherButtonRect.Width / 2, isLastChild ? switcherButtonRect.Bottom : switcherButtonRect.Top + DesiredSize.Height); if (isLastChild) { childEndPoint = childEndPoint.WithY(childEndPoint.Y / 2); } context.DrawLine(new Pen(BorderBrush, penWidth), childStartPoint.WithY(0), childEndPoint); } { // 横向 var childStartPoint = new Point(switcherButtonRect.X + switcherButtonRect.Width / 2 - penWidth / 2, switcherButtonRect.Top + switcherButtonRect.Height / 2 - penWidth / 2); var childEndPoint = new Point(switcherButtonRect.Right, switcherButtonRect.Top + switcherButtonRect.Height / 2 - penWidth / 2); context.DrawLine(new Pen(BorderBrush, penWidth), childStartPoint, childEndPoint); } } } internal Rect GetDragBounds(bool includeChildren = false) { var dragOffset = _header?.GetDragIndicatorOffset(this) ?? default; var offsetX = dragOffset.X; return new Rect(new Point(offsetX, 0), new Size(Bounds.Width - offsetX, includeChildren ? Bounds.Height : _header?.Bounds.Height ?? default)); } internal bool IsInDragBounds(Point point) { return GetDragBounds(true).Contains(point); } internal bool IsInDragHeaderBounds(Point point) { return GetDragBounds().Contains(point); } internal bool IsDragOverForOffsetY(Point point) { if (OwnerTreeView is null) { return false; } return new Rect(Bounds.Size).Contains(point); } internal DragPreviewAdorner BuildPreviewAdorner() { return new DragPreviewAdorner(_header!); } protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); if (PointInHeaderBounds(e)) { NotifyHeaderClick(); } } internal bool PointInHeaderBounds(PointerReleasedEventArgs e) { var bounds = new Rect(new Point(0, 0), _header?.DesiredSize ?? default); var point = e.GetPosition(_header); return bounds.Contains(point); } protected virtual void NotifyHeaderClick() { } internal void RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return new TreeViewItem(); } protected override bool NeedsContainerOverride( object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); } protected override void ContainerForItemPreparedOverride( Control container, object? item, int index) { base.ContainerForItemPreparedOverride(container, item, index); if (container is TreeViewItem treeViewItem) { treeViewItem.OwnerTreeView = OwnerTreeView; var disposables = new CompositeDisposable(8); if (item != null && item is not Visual && item is ITreeViewItemData treeViewItemData) { ApplyNodeData(treeViewItem, treeViewItemData); } if (ItemTemplate != null) { disposables.Add(BindUtils.RelayBind(this, ItemTemplateProperty, treeViewItem, TreeViewItem.HeaderTemplateProperty)); } disposables.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, treeViewItem, TreeViewItem.IsMotionEnabledProperty)); disposables.Add(BindUtils.RelayBind(this, NodeHoverModeProperty, treeViewItem, TreeViewItem.NodeHoverModeProperty)); disposables.Add(BindUtils.RelayBind(this, IsShowLineProperty, treeViewItem, TreeViewItem.IsShowLineProperty)); disposables.Add(BindUtils.RelayBind(this, IsShowIconProperty, treeViewItem, TreeViewItem.IsShowIconProperty)); disposables.Add(BindUtils.RelayBind(this, IsShowLeafIconProperty, treeViewItem, TreeViewItem.IsShowLeafIconProperty)); disposables.Add(BindUtils.RelayBind(this, IsSwitcherRotationProperty, treeViewItem, TreeViewItem.IsSwitcherRotationProperty)); disposables.Add(BindUtils.RelayBind(this, ToggleTypeProperty, treeViewItem, TreeViewItem.ToggleTypeProperty)); PrepareTreeViewItem(treeViewItem, item, index, disposables); if (_itemsBindingDisposables.TryGetValue(treeViewItem, out var oldDisposables)) { oldDisposables.Dispose(); _itemsBindingDisposables.Remove(treeViewItem); } _itemsBindingDisposables.Add(treeViewItem, disposables); } else { throw new ArgumentOutOfRangeException(nameof(container), "The container type is incorrect, it must be type TreeViewItem."); } } protected virtual void PrepareTreeViewItem(TreeViewItem treeViewItem, object? item, int index, CompositeDisposable compositeDisposable) { } internal static void ApplyNodeData(TreeViewItem treeViewItem, ITreeViewItemData treeViewItemData) { if (treeViewItemData is not Visual) { if (!treeViewItem.IsSet(TreeViewItem.HeaderProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.HeaderProperty, treeViewItem); } if (!treeViewItem.IsSet(TreeViewItem.IconProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.IconProperty, treeViewItemData.Icon); } if (treeViewItem.ItemKey == null) { treeViewItem.ItemKey = treeViewItemData.ItemKey; } if (!treeViewItem.IsSet(TreeViewItem.IsCheckedProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.IsCheckedProperty, treeViewItemData.IsChecked); } if (!treeViewItem.IsSet(TreeViewItem.IsSelectedProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.IsSelectedProperty, treeViewItemData.IsSelected); } if (!treeViewItem.IsSet(TreeViewItem.IsEnabledProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.IsEnabledProperty, treeViewItemData.IsEnabled); } if (!treeViewItem.IsSet(TreeViewItem.IsExpandedProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.IsExpandedProperty, treeViewItemData.IsExpanded); } if (!treeViewItem.IsSet(TreeViewItem.IsIndicatorEnabledProperty)) { treeViewItem.SetCurrentValue(TreeViewItem.IsIndicatorEnabledProperty, treeViewItemData.IsIndicatorEnabled); } } } }