git push Merge branch 'bugfix/esc-ticketdropdown-hang'

This commit is contained in:
Meik
2026-02-20 16:21:02 +01:00
9 changed files with 3523 additions and 3605 deletions

View File

@@ -3,70 +3,25 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FasdDesktopUi.Basics.UserControls"
xmlns:ico="clr-namespace:FasdDesktopUi.Basics.UserControls.AdaptableIcon;assembly=F4SD-AdaptableIcon"
mc:Ignorable="d"
d:DesignHeight="20"
d:DesignWidth="40"
x:Name="BadgeControl"
Loaded="BadgeControl_Loaded"
Unloaded="BadgeControl_Unloaded">
x:Name="BadgeControl">
<Grid ClipToBounds="False">
<Border Background="{DynamicResource Color.SoftContrast}"
CornerRadius="5">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal"
Margin="7.5 2.5">
<TextBlock FontSize="12"
FontWeight="Bold"
Foreground="{DynamicResource FontColor.DetailsPage.DataHistory.Value}"
Text="{Binding ElementName=BadgeControl, Path=Text}" />
</StackPanel>
</Border>
<Border Background="{DynamicResource Color.SoftContrast}"
CornerRadius="5">
<Canvas x:Name="SparkleCanvas"
IsHitTestVisible="False"
ClipToBounds="False"
Visibility="Collapsed">
<Grid x:Name="SparkleTopRight"
Canvas.Right="-3"
Canvas.Top="-4"
Opacity="0"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<ScaleTransform ScaleX="0.6" ScaleY="0.6" />
</Grid.RenderTransform>
<Rectangle Width="2"
Height="8"
RadiusX="1"
RadiusY="1"
Fill="{DynamicResource FontColor.DetailsPage.DataHistory.Value}" />
<Rectangle Width="8"
Height="2"
RadiusX="1"
RadiusY="1"
Fill="{DynamicResource FontColor.DetailsPage.DataHistory.Value}" />
</Grid>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal"
Margin="7.5 2.5">
<TextBlock FontSize="12"
FontWeight="Bold"
Foreground="{DynamicResource FontColor.DetailsPage.DataHistory.Value}"
Text="{Binding ElementName=BadgeControl, Path=Text}" />
</StackPanel>
<Grid x:Name="SparkleBottomLeft"
Canvas.Left="-3"
Canvas.Bottom="-4"
Opacity="0"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<ScaleTransform ScaleX="0.6" ScaleY="0.6" />
</Grid.RenderTransform>
<Rectangle Width="2"
Height="8"
RadiusX="1"
RadiusY="1"
Fill="{DynamicResource FontColor.DetailsPage.DataHistory.Value}" />
<Rectangle Width="8"
Height="2"
RadiusX="1"
RadiusY="1"
Fill="{DynamicResource FontColor.DetailsPage.DataHistory.Value}" />
</Grid>
</Canvas>
</Grid>
</Border>
</UserControl>

View File

