emby-toolbox/EmbyToolbox/Behaviors/TreeViewScrollSyncBehavior.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

167 lines
4.4 KiB
C#

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace EmbyToolbox.Behaviors;
public static class TreeViewScrollSyncBehavior
{
public static readonly DependencyProperty SyncGroupProperty =
DependencyProperty.RegisterAttached(
"SyncGroup",
typeof(string),
typeof(TreeViewScrollSyncBehavior),
new PropertyMetadata(string.Empty, OnSyncGroupChanged));
private static readonly Dictionary<string, List<WeakReference<TreeView>>> Groups = new(StringComparer.Ordinal);
private static bool _syncing;
public static string GetSyncGroup(DependencyObject obj) => (string)obj.GetValue(SyncGroupProperty);
public static void SetSyncGroup(DependencyObject obj, string value) => obj.SetValue(SyncGroupProperty, value);
private static void OnSyncGroupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TreeView treeView)
{
return;
}
treeView.Loaded -= TreeViewOnLoaded;
treeView.Unloaded -= TreeViewOnUnloaded;
if (!string.IsNullOrWhiteSpace(e.NewValue as string))
{
treeView.Loaded += TreeViewOnLoaded;
treeView.Unloaded += TreeViewOnUnloaded;
}
}
private static void TreeViewOnLoaded(object sender, RoutedEventArgs e)
{
if (sender is not TreeView tree)
{
return;
}
var group = GetSyncGroup(tree);
if (string.IsNullOrWhiteSpace(group))
{
return;
}
if (!Groups.TryGetValue(group, out var list))
{
list = new List<WeakReference<TreeView>>();
Groups[group] = list;
}
list.Add(new WeakReference<TreeView>(tree));
var scroll = FindScrollViewer(tree);
if (scroll is not null)
{
scroll.ScrollChanged -= OnScrollChanged;
scroll.ScrollChanged += OnScrollChanged;
}
}
private static void TreeViewOnUnloaded(object sender, RoutedEventArgs e)
{
if (sender is not TreeView tree)
{
return;
}
var scroll = FindScrollViewer(tree);
if (scroll is not null)
{
scroll.ScrollChanged -= OnScrollChanged;
}
}
private static void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_syncing || e.VerticalChange == 0 || sender is not ScrollViewer sourceScroll)
{
return;
}
var sourceTree = FindAncestor<TreeView>(sourceScroll);
if (sourceTree is null)
{
return;
}
var group = GetSyncGroup(sourceTree);
if (string.IsNullOrWhiteSpace(group) || !Groups.TryGetValue(group, out var members))
{
return;
}
try
{
_syncing = true;
foreach (var weak in members.ToList())
{
if (!weak.TryGetTarget(out var targetTree))
{
members.Remove(weak);
continue;
}
if (ReferenceEquals(targetTree, sourceTree))
{
continue;
}
var targetScroll = FindScrollViewer(targetTree);
if (targetScroll is not null)
{
targetScroll.ScrollToVerticalOffset(sourceScroll.VerticalOffset);
}
}
}
finally
{
_syncing = false;
}
}
private static ScrollViewer? FindScrollViewer(DependencyObject root)
{
if (root is ScrollViewer sv)
{
return sv;
}
var count = VisualTreeHelper.GetChildrenCount(root);
for (var i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
var found = FindScrollViewer(child);
if (found is not null)
{
return found;
}
}
return null;
}
private static T? FindAncestor<T>(DependencyObject node) where T : DependencyObject
{
var current = node;
while (current is not null)
{
if (current is T t)
{
return t;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
}