Compare commits

..

2 Commits

Author SHA1 Message Date
Meik
764a8cffb8 generic hierarchical control 2026-02-09 21:07:39 +01:00
Meik
e8584c1453 kategorie fix highlight current value 2026-02-09 20:43:47 +01:00
3 changed files with 177 additions and 67 deletions

View File

@@ -105,12 +105,12 @@
Padding="4" Padding="4"
BorderBrush="{DynamicResource BackgroundColor.Menu.SubCategory.Hover}" BorderBrush="{DynamicResource BackgroundColor.Menu.SubCategory.Hover}"
BorderThickness="1"> BorderThickness="1">
<ScrollViewer x:Name="PART_CategoryScrollViewer" <ScrollViewer x:Name="PART_ItemsScrollViewer"
MaxHeight="320" MaxHeight="320"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
PreviewMouseWheel="CategoryScrollViewer_PreviewMouseWheel"> PreviewMouseWheel="ItemsScrollViewer_PreviewMouseWheel">
<TreeView x:Name="PART_TreeView" <TreeView x:Name="PART_TreeView"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"

View File

@@ -8,7 +8,6 @@ using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using static C4IT.Logging.cLogManager;
namespace FasdDesktopUi.Basics.UserControls namespace FasdDesktopUi.Basics.UserControls
{ {
@@ -22,7 +21,7 @@ namespace FasdDesktopUi.Basics.UserControls
private TextBox searchTextBox; private TextBox searchTextBox;
private TreeView treeViewControl; private TreeView treeViewControl;
private ScrollViewer categoryScrollViewer; private ScrollViewer itemsScrollViewer;
public ObservableCollection<HierarchicalSelectionItem> VisibleItems => visibleItems; public ObservableCollection<HierarchicalSelectionItem> VisibleItems => visibleItems;
@@ -41,7 +40,6 @@ namespace FasdDesktopUi.Basics.UserControls
EnsureTemplateParts(); EnsureTemplateParts();
if (treeViewControl != null) if (treeViewControl != null)
treeViewControl.ItemsSource = VisibleItems; treeViewControl.ItemsSource = VisibleItems;
UpdateDisplaySelection();
} }
#region DependencyProperties #region DependencyProperties
@@ -64,7 +62,6 @@ namespace FasdDesktopUi.Basics.UserControls
{ {
control.RebuildLookup(); control.RebuildLookup();
control.ApplyFilter(control.lastSearchText); control.ApplyFilter(control.lastSearchText);
control.TryExpandToSelectedItem();
} }
} }
@@ -84,8 +81,8 @@ namespace FasdDesktopUi.Basics.UserControls
{ {
if (d is HierarchicalSelectionControl control) if (d is HierarchicalSelectionControl control)
{ {
control.LogSelectedItemChange(e.NewValue as HierarchicalSelectionItem);
control.TryExpandToSelectedItem(); control.TryExpandToSelectedItem();
control.SyncTreeSelectionWithSelectedItem(bringIntoView: false);
} }
} }
@@ -109,13 +106,15 @@ namespace FasdDesktopUi.Basics.UserControls
#endregion #endregion
#region UI Event Handling
private void ComboBoxControl_DropDownOpened(object sender, EventArgs e) private void ComboBoxControl_DropDownOpened(object sender, EventArgs e)
{ {
EnsureTemplateParts(); EnsureTemplateParts();
searchTextBox?.Focus(); searchTextBox?.Focus();
searchTextBox?.SelectAll(); searchTextBox?.SelectAll();
suppressTreeSelectionChanged = false; suppressTreeSelectionChanged = false;
LogEntry($"[CategoryPicker] DropDownOpened. Selected={SelectedItem?.FullPath ?? "<null>"}"); SyncTreeSelectionWithSelectedItem(bringIntoView: true);
DropDownOpened?.Invoke(this, e); DropDownOpened?.Invoke(this, e);
} }
@@ -123,7 +122,6 @@ namespace FasdDesktopUi.Basics.UserControls
{ {
searchDelayTimer.Stop(); searchDelayTimer.Stop();
suppressTreeSelectionChanged = false; suppressTreeSelectionChanged = false;
LogEntry("[CategoryPicker] DropDownClosed");
DropDownClosed?.Invoke(this, e); DropDownClosed?.Invoke(this, e);
} }
@@ -137,7 +135,6 @@ namespace FasdDesktopUi.Basics.UserControls
{ {
searchDelayTimer.Stop(); searchDelayTimer.Stop();
lastSearchText = searchTextBox?.Text ?? string.Empty; lastSearchText = searchTextBox?.Text ?? string.Empty;
LogEntry($"[CategoryPicker] Search text changed: '{lastSearchText}'");
ApplyFilter(lastSearchText); ApplyFilter(lastSearchText);
} }
@@ -152,7 +149,6 @@ namespace FasdDesktopUi.Basics.UserControls
if (original != null && !Equals(SelectedItem, original)) if (original != null && !Equals(SelectedItem, original))
{ {
SelectedItem = original; SelectedItem = original;
LogEntry($"[CategoryPicker] Tree selection changed: {original.FullPath}");
} }
suppressTreeSelectionChanged = true; suppressTreeSelectionChanged = true;
@@ -160,6 +156,10 @@ namespace FasdDesktopUi.Basics.UserControls
} }
} }
#endregion
#region Data Preparation and Filtering
private HierarchicalSelectionItem ResolveOriginalItem(HierarchicalSelectionItem item) private HierarchicalSelectionItem ResolveOriginalItem(HierarchicalSelectionItem item)
{ {
if (item == null) if (item == null)
@@ -203,6 +203,7 @@ namespace FasdDesktopUi.Basics.UserControls
} }
TryExpandToSelectedItem(); TryExpandToSelectedItem();
SyncTreeSelectionWithSelectedItem(bringIntoView: false);
return; return;
} }
@@ -220,6 +221,9 @@ namespace FasdDesktopUi.Basics.UserControls
clone.SetExpandedRecursive(true); clone.SetExpandedRecursive(true);
visibleItems.Add(clone); visibleItems.Add(clone);
} }
// If the selected item is part of current results, keep it visually selected.
SyncTreeSelectionWithSelectedItem(bringIntoView: false);
} }
private void TryExpandToSelectedItem() private void TryExpandToSelectedItem()
@@ -238,6 +242,101 @@ namespace FasdDesktopUi.Basics.UserControls
} }
} }
#endregion
#region Tree Selection Sync
private void SyncTreeSelectionWithSelectedItem(bool bringIntoView)
{
if (SelectedItem == null || string.IsNullOrWhiteSpace(SelectedItem.Id))
return;
EnsureTemplateParts();
if (treeViewControl == null)
return;
// Wait for popup/template layout to finish so item containers are generated.
Dispatcher.BeginInvoke(new Action(() =>
{
var target = FindVisibleItemById(VisibleItems, SelectedItem.Id);
if (target == null)
return;
var ancestor = target.Parent;
while (ancestor != null)
{
ancestor.IsExpanded = true;
ancestor = ancestor.Parent;
}
treeViewControl.UpdateLayout();
var targetContainer = GetTreeViewItemContainer(treeViewControl, target);
if (targetContainer == null)
return;
suppressTreeSelectionChanged = true;
try
{
targetContainer.IsSelected = true;
if (bringIntoView)
targetContainer.BringIntoView();
}
finally
{
suppressTreeSelectionChanged = false;
}
}), DispatcherPriority.Loaded);
}
private static HierarchicalSelectionItem FindVisibleItemById(IEnumerable<HierarchicalSelectionItem> items, string id)
{
if (items == null || string.IsNullOrWhiteSpace(id))
return null;
foreach (var item in items)
{
if (item == null)
continue;
if (string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase))
return item;
var childMatch = FindVisibleItemById(item.Children, id);
if (childMatch != null)
return childMatch;
}
return null;
}
private static TreeViewItem GetTreeViewItemContainer(ItemsControl root, object targetItem)
{
if (root == null || targetItem == null)
return null;
var directContainer = root.ItemContainerGenerator.ContainerFromItem(targetItem) as TreeViewItem;
if (directContainer != null)
return directContainer;
foreach (var child in root.Items)
{
var childContainer = root.ItemContainerGenerator.ContainerFromItem(child) as TreeViewItem;
if (childContainer == null)
continue;
childContainer.UpdateLayout();
var result = GetTreeViewItemContainer(childContainer, targetItem);
if (result != null)
return result;
}
return null;
}
#endregion
#region Template and Scroll Handling
private void EnsureTemplateParts() private void EnsureTemplateParts()
{ {
if (treeViewControl == null) if (treeViewControl == null)
@@ -257,15 +356,15 @@ namespace FasdDesktopUi.Basics.UserControls
searchTextBox.TextChanged += SearchTextBox_TextChanged; searchTextBox.TextChanged += SearchTextBox_TextChanged;
} }
if (categoryScrollViewer == null) if (itemsScrollViewer == null)
{ {
categoryScrollViewer = ComboBoxControl.Template.FindName("PART_CategoryScrollViewer", ComboBoxControl) as ScrollViewer; itemsScrollViewer = ComboBoxControl.Template.FindName("PART_ItemsScrollViewer", ComboBoxControl) as ScrollViewer;
} }
} }
private void CategoryScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) private void ItemsScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{ {
var scroller = categoryScrollViewer ?? sender as ScrollViewer; var scroller = itemsScrollViewer ?? sender as ScrollViewer;
if (scroller == null || scroller.ScrollableHeight <= 0) if (scroller == null || scroller.ScrollableHeight <= 0)
return; return;
@@ -300,23 +399,9 @@ namespace FasdDesktopUi.Basics.UserControls
e.Handled = true; e.Handled = true;
} }
private void UpdateDisplaySelection() #endregion
{
// Display handled by template TextBlock bound to SelectedItem.FullPath.
}
private void LogSelectedItemChange(HierarchicalSelectionItem newValue) #region Keyboard
{
var description = "<null>";
if (newValue != null)
{
var fullPath = string.IsNullOrWhiteSpace(newValue.FullPath) ? newValue.DisplayName : newValue.FullPath;
var id = string.IsNullOrWhiteSpace(newValue.Id) ? "<null>" : newValue.Id;
description = $"{fullPath} (Id={id})";
}
LogEntry($"[CategoryPicker] DependencyProperty SelectedItem updated -> {description}");
}
protected override void OnPreviewKeyDown(KeyEventArgs e) protected override void OnPreviewKeyDown(KeyEventArgs e)
{ {
@@ -338,5 +423,7 @@ namespace FasdDesktopUi.Basics.UserControls
e.Handled = true; e.Handled = true;
} }
} }
#endregion
} }
} }

