using FasdDesktopUi.Basics.Models; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using static C4IT.Logging.cLogManager; namespace FasdDesktopUi.Basics.UserControls { public partial class HierarchicalSelectionControl : UserControl { private readonly ObservableCollection visibleItems = new ObservableCollection(); private readonly Dictionary itemLookup = new Dictionary(); private readonly DispatcherTimer searchDelayTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; private string lastSearchText = string.Empty; private bool suppressTreeSelectionChanged; private TextBox searchTextBox; private TreeView treeViewControl; public ObservableCollection VisibleItems => visibleItems; public event EventHandler DropDownOpened; public event EventHandler DropDownClosed; public HierarchicalSelectionControl() { InitializeComponent(); searchDelayTimer.Tick += SearchDelayTimer_Tick; } public override void OnApplyTemplate() { base.OnApplyTemplate(); EnsureTemplateParts(); if (treeViewControl != null) treeViewControl.ItemsSource = VisibleItems; UpdateDisplaySelection(); } #region DependencyProperties public ObservableCollection ItemsSource { get => (ObservableCollection)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( nameof(ItemsSource), typeof(ObservableCollection), typeof(HierarchicalSelectionControl), new PropertyMetadata(null, OnItemsSourceChanged)); private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is HierarchicalSelectionControl control) { control.RebuildLookup(); control.ApplyFilter(control.lastSearchText); control.TryExpandToSelectedItem(); } } public HierarchicalSelectionItem SelectedItem { get => (HierarchicalSelectionItem)GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register( nameof(SelectedItem), typeof(HierarchicalSelectionItem), typeof(HierarchicalSelectionControl), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged)); private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is HierarchicalSelectionControl control) { control.LogSelectedItemChange(e.NewValue as HierarchicalSelectionItem); control.TryExpandToSelectedItem(); } } public Brush ComboBoxBackground { get => (Brush)GetValue(ComboBoxBackgroundProperty); set => SetValue(ComboBoxBackgroundProperty, value); } public static readonly DependencyProperty ComboBoxBackgroundProperty = DependencyProperty.Register(nameof(ComboBoxBackground), typeof(Brush), typeof(HierarchicalSelectionControl), new PropertyMetadata(Brushes.Transparent)); public string SearchPlaceholderText { get => (string)GetValue(SearchPlaceholderTextProperty); set => SetValue(SearchPlaceholderTextProperty, value); } public static readonly DependencyProperty SearchPlaceholderTextProperty = DependencyProperty.Register(nameof(SearchPlaceholderText), typeof(string), typeof(HierarchicalSelectionControl), new PropertyMetadata(string.Empty)); #endregion private void ComboBoxControl_DropDownOpened(object sender, EventArgs e) { EnsureTemplateParts(); searchTextBox?.Focus(); searchTextBox?.SelectAll(); suppressTreeSelectionChanged = false; LogEntry($"[CategoryPicker] DropDownOpened. Selected={SelectedItem?.FullPath ?? ""}"); DropDownOpened?.Invoke(this, e); } private void ComboBoxControl_DropDownClosed(object sender, EventArgs e) { searchDelayTimer.Stop(); suppressTreeSelectionChanged = false; LogEntry("[CategoryPicker] DropDownClosed"); DropDownClosed?.Invoke(this, e); } private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) { searchDelayTimer.Stop(); searchDelayTimer.Start(); } private void SearchDelayTimer_Tick(object sender, EventArgs e) { searchDelayTimer.Stop(); lastSearchText = searchTextBox?.Text ?? string.Empty; LogEntry($"[CategoryPicker] Search text changed: '{lastSearchText}'"); ApplyFilter(lastSearchText); } private void TreeViewControl_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) { if (suppressTreeSelectionChanged) return; if (e.NewValue is HierarchicalSelectionItem selected) { var original = ResolveOriginalItem(selected); if (original != null && !Equals(SelectedItem, original)) { SelectedItem = original; LogEntry($"[CategoryPicker] Tree selection changed: {original.FullPath}"); } suppressTreeSelectionChanged = true; ComboBoxControl.IsDropDownOpen = false; } } private HierarchicalSelectionItem ResolveOriginalItem(HierarchicalSelectionItem item) { if (item == null) return null; if (!string.IsNullOrWhiteSpace(item.Id) && itemLookup.TryGetValue(item.Id, out var original)) return original; return null; } private void RebuildLookup() { itemLookup.Clear(); if (ItemsSource == null) return; foreach (var entry in ItemsSource.SelectMany(item => item.SelfAndDescendants())) { if (string.IsNullOrWhiteSpace(entry.Id)) continue; itemLookup[entry.Id] = entry; } } private void ApplyFilter(string searchText) { visibleItems.Clear(); if (ItemsSource == null) return; if (string.IsNullOrWhiteSpace(searchText)) { foreach (var item in ItemsSource) { item.SetExpandedRecursive(false); visibleItems.Add(item); } TryExpandToSelectedItem(); return; } var comparison = StringComparison.CurrentCultureIgnoreCase; foreach (var root in ItemsSource) { var clone = root.CloneBranch(node => node.FullPath?.IndexOf(searchText, comparison) >= 0 || node.DisplayName?.IndexOf(searchText, comparison) >= 0); if (clone == null) continue; clone.SetExpandedRecursive(true); visibleItems.Add(clone); } } private void TryExpandToSelectedItem() { if (SelectedItem == null || string.IsNullOrWhiteSpace(SelectedItem.Id)) return; if (!string.IsNullOrWhiteSpace(lastSearchText)) return; var chain = SelectedItem; while (chain != null) { chain.IsExpanded = true; chain = chain.Parent; } } private void EnsureTemplateParts() { if (treeViewControl == null) { treeViewControl = ComboBoxControl.Template.FindName("PART_TreeView", ComboBoxControl) as TreeView; if (treeViewControl != null) { treeViewControl.SelectedItemChanged += TreeViewControl_SelectedItemChanged; treeViewControl.ItemsSource = VisibleItems; } } if (searchTextBox == null) { searchTextBox = ComboBoxControl.Template.FindName("PART_SearchTextBox", ComboBoxControl) as TextBox; if (searchTextBox != null) searchTextBox.TextChanged += SearchTextBox_TextChanged; } } private void UpdateDisplaySelection() { // Display handled by template TextBlock bound to SelectedItem.FullPath. } private void LogSelectedItemChange(HierarchicalSelectionItem newValue) { var description = ""; if (newValue != null) { var fullPath = string.IsNullOrWhiteSpace(newValue.FullPath) ? newValue.DisplayName : newValue.FullPath; var id = string.IsNullOrWhiteSpace(newValue.Id) ? "" : newValue.Id; description = $"{fullPath} (Id={id})"; } LogEntry($"[CategoryPicker] DependencyProperty SelectedItem updated -> {description}"); } protected override void OnPreviewKeyDown(KeyEventArgs e) { base.OnPreviewKeyDown(e); if (!IsEnabled) return; if (!ComboBoxControl.IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Down || e.Key == Key.Space)) { ComboBoxControl.IsDropDownOpen = true; e.Handled = true; return; } if (ComboBoxControl.IsDropDownOpen && e.Key == Key.Escape) { ComboBoxControl.IsDropDownOpen = false; e.Handled = true; } } } }