@@ -1,16 +1,10 @@
using System;
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace FasdDesktopUi.Basics.UserControls
{
public partial class Badge : UserControl
{
private readonly Storyboard _sparkleStoryboard;
private const double SparkleBaseScale = 0.6;
public string Text
{
get { return (string)GetValue(TextProperty); }
@@ -20,118 +14,10 @@ namespace FasdDesktopUi.Basics.UserControls
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(Badge), new PropertyMetadata("Beta"));
public bool IsSparkleEnabled
{
get { return (bool)GetValue(IsSparkleEnabledProperty); }
set { SetValue(IsSparkleEnabledProperty, value); }
}
public static readonly DependencyProperty IsSparkleEnabledProperty =
DependencyProperty.Register(nameof(IsSparkleEnabled), typeof(bool), typeof(Badge), new PropertyMetadata(false, OnIsSparkleEnabledChanged));
public Badge()
{
InitializeComponent();
_sparkleStoryboard = CreateSparkleStoryboard();
UpdateSparkleState();
}
private static void OnIsSparkleEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as Badge;
control?.UpdateSparkleState();
}
private void BadgeControl_Loaded(object sender, RoutedEventArgs e)
{
UpdateSparkleState();
}
private void BadgeControl_Unloaded(object sender, RoutedEventArgs e)
{
StopSparkles();
}
private void UpdateSparkleState()
{
if (SparkleCanvas == null || _sparkleStoryboard == null)
{
return;
}
if (IsSparkleEnabled)
{
SparkleCanvas.Visibility = Visibility.Visible;
_sparkleStoryboard.Begin(this, true);
return;
}
SparkleCanvas.Visibility = Visibility.Collapsed;
StopSparkles();
}
private void StopSparkles()
{
_sparkleStoryboard?.Stop(this);
if (SparkleTopRight != null)
{
SparkleTopRight.Opacity = 0;
if (SparkleTopRight.RenderTransform is ScaleTransform topTransform)
{
topTransform.ScaleX = SparkleBaseScale;
topTransform.ScaleY = SparkleBaseScale;
}
}
if (SparkleBottomLeft != null)
{
SparkleBottomLeft.Opacity = 0;
if (SparkleBottomLeft.RenderTransform is ScaleTransform bottomTransform)
{
bottomTransform.ScaleX = SparkleBaseScale;
bottomTransform.ScaleY = SparkleBaseScale;
}
}
}
private Storyboard CreateSparkleStoryboard()
{
var storyboard = new Storyboard
{
RepeatBehavior = RepeatBehavior.Forever
};
AddKeyFrames(storyboard, SparkleTopRight, "Opacity",
(0.00, 0.0), (0.18, 1.0), (0.42, 0.0), (2.20, 0.0));
AddKeyFrames(storyboard, SparkleTopRight, "(UIElement.RenderTransform).(ScaleTransform.ScaleX)",
(0.00, SparkleBaseScale), (0.22, 1.15), (0.42, SparkleBaseScale), (2.20, SparkleBaseScale));
AddKeyFrames(storyboard, SparkleTopRight, "(UIElement.RenderTransform).(ScaleTransform.ScaleY)",
(0.00, SparkleBaseScale), (0.22, 1.15), (0.42, SparkleBaseScale), (2.20, SparkleBaseScale));
AddKeyFrames(storyboard, SparkleBottomLeft, "Opacity",
(0.00, 0.0), (1.00, 0.0), (1.18, 1.0), (1.42, 0.0), (2.20, 0.0));
AddKeyFrames(storyboard, SparkleBottomLeft, "(UIElement.RenderTransform).(ScaleTransform.ScaleX)",
(0.00, SparkleBaseScale), (1.00, SparkleBaseScale), (1.22, 1.15), (1.42, SparkleBaseScale), (2.20, SparkleBaseScale));
AddKeyFrames(storyboard, SparkleBottomLeft, "(UIElement.RenderTransform).(ScaleTransform.ScaleY)",
(0.00, SparkleBaseScale), (1.00, SparkleBaseScale), (1.22, 1.15), (1.42, SparkleBaseScale), (2.20, SparkleBaseScale));
return storyboard;
}
private static void AddKeyFrames(Storyboard storyboard, DependencyObject target, string targetProperty, params (double TimeSeconds, double Value)[] frames)
{
var animation = new DoubleAnimationUsingKeyFrames();
foreach (var frame in frames)
{
animation.KeyFrames.Add(new LinearDoubleKeyFrame(frame.Value, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(frame.TimeSeconds))));
}
Storyboard.SetTarget(animation, target);
Storyboard.SetTargetProperty(animation, new PropertyPath(targetProperty));
storyboard.Children.Add(animation);
}
}
}

View File