View File

@@ -1,29 +1,30 @@
<settingspagebase:SettingsPageBase <settingspagebase:SettingsPageBase xmlns:settingspagebase="clr-namespace:FasdDesktopUi.Pages.SettingsPage"
xmlns:settingspagebase="clr-namespace:FasdDesktopUi.Pages.SettingsPage" x:Class="FasdDesktopUi.Pages.SettingsPage.SettingsPageView"
x:Class="FasdDesktopUi.Pages.SettingsPage.SettingsPageView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:FasdDesktopUi.Pages.SettingsPage"
xmlns:local="clr-namespace:FasdDesktopUi.Pages.SettingsPage" xmlns:ico="clr-namespace:FasdDesktopUi.Basics.UserControls.AdaptableIcon;assembly=F4SD-AdaptableIcon"
xmlns:ico="clr-namespace:FasdDesktopUi.Basics.UserControls.AdaptableIcon;assembly=F4SD-AdaptableIcon" xmlns:vc="clr-namespace:FasdDesktopUi.Basics.Converter"
xmlns:vc="clr-namespace:FasdDesktopUi.Basics.Converter" xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:sys="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d"
mc:Ignorable="d" Title="SettingsPageView"
Title="SettingsPageView" ResizeMode="NoResize"
ResizeMode="NoResize" WindowStyle="None"
WindowStyle="None" AllowsTransparency="True"
AllowsTransparency="True" Background="Transparent"
Background="Transparent" MinHeight="100"
MinHeight="100" MinWidth="400"
MinWidth="400" ShowInTaskbar="False"
ShowInTaskbar="False" Topmost="True"
Topmost="True" SizeToContent="WidthAndHeight"
SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen"
WindowStartupLocation="CenterScreen" Initialized="Window_Initialized"
Initialized="Window_Initialized" x:Name="SettingsWindow"
x:Name="SettingsWindow" KeyDown="SettingsWindow_KeyDown"
KeyDown="SettingsWindow_KeyDown" Closed="SettingsWindow_Closed" IsVisibleChanged="SettingsWindow_IsVisibleChanged"> Closed="SettingsWindow_Closed"
IsVisibleChanged="SettingsWindow_IsVisibleChanged">
<settingspagebase:SettingsPageBase.Resources> <settingspagebase:SettingsPageBase.Resources>
<vc:LanguageDefinitionsConverter x:Key="LanguageConverter" /> <vc:LanguageDefinitionsConverter x:Key="LanguageConverter" />
@@ -203,25 +204,39 @@
SelectedCountryCode="GB" /> SelectedCountryCode="GB" />
</StackPanel> </StackPanel>
<StackPanel x:Name="ShouldSkipSlimViewLabel" Orientation="Horizontal"> <StackPanel x:Name="ShouldSkipSlimViewLabel"
Orientation="Horizontal">
<TextBlock Text="{Binding Converter={StaticResource LanguageConverter}, ConverterParameter=Menu.ShouldSkipSlimView}" /> <TextBlock Text="{Binding Converter={StaticResource LanguageConverter}, ConverterParameter=Menu.ShouldSkipSlimView}" />
<ico:AdaptableIcon x:Name="ShouldSkipSlimViewPolicy" SelectedInternIcon="lock_closed" Margin="-25,-5,0,0" IconWidth="23" IconHeight="23" Visibility="Collapsed"/> <ico:AdaptableIcon x:Name="ShouldSkipSlimViewPolicy"
SelectedInternIcon="lock_closed"
PrimaryIconColor="{DynamicResource Color.Menu.Icon}"
Margin="-25,-5,0,0"
IconWidth="23"
IconHeight="23"
Visibility="Collapsed" />
</StackPanel> </StackPanel>
<CheckBox x:Name="ShouldSkipSlimViewCheckBox" <CheckBox x:Name="ShouldSkipSlimViewCheckBox"
Style="{DynamicResource ToggleSwitch}" Style="{DynamicResource ToggleSwitch}"
IsChecked="{Binding ElementName=SettingsWindow, Path=ShouldSkipSlimView}" IsChecked="{Binding ElementName=SettingsWindow, Path=ShouldSkipSlimView}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Margin="0,3,0,10" Margin="0,3,0,10">
>
</CheckBox> </CheckBox>
<StackPanel x:Name="PositionOfSmallViewsLabel" Orientation="Horizontal"> <StackPanel x:Name="PositionOfSmallViewsLabel"
Orientation="Horizontal">
<TextBlock Text="{Binding Converter={StaticResource LanguageConverter}, ConverterParameter=Menu.PositionOfSmallViews}" /> <TextBlock Text="{Binding Converter={StaticResource LanguageConverter}, ConverterParameter=Menu.PositionOfSmallViews}" />
<ico:AdaptableIcon x:Name="PositionOfSmallViewsPolicy" SelectedInternIcon="lock_closed" Margin="-25,-5,0,0" IconWidth="23" IconHeight="23" Visibility="Collapsed"/> <ico:AdaptableIcon x:Name="PositionOfSmallViewsPolicy"
SelectedInternIcon="lock_closed"
PrimaryIconColor="{DynamicResource Color.Menu.Icon}"
Margin="-25,-5,0,0"
IconWidth="23"
IconHeight="23"
Visibility="Collapsed" />
</StackPanel> </StackPanel>
<Grid x:Name="PositionOfSmallViewsInput" Grid.IsSharedSizeScope="True" <Grid x:Name="PositionOfSmallViewsInput"
Grid.IsSharedSizeScope="True"
HorizontalAlignment="Left"> HorizontalAlignment="Left">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" <ColumnDefinition Width="Auto"
@@ -269,12 +284,20 @@
</Grid> </Grid>
<StackPanel x:Name="PositionOfFavouriteBarLabel" Orientation="Horizontal"> <StackPanel x:Name="PositionOfFavouriteBarLabel"
Orientation="Horizontal">
<TextBlock Text="{Binding Converter={StaticResource LanguageConverter}, ConverterParameter=Menu.PositionOfFavouriteBar}" /> <TextBlock Text="{Binding Converter={StaticResource LanguageConverter}, ConverterParameter=Menu.PositionOfFavouriteBar}" />
<ico:AdaptableIcon x:Name="PositionOfFavouriteBarPolicy" SelectedInternIcon="lock_closed" Margin="-25,-5,0,0" IconWidth="23" IconHeight="23" Visibility="Collapsed"/> <ico:AdaptableIcon x:Name="PositionOfFavouriteBarPolicy"
SelectedInternIcon="lock_closed"
PrimaryIconColor="{DynamicResource Color.Menu.Icon}"
Margin="-25,-5,0,0"
IconWidth="23"
IconHeight="23"
Visibility="Collapsed" />
</StackPanel> </StackPanel>
<Grid x:Name="PositionOfFavouriteBarInput" Grid.IsSharedSizeScope="True" <Grid x:Name="PositionOfFavouriteBarInput"
Grid.IsSharedSizeScope="True"
HorizontalAlignment="Left"> HorizontalAlignment="Left">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" <ColumnDefinition Width="Auto"