using System; using System.Collections; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using WPFDark.Interfaces; using WPFDark.Internals; using Jewelry.Memory; namespace WPFDark.Controls { public class BiaTreeView : TreeView { public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register( nameof(SelectedItems), typeof(IList), typeof(BiaTreeView)); public IList SelectedItems { get => (IList) GetValue(SelectedItemsProperty); set => SetValue(SelectedItemsProperty, value); } public new static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register( nameof(SelectedItem), typeof(object), typeof(BiaTreeView), new FrameworkPropertyMetadata(OnSelectedItemChanged)); private static void OnSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { var self = (BiaTreeView) obj; self._selectedItem = e.NewValue; } private object? _selectedItem; public new object? SelectedItem { get => _selectedItem; set { if (_selectedItem != value) SetValue(SelectedItemProperty, value); } } public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.RegisterAttached( "IsSelected", typeof(bool), typeof(BiaTreeView), new FrameworkPropertyMetadata(OnIsSelectedChanged)); public static bool GetIsSelected(TreeViewItem target) { return (bool) target.GetValue(IsSelectedProperty); } public static void SetIsSelected(TreeViewItem target, bool value) { target.SetValue(IsSelectedProperty, value); } public event EventHandler? ItemSelectionStarting; public event EventHandler? ItemSelectionCompleted; #region CornerRadius public CornerRadius CornerRadius { get => _CornerRadius; set { if (value != _CornerRadius) SetValue(CornerRadiusProperty, value); } } private CornerRadius _CornerRadius = Constants.GroupCornerRadius; public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register( nameof(CornerRadius), typeof(CornerRadius), typeof(BiaTreeView), new PropertyMetadata( Constants.GroupCornerRadius, (s, e) => { var self = (BiaTreeView) s; self._CornerRadius = (CornerRadius) e.NewValue; })); #endregion #region IsVisibleItemExpanderButton public bool IsVisibleItemExpanderButton { get => _IsVisibleItemExpanderButton; set { if (value != _IsVisibleItemExpanderButton) SetValue(IsVisibleItemExpanderButtonProperty, value); } } private bool _IsVisibleItemExpanderButton = true; public static readonly DependencyProperty IsVisibleItemExpanderButtonProperty = DependencyProperty.Register( nameof(IsVisibleItemExpanderButton), typeof(bool), typeof(BiaTreeView), new PropertyMetadata( true, (s, e) => { var self = (BiaTreeView) s; self._IsVisibleItemExpanderButton = (bool) e.NewValue; })); #endregion #region IsSelectionEnabled public bool IsSelectionEnabled { get => _IsSelectionEnabled; set { if (value != _IsSelectionEnabled) SetValue(IsSelectionEnabledProperty, value); } } private bool _IsSelectionEnabled = true; public static readonly DependencyProperty IsSelectionEnabledProperty = DependencyProperty.Register( nameof(IsSelectionEnabled), typeof(bool), typeof(BiaTreeView), new PropertyMetadata( true, (s, e) => { var self = (BiaTreeView) s; self._IsSelectionEnabled = (bool) e.NewValue; })); #endregion #region IndentSize public double IndentSize { get => _IndentSize; set { if (NumberHelper.AreClose(value, _IndentSize) == false) SetValue(IndentSizeProperty, value); } } private double _IndentSize = 19.0; public static readonly DependencyProperty IndentSizeProperty = DependencyProperty.Register( nameof(IndentSize), typeof(double), typeof(BiaTreeView), new PropertyMetadata( 19.0, (s, e) => { var self = (BiaTreeView) s; self._IndentSize = (double) e.NewValue; })); #endregion static BiaTreeView() { DefaultStyleKeyProperty.OverrideMetadata(typeof(BiaTreeView), new FrameworkPropertyMetadata(typeof(BiaTreeView))); } private static void OnIsSelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { var treeViewItem = (TreeViewItem)obj; var parent = treeViewItem.GetParent(); if (parent is null) return; var selectedItems = parent.SelectedItems; var isSelected = GetIsSelected(treeViewItem); if (selectedItems != null) { if (isSelected) selectedItems.Add(treeViewItem.DataContext); else selectedItems.Remove(treeViewItem.DataContext); } if (isSelected) { if (parent.SelectedItem is null) parent.SelectedItem = treeViewItem.DataContext; } else { if (selectedItems is null || selectedItems.Count == 0) parent.SelectedItem = null; else parent.SelectedItem = selectedItems[0]; } } private INotifyCollectionChanged? _oldItemsSource; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly PropertyChangeNotifier _itemsSourceChangeNotifier; public BiaTreeView() { _itemsSourceChangeNotifier = new PropertyChangeNotifier(this, ItemsSourceProperty); _itemsSourceChangeNotifier.ValueChanged += ItemsSourceChangedHandler; } private void ItemsSourceChangedHandler(object? sender, EventArgs e) { if (_oldItemsSource != null) RemoveCollectionChangedEvent(_oldItemsSource); var itemsSource = ItemsSource as INotifyCollectionChanged; if (itemsSource != null) AddCollectionChangedEvent(itemsSource); _oldItemsSource = itemsSource; } private void AddCollectionChangedEvent(INotifyCollectionChanged c) { c.CollectionChanged += ItemsSourceOnCollectionChanged; if (!(c is IList list)) return; foreach (var item in list) { if (!(item is IBiaHasChildren hasChildren)) continue; if (hasChildren.Children is INotifyCollectionChanged ncc) AddCollectionChangedEvent(ncc); foreach (var child in hasChildren.Children) if (child is IBiaHasChildren nccHasChildren) if (nccHasChildren.Children is INotifyCollectionChanged nccChild) AddCollectionChangedEvent(nccChild); } } private void RemoveCollectionChangedEvent(INotifyCollectionChanged c) { c.CollectionChanged -= ItemsSourceOnCollectionChanged; if (!(c is IList list)) return; foreach (var item in list) { if (!(item is IBiaHasChildren hasChildren)) continue; if (hasChildren.Children is INotifyCollectionChanged ncc) RemoveCollectionChangedEvent(ncc); foreach (var child in hasChildren.Children) if (child is IBiaHasChildren nccHasChildren) if (nccHasChildren.Children is INotifyCollectionChanged nccChild) RemoveCollectionChangedEvent(nccChild); } } private void ItemsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: { var vm = e.NewItems[0]; var item = this.EnumerateChildren().FirstOrDefault(x => x.DataContext == vm); item?.BringIntoView(); _multipleSelectionEdgeItemDataContext = vm; foreach (var newItem in e.NewItems.OfType()) if (newItem.Children is INotifyCollectionChanged ncc) AddCollectionChangedEvent(ncc); break; } case NotifyCollectionChangedAction.Remove: if (SelectedItems != null) { foreach (var item in e.OldItems) SelectedItems.Remove(item); SelectedItem = SelectedItems.Count == 0 ? null : SelectedItems[0]; } _multipleSelectionEdgeItemDataContext = SelectedItem; foreach (var oldItem in e.OldItems.OfType()) if (oldItem.Children is INotifyCollectionChanged ncc) RemoveCollectionChangedEvent(ncc); break; case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: throw new NotImplementedException(); default: throw new ArgumentOutOfRangeException(); } } protected override void OnPreviewKeyDown(KeyEventArgs e) { base.OnPreviewKeyDown(e); if (!(e.OriginalSource is TreeViewItem treeViewItem)) return; // [CTRL] + A if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { SelectAllItems(); e.Handled = true; } var targetItem = e.Key switch { Key.Down => GetRelativeItem(treeViewItem, 1), Key.Up => GetRelativeItem(treeViewItem, -1), _ => null }; if (targetItem is null) { e.Handled = true; return; } if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) SelectMultipleItems(targetItem); else SelectSingleItem(targetItem); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnPreviewMouseLeftButtonDown(e); OnPreviewMouseLeftButton(e, true); } protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e) { base.OnPreviewMouseLeftButtonUp(e); OnPreviewMouseLeftButton(e, false); } private void OnPreviewMouseLeftButton(MouseButtonEventArgs e, bool isDown) { if (!(e.OriginalSource is FrameworkElement orgSource)) return; // アイテムの改変マークだったら選択処理を行わない var toggleButton = orgSource.GetParent(); if (toggleButton != null) if (toggleButton.Name == WPFDark.Internals.Constants.TreeViewItemExpanderName) if (toggleButton.Visibility == Visibility.Visible) return; var treeViewItem = orgSource.GetParent(); if (treeViewItem is null) return; switch (Keyboard.Modifiers) { case ModifierKeys.Control: if (isDown) ToggleSingleItem(treeViewItem); break; case ModifierKeys.Shift: if (isDown) { if (_multipleSelectionEdgeItemDataContext is null) SelectSingleItem(treeViewItem); else SelectMultipleItems(treeViewItem); } break; default: if (isDown) { if (GetIsSelected(treeViewItem) == false) SelectSingleItem(treeViewItem); } else SelectSingleItem(treeViewItem); break; } } private void SelectAllItems() { ItemSelectionStarting?.Invoke(this, EventArgs.Empty); { var items = this.EnumerateChildren(); TreeViewItem? firstItem = null; foreach (var item in items) { if (firstItem is null) firstItem = item; SetIsSelected(item, true); } if (firstItem != null) { SelectedItem = firstItem.DataContext; _multipleSelectionEdgeItemDataContext = firstItem.DataContext; } } ItemSelectionCompleted?.Invoke(this, EventArgs.Empty); } private void ToggleSingleItem(TreeViewItem treeViewItem) { ItemSelectionStarting?.Invoke(this, EventArgs.Empty); { var current = GetIsSelected(treeViewItem); var next = !current; SetIsSelected(treeViewItem, next); SelectedItem = next ? treeViewItem.DataContext : null; _multipleSelectionEdgeItemDataContext = treeViewItem.DataContext; } ItemSelectionCompleted?.Invoke(this, EventArgs.Empty); } private void SelectSingleItem(TreeViewItem treeViewItem) { ItemSelectionStarting?.Invoke(this, EventArgs.Empty); { var items = this.EnumerateChildren(); foreach (var item in items) SetIsSelected(item, item == treeViewItem); SelectedItem = treeViewItem.DataContext; _multipleSelectionEdgeItemDataContext = treeViewItem.DataContext; } ItemSelectionCompleted?.Invoke(this, EventArgs.Empty); } private object? _multipleSelectionEdgeItemDataContext; private void SelectMultipleItems(TreeViewItem edgeItem) { if (_multipleSelectionEdgeItemDataContext is null) return; if (edgeItem is null) return; if (edgeItem.DataContext == _multipleSelectionEdgeItemDataContext) { SelectSingleItem(edgeItem); return; } ItemSelectionStarting?.Invoke(this, EventArgs.Empty); { var isInSelection = false; foreach (var item in this.EnumerateChildren()) { if (ReferenceEquals(item.DataContext, edgeItem.DataContext) || ReferenceEquals(item.DataContext, _multipleSelectionEdgeItemDataContext)) { isInSelection = !isInSelection; SetIsSelected(item, true); if (isInSelection == false) break; } else if (isInSelection) { SetIsSelected(item, true); } } SelectedItem = edgeItem.DataContext; _multipleSelectionEdgeItemDataContext = edgeItem.DataContext; } ItemSelectionCompleted?.Invoke(this, EventArgs.Empty); } [SuppressMessage("ReSharper", "PossiblyImpureMethodCallOnReadonlyVariable")] private T? GetRelativeItem(T item, int relativePosition) where T : ItemsControl { if (item is null) throw new ArgumentNullException(nameof(item)); using var items = this.EnumerateChildren().ToTempBuffer(128); var index = items.IndexOf(item); if (index == -1) return null; var relativeIndex = index + relativePosition; if (relativeIndex >= 0 && relativeIndex < items.Length) return items[relativeIndex]; return null; } } }