@@ -1,7 +1,7 @@
using C4IT.FASD.Base;
using C4IT.MultiLanguage;
using FasdDesktopUi.Basics.Helper;
using FasdDesktopUi.Basics.Models;
using C4IT.MultiLanguage;
using FasdDesktopUi.Basics.Helper;
using FasdDesktopUi.Basics.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -125,25 +125,25 @@ namespace FasdDesktopUi.Basics.UserControls
}
public static readonly DependencyProperty SearchDataChangedProperty =
DependencyProperty.Register("SearchDataChanged", typeof(EventHandler<cF4sdHealthSelectionDataRequest>), typeof(ComboBoxPageable), new PropertyMetadata(null));
#endregion
#region RestoreParentScrollFocusOnDropDownClose
public bool RestoreParentScrollFocusOnDropDownClose
{
get { return (bool)GetValue(RestoreParentScrollFocusOnDropDownCloseProperty); }
set { SetValue(RestoreParentScrollFocusOnDropDownCloseProperty, value); }
}
public static readonly DependencyProperty RestoreParentScrollFocusOnDropDownCloseProperty =
DependencyProperty.Register("RestoreParentScrollFocusOnDropDownClose", typeof(bool), typeof(ComboBoxPageable), new PropertyMetadata(false));
#endregion
#region ItemData
public static readonly DependencyProperty SearchDataChangedProperty =
DependencyProperty.Register("SearchDataChanged", typeof(EventHandler<cF4sdHealthSelectionDataRequest>), typeof(ComboBoxPageable), new PropertyMetadata(null));
#endregion
#region RestoreParentScrollFocusOnDropDownClose
public bool RestoreParentScrollFocusOnDropDownClose
{
get { return (bool)GetValue(RestoreParentScrollFocusOnDropDownCloseProperty); }
set { SetValue(RestoreParentScrollFocusOnDropDownCloseProperty, value); }
}
public static readonly DependencyProperty RestoreParentScrollFocusOnDropDownCloseProperty =
DependencyProperty.Register("RestoreParentScrollFocusOnDropDownClose", typeof(bool), typeof(ComboBoxPageable), new PropertyMetadata(false));
#endregion
#region ItemData
public ObservableCollection<KeyValuePair<string, object>> ItemData
{
@@ -222,6 +222,15 @@ namespace FasdDesktopUi.Basics.UserControls
return null;
}
public bool CloseDropDownIfOpen()
{
if (ComboBoxControl?.IsDropDownOpen != true)
return false;
ComboBoxControl.IsDropDownOpen = false;
return true;
}
#region Paging Events
#region PageBack
@@ -316,33 +325,42 @@ namespace FasdDesktopUi.Basics.UserControls
}
}
private void ComboBoxControl_DropDownClosed(object sender, EventArgs e)
{
timer.Stop();
cFocusInvoker.InvokeLostFocus(this, e);
if (RestoreParentScrollFocusOnDropDownClose)
{
_ = Dispatcher.BeginInvoke((Action)(() =>
{
try
{
var parentScrollViewer = cUiElementHelper.GetFirstParentOfType<ScrollViewer>(this);
Keyboard.ClearFocus();
if (parentScrollViewer != null)
{
parentScrollViewer.Focus();
Keyboard.Focus(parentScrollViewer);
}
}
catch (Exception exception)
{
LogException(exception);
}
}), System.Windows.Threading.DispatcherPriority.Input);
}
}
private void SearchTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Escape)
return;
if (CloseDropDownIfOpen())
e.Handled = true;
}
private void ComboBoxControl_DropDownClosed(object sender, EventArgs e)
{
timer.Stop();
cFocusInvoker.InvokeLostFocus(this, e);
if (RestoreParentScrollFocusOnDropDownClose)
{
_ = Dispatcher.BeginInvoke((Action)(() =>
{
try
{
var parentScrollViewer = cUiElementHelper.GetFirstParentOfType<ScrollViewer>(this);
Keyboard.ClearFocus();
if (parentScrollViewer != null)
{
parentScrollViewer.Focus();
Keyboard.Focus(parentScrollViewer);
}
}
catch (Exception exception)
{
LogException(exception);
}
}), System.Windows.Threading.DispatcherPriority.Input);
}
}
private void ComboBoxControl_DropDownOpened(object sender, EventArgs e)
{
@@ -384,6 +402,8 @@ namespace FasdDesktopUi.Basics.UserControls
TextBox searchTextBox = FindVisualChild<TextBox>(partPopup.Child, "SearchTextBox");
if (searchTextBox != null)
{
searchTextBox.PreviewKeyDown -= SearchTextBox_PreviewKeyDown;
searchTextBox.PreviewKeyDown += SearchTextBox_PreviewKeyDown;
// Setzen des Fokus auf TextBox
searchTextBox.Focus();
}

View File

@@ -1,429 +1,451 @@
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;
namespace FasdDesktopUi.Basics.UserControls
{
public partial class HierarchicalSelectionControl : UserControl
{
private readonly ObservableCollection<HierarchicalSelectionItem> visibleItems = new ObservableCollection<HierarchicalSelectionItem>();
private readonly Dictionary<string, HierarchicalSelectionItem> itemLookup = new Dictionary<string, HierarchicalSelectionItem>();
private readonly DispatcherTimer searchDelayTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
private string lastSearchText = string.Empty;
private bool suppressTreeSelectionChanged;
private TextBox searchTextBox;
private TreeView treeViewControl;
private ScrollViewer itemsScrollViewer;
public ObservableCollection<HierarchicalSelectionItem> 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;
}
#region DependencyProperties
public ObservableCollection<HierarchicalSelectionItem> ItemsSource
{
get => (ObservableCollection<HierarchicalSelectionItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
nameof(ItemsSource),
typeof(ObservableCollection<HierarchicalSelectionItem>),
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);
}
}
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.TryExpandToSelectedItem();
control.SyncTreeSelectionWithSelectedItem(bringIntoView: false);
}
}
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
#region UI Event Handling
private void ComboBoxControl_DropDownOpened(object sender, EventArgs e)
{
EnsureTemplateParts();
searchTextBox?.Focus();
searchTextBox?.SelectAll();
suppressTreeSelectionChanged = false;
SyncTreeSelectionWithSelectedItem(bringIntoView: true);
DropDownOpened?.Invoke(this, e);
}
private void ComboBoxControl_DropDownClosed(object sender, EventArgs e)
{
searchDelayTimer.Stop();
suppressTreeSelectionChanged = false;
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;
ApplyFilter(lastSearchText);
}
private void TreeViewControl_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (suppressTreeSelectionChanged)
return;
if (e.NewValue is HierarchicalSelectionItem selected)
{
var original = ResolveOriginalItem(selected);
if (original != null && !Equals(SelectedItem, original))
{
SelectedItem = original;
}
suppressTreeSelectionChanged = true;
ComboBoxControl.IsDropDownOpen = false;
}
}
#endregion
#region Data Preparation and Filtering
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();
SyncTreeSelectionWithSelectedItem(bringIntoView: false);
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);
}
// If the selected item is part of current results, keep it visually selected.
SyncTreeSelectionWithSelectedItem(bringIntoView: false);
}
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;
}
}
#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()
{
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;
}
if (itemsScrollViewer == null)
{
itemsScrollViewer = ComboBoxControl.Template.FindName("PART_ItemsScrollViewer", ComboBoxControl) as ScrollViewer;
}
}
private void ItemsScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scroller = itemsScrollViewer ?? sender as ScrollViewer;
if (scroller == null || scroller.ScrollableHeight <= 0)
return;
var lines = SystemParameters.WheelScrollLines;
if (lines < 0)
{
if (e.Delta < 0)
scroller.PageDown();
else
scroller.PageUp();
e.Handled = true;
return;
}
if (lines == 0)
{
scroller.ScrollToVerticalOffset(scroller.VerticalOffset - e.Delta);
e.Handled = true;
return;
}
var direction = e.Delta < 0 ? 1 : -1;
var stepCount = Math.Max(1, Math.Abs(e.Delta) / 120) * lines;
for (var i = 0; i < stepCount; i++)
{
if (direction > 0)
scroller.LineDown();
else
scroller.LineUp();
}
e.Handled = true;
}
#endregion
#region Keyboard
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;
}
}
#endregion
}
}
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;
namespace FasdDesktopUi.Basics.UserControls
{
public partial class HierarchicalSelectionControl : UserControl
{
private readonly ObservableCollection<HierarchicalSelectionItem> visibleItems = new ObservableCollection<HierarchicalSelectionItem>();
private readonly Dictionary<string, HierarchicalSelectionItem> itemLookup = new Dictionary<string, HierarchicalSelectionItem>();
private readonly DispatcherTimer searchDelayTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
private string lastSearchText = string.Empty;
private bool suppressTreeSelectionChanged;
private TextBox searchTextBox;
private TreeView treeViewControl;
private ScrollViewer itemsScrollViewer;
public ObservableCollection<HierarchicalSelectionItem> VisibleItems => visibleItems;
public event EventHandler DropDownOpened;
public event EventHandler DropDownClosed;
public HierarchicalSelectionControl()
{
InitializeComponent();
searchDelayTimer.Tick += SearchDelayTimer_Tick;
}
public bool CloseDropDownIfOpen()
{
if (ComboBoxControl?.IsDropDownOpen != true)
return false;
ComboBoxControl.IsDropDownOpen = false;
return true;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
EnsureTemplateParts();
if (treeViewControl != null)
treeViewControl.ItemsSource = VisibleItems;
}
#region DependencyProperties
public ObservableCollection<HierarchicalSelectionItem> ItemsSource
{
get => (ObservableCollection<HierarchicalSelectionItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
nameof(ItemsSource),
typeof(ObservableCollection<HierarchicalSelectionItem>),
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);
}
}
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.TryExpandToSelectedItem();
control.SyncTreeSelectionWithSelectedItem(bringIntoView: false);
}
}
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
#region UI Event Handling
private void ComboBoxControl_DropDownOpened(object sender, EventArgs e)
{
EnsureTemplateParts();
searchTextBox?.Focus();
searchTextBox?.SelectAll();
suppressTreeSelectionChanged = false;
SyncTreeSelectionWithSelectedItem(bringIntoView: true);
DropDownOpened?.Invoke(this, e);
}
private void ComboBoxControl_DropDownClosed(object sender, EventArgs e)
{
searchDelayTimer.Stop();
suppressTreeSelectionChanged = false;
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;
ApplyFilter(lastSearchText);
}
private void TreeViewControl_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (suppressTreeSelectionChanged)
return;
if (e.NewValue is HierarchicalSelectionItem selected)
{
var original = ResolveOriginalItem(selected);
if (original != null && !Equals(SelectedItem, original))
{
SelectedItem = original;
}
suppressTreeSelectionChanged = true;
ComboBoxControl.IsDropDownOpen = false;
}
}
#endregion
#region Data Preparation and Filtering
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();
SyncTreeSelectionWithSelectedItem(bringIntoView: false);
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);
}
// If the selected item is part of current results, keep it visually selected.
SyncTreeSelectionWithSelectedItem(bringIntoView: false);
}
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;
}
}
#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()
{
if (treeViewControl == null)
{
treeViewControl = ComboBoxControl.Template.FindName("PART_TreeView", ComboBoxControl) as TreeView;
if (treeViewControl != null)
{
treeViewControl.SelectedItemChanged += TreeViewControl_SelectedItemChanged;
treeViewControl.PreviewKeyDown += DropDownContent_PreviewKeyDown;
treeViewControl.ItemsSource = VisibleItems;
}
}
if (searchTextBox == null)
{
searchTextBox = ComboBoxControl.Template.FindName("PART_SearchTextBox", ComboBoxControl) as TextBox;
if (searchTextBox != null)
{
searchTextBox.TextChanged += SearchTextBox_TextChanged;
searchTextBox.PreviewKeyDown += DropDownContent_PreviewKeyDown;
}
}
if (itemsScrollViewer == null)
{
itemsScrollViewer = ComboBoxControl.Template.FindName("PART_ItemsScrollViewer", ComboBoxControl) as ScrollViewer;
}
}
private void ItemsScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scroller = itemsScrollViewer ?? sender as ScrollViewer;
if (scroller == null || scroller.ScrollableHeight <= 0)
return;
var lines = SystemParameters.WheelScrollLines;
if (lines < 0)
{
if (e.Delta < 0)
scroller.PageDown();
else
scroller.PageUp();
e.Handled = true;
return;
}
if (lines == 0)
{
scroller.ScrollToVerticalOffset(scroller.VerticalOffset - e.Delta);
e.Handled = true;
return;
}
var direction = e.Delta < 0 ? 1 : -1;
var stepCount = Math.Max(1, Math.Abs(e.Delta) / 120) * lines;
for (var i = 0; i < stepCount; i++)
{
if (direction > 0)
scroller.LineDown();
else
scroller.LineUp();
}
e.Handled = true;
}
private void DropDownContent_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Escape)
return;
if (CloseDropDownIfOpen())
e.Handled = true;
}
#endregion
#region Keyboard
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;
}
}
#endregion
}
}

View File

@@ -167,7 +167,9 @@
<Border Background="{DynamicResource BackgroundColor.DetailsPage.Widget.Title}"
CornerRadius="7.5"
Width="{Binding ElementName=TicketSelectionBorder, Path=ActualWidth}">
<StackPanel x:Name="TicketSelectionContainer" />
<StackPanel x:Name="TicketSelectionContainer"
Focusable="True"
PreviewKeyDown="TicketSelectionContainer_PreviewKeyDown" />
</Border>
</Popup>
</StackPanel>

View File

@@ -120,7 +120,6 @@
FontWeight="Bold"
Visibility="Visible" />
<buc:Badge Margin="6 0 0 0"
IsSparkleEnabled="True"
VerticalAlignment="Center"
Text="Beta">
<buc:Badge.LayoutTransform>

View File

@@ -18,7 +18,7 @@
Background="Transparent"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
PreviewKeyDown="CustomMessageBox_PreviewKeyDown"
KeyDown="CustomMessageBox_KeyDown"
IsVisibleChanged="BlurInvoker_IsActiveChanged">
<WindowChrome.WindowChrome>

View File

@@ -3,26 +3,26 @@ using FasdDesktopUi.Basics;
using FasdDesktopUi.Basics.Models;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using WinForms = System.Windows.Forms;
using static C4IT.Logging.cLogManager;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using WinForms = System.Windows.Forms;
using static C4IT.Logging.cLogManager;
namespace FasdDesktopUi.Pages.TicketCompletion
{
public partial class TicketCompletion : Window, IBlurInvoker, INotifyPropertyChanged
{
private const double MinWindowHeightDip = 220d;
private const double WindowWorkingAreaMarginDip = 12d;
private const double DialogNonContentReserveDip = 180d;
private bool isUpdatingDialogBounds;
private bool isCanceled = false;
public partial class TicketCompletion : Window, IBlurInvoker, INotifyPropertyChanged
{
private const double MinWindowHeightDip = 220d;
private const double WindowWorkingAreaMarginDip = 12d;
private const double DialogNonContentReserveDip = 180d;
private bool isUpdatingDialogBounds;
private bool isCanceled = false;
private bool _WaitForClosing = false;
public bool WaitForClosing
@@ -49,31 +49,31 @@ namespace FasdDesktopUi.Pages.TicketCompletion
CloseCaseDialogUc.DataProvider = _dataProvider;
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
cFocusInvoker.GotFocus += ElementGotFocus;
cFocusInvoker.LostFocus += ElementLostFocus;
SizeChanged += TicketCompletion_SizeChanged;
Loaded += TicketCompletion_Loaded;
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
UpdateDialogMaxHeightToScreen();
}
protected override void OnLocationChanged(EventArgs e)
{
base.OnLocationChanged(e);
UpdateDialogMaxHeightToScreen();
}
private void TicketCompletion_Loaded(object sender, RoutedEventArgs e) => UpdateDialogMaxHeightToScreen();
private void TicketCompletion_SizeChanged(object sender, SizeChangedEventArgs e) => UpdateDialogMaxHeightToScreen();
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
cFocusInvoker.GotFocus += ElementGotFocus;
cFocusInvoker.LostFocus += ElementLostFocus;
SizeChanged += TicketCompletion_SizeChanged;
Loaded += TicketCompletion_Loaded;
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
UpdateDialogMaxHeightToScreen();
}
protected override void OnLocationChanged(EventArgs e)
{
base.OnLocationChanged(e);
UpdateDialogMaxHeightToScreen();
}
private void TicketCompletion_Loaded(object sender, RoutedEventArgs e) => UpdateDialogMaxHeightToScreen();
private void TicketCompletion_SizeChanged(object sender, SizeChangedEventArgs e) => UpdateDialogMaxHeightToScreen();
#region ClosingBusy
@@ -126,98 +126,98 @@ namespace FasdDesktopUi.Pages.TicketCompletion
return null;
}
#region Close_Click
private void Close_Click()
{
isCanceled = true;
TrySetDialogResult(null);
Close();
}
#region Close_Click
private void CloseButton_Click(object sender, InputEventArgs e) => Close_Click();
#endregion
private void TrySetDialogResult(bool? result)
{
try
{
DialogResult = result;
}
catch (InvalidOperationException)
{
// Window was not shown as dialog; ignore.
}
}
private void UpdateDialogMaxHeightToScreen()
{
if (isUpdatingDialogBounds)
return;
isUpdatingDialogBounds = true;
try
{
WinForms.Screen screen = null;
IntPtr currentHandle = new WindowInteropHelper(this).Handle;
if (currentHandle != IntPtr.Zero)
{
screen = WinForms.Screen.FromHandle(currentHandle);
}
else if (Owner != null)
{
IntPtr ownerHandle = new WindowInteropHelper(Owner).Handle;
if (ownerHandle != IntPtr.Zero)
screen = WinForms.Screen.FromHandle(ownerHandle);
}
screen = screen ?? WinForms.Screen.PrimaryScreen;
var workingArea = screen?.WorkingArea ?? WinForms.Screen.PrimaryScreen.WorkingArea;
var dpiScaleY = VisualTreeHelper.GetDpi(this).DpiScaleY;
var safeDpiScaleY = Math.Max(0.1, dpiScaleY);
var workingAreaTopDip = workingArea.Top / safeDpiScaleY;
var workingAreaBottomDip = workingArea.Bottom / safeDpiScaleY;
var workingAreaHeightDip = workingArea.Height / safeDpiScaleY;
var availableWindowHeightDip = workingAreaHeightDip - (WindowWorkingAreaMarginDip * 2);
MaxHeight = Math.Max(MinWindowHeightDip, availableWindowHeightDip);
if (!double.IsNaN(Top))
{
var minTop = workingAreaTopDip + WindowWorkingAreaMarginDip;
var maxBottom = workingAreaBottomDip - WindowWorkingAreaMarginDip;
if (Top < minTop)
Top = minTop;
var currentBottom = Top + ActualHeight;
if (currentBottom > maxBottom)
Top = Math.Max(minTop, maxBottom - ActualHeight);
}
double nonDialogReserve = DialogNonContentReserveDip;
if (CloseCaseDialogUc != null && CloseCaseDialogUc.IsLoaded)
{
var estimatedReserve = ActualHeight - CloseCaseDialogUc.ActualHeight;
if (estimatedReserve > 0)
nonDialogReserve = Math.Max(nonDialogReserve, estimatedReserve + WindowWorkingAreaMarginDip);
}
CloseCaseDialogUc?.SetDialogContentMaxHeight(MaxHeight - nonDialogReserve);
}
catch (Exception ex)
{
LogException(ex);
}
finally
{
isUpdatingDialogBounds = false;
}
}
#region Internal Focus Events
private void Close_Click()
{
isCanceled = true;
TrySetDialogResult(null);
Close();
}
private void CloseButton_Click(object sender, InputEventArgs e) => Close_Click();
#endregion
private void TrySetDialogResult(bool? result)
{
try
{
DialogResult = result;
}
catch (InvalidOperationException)
{
// Window was not shown as dialog; ignore.
}
}
private void UpdateDialogMaxHeightToScreen()
{
if (isUpdatingDialogBounds)
return;
isUpdatingDialogBounds = true;
try
{
WinForms.Screen screen = null;
IntPtr currentHandle = new WindowInteropHelper(this).Handle;
if (currentHandle != IntPtr.Zero)
{
screen = WinForms.Screen.FromHandle(currentHandle);
}
else if (Owner != null)
{
IntPtr ownerHandle = new WindowInteropHelper(Owner).Handle;
if (ownerHandle != IntPtr.Zero)
screen = WinForms.Screen.FromHandle(ownerHandle);
}
screen = screen ?? WinForms.Screen.PrimaryScreen;
var workingArea = screen?.WorkingArea ?? WinForms.Screen.PrimaryScreen.WorkingArea;
var dpiScaleY = VisualTreeHelper.GetDpi(this).DpiScaleY;
var safeDpiScaleY = Math.Max(0.1, dpiScaleY);
var workingAreaTopDip = workingArea.Top / safeDpiScaleY;
var workingAreaBottomDip = workingArea.Bottom / safeDpiScaleY;
var workingAreaHeightDip = workingArea.Height / safeDpiScaleY;
var availableWindowHeightDip = workingAreaHeightDip - (WindowWorkingAreaMarginDip * 2);
MaxHeight = Math.Max(MinWindowHeightDip, availableWindowHeightDip);
if (!double.IsNaN(Top))
{
var minTop = workingAreaTopDip + WindowWorkingAreaMarginDip;
var maxBottom = workingAreaBottomDip - WindowWorkingAreaMarginDip;
if (Top < minTop)
Top = minTop;
var currentBottom = Top + ActualHeight;
if (currentBottom > maxBottom)
Top = Math.Max(minTop, maxBottom - ActualHeight);
}
double nonDialogReserve = DialogNonContentReserveDip;
if (CloseCaseDialogUc != null && CloseCaseDialogUc.IsLoaded)
{
var estimatedReserve = ActualHeight - CloseCaseDialogUc.ActualHeight;
if (estimatedReserve > 0)
nonDialogReserve = Math.Max(nonDialogReserve, estimatedReserve + WindowWorkingAreaMarginDip);
}
CloseCaseDialogUc?.SetDialogContentMaxHeight(MaxHeight - nonDialogReserve);
}
catch (Exception ex)
{
LogException(ex);
}
finally
{
isUpdatingDialogBounds = false;
}
}
#region Internal Focus Events
private void ElementGotFocus(object sender, EventArgs e)
{
@@ -280,14 +280,15 @@ namespace FasdDesktopUi.Pages.TicketCompletion
#endregion
private void CustomMessageBox_PreviewKeyDown(object sender, KeyEventArgs e)
private void CustomMessageBox_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
if (e.Key != Key.Escape)
{
case Key.Escape:
Close_Click();
break;
return;
}
Close_Click();
e.Handled = true;
}
private async Task DoCloseActionAsync()
@@ -303,15 +304,15 @@ namespace FasdDesktopUi.Pages.TicketCompletion
bool closedSuccessfull = await CloseCaseDialogUc.CloseCaseAsync(_dataProvider.Identities.FirstOrDefault(identity => identity.Class == enumFasdInformationClass.User).Id);
if (closedSuccessfull)
{
SuccessPage.SuccessPage successPage = new SuccessPage.SuccessPage();
successPage.Show();
await _dataProvider?.CloseCaseAsync();
TrySetDialogResult(true);
Close();
}
}
if (closedSuccessfull)
{
SuccessPage.SuccessPage successPage = new SuccessPage.SuccessPage();
successPage.Show();
await _dataProvider?.CloseCaseAsync();
TrySetDialogResult(true);
Close();
}
}
catch (Exception E)
{
LogException(E);