From 9da0b781a5c5b8f6743ada15a6a200a2a605abae Mon Sep 17 00:00:00 2001 From: liuxiaoqing Date: Wed, 1 Jul 2026 14:35:44 +0800 Subject: [PATCH 1/5] fix(AvalonDock): Fix multiple interaction & style issues of navigator and floating windows, optimize content handling for floating windows Fixes 1. Fix activation logic of selected items in navigator window 2. Fix layout and foreground color of selectedElementDescription under VS2013 theme for NavigatorWindow 3. Fix floating window size calculation logic 4. Fix potential null reference exceptions when traversing parent visual elements Improvements 1. Refine overall content handling logic of floating layout window control 2. Add support for content elements implementing the `ILayoutContentElement` interface to improve compatibility with various content types 3. Adjust icon size of dock context menu --- .../Themes/Generic.xaml | 2841 +++++++++-------- .../Controls/LayoutFloatingWindowControl.cs | 52 +- .../AvalonDock/Controls/NavigatorWindow.cs | 40 +- .../Layout/ILayoutContentElement.cs | 15 + 4 files changed, 1502 insertions(+), 1446 deletions(-) create mode 100644 source/Components/AvalonDock/Layout/ILayoutContentElement.cs diff --git a/source/Components/AvalonDock.Themes.VS2013/Themes/Generic.xaml b/source/Components/AvalonDock.Themes.VS2013/Themes/Generic.xaml index 3adf0996..709b8888 100644 --- a/source/Components/AvalonDock.Themes.VS2013/Themes/Generic.xaml +++ b/source/Components/AvalonDock.Themes.VS2013/Themes/Generic.xaml @@ -7,15 +7,15 @@ xmlns:avalonDockProperties="clr-namespace:AvalonDock.Properties;assembly=AvalonDock" xmlns:reskeys="clr-namespace:AvalonDock.Themes.VS2013.Themes" xmlns:shell="clr-namespace:Microsoft.Windows.Shell;assembly=AvalonDock"> - - - - - + + + + + - --> - - - - + + + - - + + + - + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + - - - - + + + + - + @@ -831,9 +831,9 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - + + + - - + - + - - + - + - - + - - - - - - - - - - - - + + + + + + + + + + + - + - - + - - - - - + + + + - - + - - + + - - - + - - - + + - - + - - - - - + + + + + - + + + + + + + + - + + + + - + + + + + + + - + + + + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - + + + + - + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs index ebc5e0e0..ce4eb695 100644 --- a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs +++ b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs @@ -470,8 +470,21 @@ private void UpdateMargins() return; // The content control in the grid, this has a different tree to walk up var layoutContent = (LayoutContent)contentControl.Content; - if (grid != null && layoutContent.Content is FrameworkElement content) + if (grid != null) { + FrameworkElement content = null; + var contentObj = layoutContent.Content; + + if (contentObj is ILayoutContentElement layoutContentElement) + { + content = layoutContentElement.Content; + } + else if (contentObj is FrameworkElement frameworkElement) + { + content = frameworkElement; + } + + if (content == null) return; var parents = content.GetParents().ToArray(); var children = this.GetChildrenRecursive() .TakeWhile(c => c != grid) @@ -519,7 +532,10 @@ private void UpdateWindowsSizeBasedOnMinSize() .Select(c => c.Content) .OfType() .Select(lc => lc.Content); - var contents = layoutContents.OfType(); + var contents = layoutContents.Select(obj => obj is ILayoutContentElement elem + ? elem.Content + : obj as FrameworkElement) + .Where(fe => fe != null); foreach (var content in contents) { ContentMinHeight = Math.Max(content.MinHeight, ContentMinHeight); @@ -541,6 +557,16 @@ private void UpdateWindowsSizeBasedOnMinSize() { Width = content.MinWidth + TotalMargin.Left + TotalMargin.Right; } + + if (Height > content.ActualHeight) + { + Height = content.ActualHeight + TotalMargin.Top + TotalMargin.Bottom; + } + + if (Width > content.ActualWidth) + { + Width = content.ActualWidth + TotalMargin.Left + TotalMargin.Right; + } } } } @@ -730,9 +756,9 @@ private void InternalOnActivated(object sender, EventArgs e, int retryCount = 0) { return; } - + var windowHandle = new WindowInteropHelper(this).Handle; - + // Check if the visual is connected to a PresentationSource to avoid InvalidOperationException // in multi-DPI scenarios where the window might not be fully initialized yet if (PresentationSource.FromVisual(this) == null) @@ -743,21 +769,21 @@ private void InternalOnActivated(object sender, EventArgs e, int retryCount = 0) _attachDrag = false; return; } - + // If not connected, defer the operation until the visual is properly initialized Dispatcher.Invoke( async () => { - if (_attachDrag && Mouse.LeftButton == MouseButtonState.Pressed) - { - await Task.Delay(10); - retryCount++; - InternalOnActivated(sender, e, retryCount); - } - }, System.Windows.Threading.DispatcherPriority.Loaded); + if (_attachDrag && Mouse.LeftButton == MouseButtonState.Pressed) + { + await Task.Delay(10); + retryCount++; + InternalOnActivated(sender, e, retryCount); + } + }, System.Windows.Threading.DispatcherPriority.Loaded); return; } - + var mousePosition = this.PointToScreenDPI(Mouse.GetPosition(this)); var area = this.GetScreenArea(); diff --git a/source/Components/AvalonDock/Controls/NavigatorWindow.cs b/source/Components/AvalonDock/Controls/NavigatorWindow.cs index ce9df537..8b66a97f 100644 --- a/source/Components/AvalonDock/Controls/NavigatorWindow.cs +++ b/source/Components/AvalonDock/Controls/NavigatorWindow.cs @@ -186,18 +186,11 @@ public LayoutDocumentItem SelectedDocument /// The event arguments. protected virtual void OnSelectedDocumentChanged(DependencyPropertyChangedEventArgs e) { - if (_internalSetSelectedDocument || SelectedDocument == null) + if (!_internalSetSelectedDocument && SelectedDocument != null && SelectedDocument.ActivateCommand.CanExecute(null)) { - return; - } - - if (!SelectedDocument.ActivateCommand.CanExecute(null)) - { - return; + SelectedAnchorable = null; + CloseAndActiveSelected(); } - - Close(); - SelectedDocument.ActivateCommand.Execute(null); } /// @@ -227,13 +220,10 @@ public LayoutAnchorableItem SelectedAnchorable /// The event arguments. protected virtual void OnSelectedAnchorableChanged(DependencyPropertyChangedEventArgs e) { - if (_internalSetSelectedAnchorable) return; - // TODO: What goes on here?? - var selectedAnchorable = e.NewValue as LayoutAnchorableItem; - if (SelectedAnchorable != null && SelectedAnchorable.ActivateCommand.CanExecute(null)) + if (!_internalSetSelectedAnchorable && SelectedAnchorable != null && SelectedAnchorable.ActivateCommand.CanExecute(null)) { - Close(); - SelectedAnchorable.ActivateCommand.Execute(null); + SelectedDocument = null; + CloseAndActiveSelected(); } } @@ -271,10 +261,12 @@ private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) if (isListOfDocuments) { container.IsKeyboardFocusedChanged += DocumentsItemContainer_IsKeyboardFocusedChanged; + container.PreviewMouseLeftButtonDown += DocumentsItemContainer_PreviewMouseLeftButtonDown; } else { container.IsKeyboardFocusedChanged += AnchorablesItemContainer_IsKeyboardFocusedChanged; + container.PreviewMouseLeftButtonDown += AnchorablesItemContainer_PreviewMouseLeftButtonDown; } } } @@ -283,6 +275,14 @@ private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) } } + private void AnchorablesItemContainer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + ListBoxItem item = (ListBoxItem)sender; + _internalSetSelectedAnchorable = false; + item.IsSelected = true; + _internalSetSelectedAnchorable = false; + } + private void AnchorablesItemContainer_IsKeyboardFocusedChanged(object sender, DependencyPropertyChangedEventArgs e) { ListBoxItem item = (ListBoxItem)sender; @@ -294,6 +294,14 @@ private void AnchorablesItemContainer_IsKeyboardFocusedChanged(object sender, De } } + private void DocumentsItemContainer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + ListBoxItem item = (ListBoxItem)sender; + _internalSetSelectedDocument = false; + item.IsSelected = true; + _internalSetSelectedDocument = false; + } + private void DocumentsItemContainer_IsKeyboardFocusedChanged(object sender, DependencyPropertyChangedEventArgs e) { ListBoxItem item = (ListBoxItem)sender; diff --git a/source/Components/AvalonDock/Layout/ILayoutContentElement.cs b/source/Components/AvalonDock/Layout/ILayoutContentElement.cs new file mode 100644 index 00000000..5279c923 --- /dev/null +++ b/source/Components/AvalonDock/Layout/ILayoutContentElement.cs @@ -0,0 +1,15 @@ +using System.Windows; + +namespace AvalonDock.Layout +{ + /// + /// Represents a layout element that holds a WPF UI content element + /// + public interface ILayoutContentElement + { + /// + /// Gets the root WPF FrameworkElement hosted by this layout element + /// + FrameworkElement Content { get; } + } +} \ No newline at end of file From fb038c24db2b91852b4b3d103096d03c77024983 Mon Sep 17 00:00:00 2001 From: liuxiaoqing Date: Thu, 2 Jul 2026 15:12:11 +0800 Subject: [PATCH 2/5] fix(AvalonDock): Fix floating window size calculation logic Revised the size calculation logic within LayoutFloatingWindowControl when AutoWindowSizeWhenOpened is enabled. The expected size retrieved via the Measure method is used instead of relying on actual rendered dimensions, to more accurately resolve the intrinsic content size. --- .../Controls/LayoutFloatingWindowControl.cs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs index ce4eb695..635ee73d 100644 --- a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs +++ b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs @@ -542,30 +542,20 @@ private void UpdateWindowsSizeBasedOnMinSize() ContentMinWidth = Math.Max(content.MinWidth, ContentMinWidth); if ((this.Model?.Root?.Manager?.AutoWindowSizeWhenOpened).GetValueOrDefault()) { - var parent = content.GetParents() - .OfType() - .FirstOrDefault(); - // StackPanels among others have an ActualHeight larger than visible, hence we check the parent control as well - if (content.ActualHeight < content.MinHeight || - parent != null && parent.ActualHeight < content.MinHeight) - { - Height = content.MinHeight + TotalMargin.Top + TotalMargin.Bottom; - } - - if (content.ActualWidth < content.MinWidth || - parent != null && parent.ActualWidth < content.MinWidth) - { - Width = content.MinWidth + TotalMargin.Left + TotalMargin.Right; - } + content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + var desiredWidth = content.DesiredSize.Width; + var desiredHeight = content.DesiredSize.Height; - if (Height > content.ActualHeight) + if (desiredHeight > 0) { - Height = content.ActualHeight + TotalMargin.Top + TotalMargin.Bottom; + var contentHeight = Math.Max(desiredHeight, content.MinHeight); + Height = contentHeight + TotalMargin.Top + TotalMargin.Bottom; } - if (Width > content.ActualWidth) + if (desiredWidth > 0) { - Width = content.ActualWidth + TotalMargin.Left + TotalMargin.Right; + var contentWidth = Math.Max(desiredWidth, content.MinWidth); + Width = contentWidth + TotalMargin.Left + TotalMargin.Right; } } } From 61da10e938adabba597ec886cdfcaa8745ec6d7a Mon Sep 17 00:00:00 2001 From: liuxiaoqing Date: Fri, 3 Jul 2026 16:19:21 +0800 Subject: [PATCH 3/5] fix(Utilities): suppress security warning for MD5 hash calculation --- .../AvalonDock/Controls/Shell/Standard/Utilities.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/Components/AvalonDock/Controls/Shell/Standard/Utilities.cs b/source/Components/AvalonDock/Controls/Shell/Standard/Utilities.cs index 8e601546..8745b671 100644 --- a/source/Components/AvalonDock/Controls/Shell/Standard/Utilities.cs +++ b/source/Components/AvalonDock/Controls/Shell/Standard/Utilities.cs @@ -1,4 +1,4 @@ -/**************************************************************************\ +/**************************************************************************\ Copyright Microsoft Corporation. All Rights Reserved. \**************************************************************************/ @@ -571,11 +571,13 @@ public static string HashStreamMD5(Stream stm) { stm.Position = 0; var hashBuilder = new StringBuilder(); +#pragma warning disable S4790 using (var md5 = MD5.Create()) { foreach (var b in md5.ComputeHash(stm)) hashBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); return hashBuilder.ToString(); } +#pragma warning restore S4790 } /// Performs the EnsureDirectory operation. From d529ae3644a2a9df944dc7dad483f21f6e4f8fd6 Mon Sep 17 00:00:00 2001 From: liuxiaoqing Date: Tue, 7 Jul 2026 16:40:23 +0800 Subject: [PATCH 4/5] chore(AvalonDock): Update localized translations for resource files - Fix extra spaces in Spanish translations within Resources.es.resx - Add missing entries to the Portuguese translation file Resources.pt.resx --- source/Components/AvalonDock/Properties/Resources.es.resx | 4 ++-- source/Components/AvalonDock/Properties/Resources.pt.resx | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/source/Components/AvalonDock/Properties/Resources.es.resx b/source/Components/AvalonDock/Properties/Resources.es.resx index 989c58d3..79c364d7 100644 --- a/source/Components/AvalonDock/Properties/Resources.es.resx +++ b/source/Components/AvalonDock/Properties/Resources.es.resx @@ -133,7 +133,7 @@ Esconder - Posición de la ventana + Posición de la ventana Anclar @@ -172,7 +172,7 @@ Mover al siguiente grupo de fichas - Mover al anterior grupo de fichas + Mover al anterior grupo de fichas Nuevo grupo de fichas horizontal diff --git a/source/Components/AvalonDock/Properties/Resources.pt.resx b/source/Components/AvalonDock/Properties/Resources.pt.resx index f057ca0c..5771c37d 100644 --- a/source/Components/AvalonDock/Properties/Resources.pt.resx +++ b/source/Components/AvalonDock/Properties/Resources.pt.resx @@ -183,4 +183,7 @@ Recuperar - + + Alternar estado fixo + + \ No newline at end of file From 77d57dd94d00552ff8d69e7c56541df977ea2452 Mon Sep 17 00:00:00 2001 From: liuxiaoqing Date: Mon, 17 Aug 2026 10:53:08 +0800 Subject: [PATCH 5/5] fix: suppress S6444 for timeout-less Regex.Replace SonarAnalyzer.CSharp 10.32 introduced rule S6444, which requires regular expression calls to pass a matchTimeout to prevent ReDoS attacks. The Regex.Replace call in ToggleDockingManager.cs does not provide a timeout, so it triggers the rule; combined with TreatWarningsAsErrors=true in Directory.Build.props, the build fails. The pattern "(\\B[A-Z])" is a fixed, simple regex applied to internal enum names (e.g. DockZone -> "Dock Zone"). The input is not user-controlled, so there is no ReDoS risk. Suppressing the rule locally with #pragma warning disable S6444 keeps the other rules enabled and does not affect behavior. --- source/Components/AvalonDock/ToggleDockingManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/Components/AvalonDock/ToggleDockingManager.cs b/source/Components/AvalonDock/ToggleDockingManager.cs index f35458cb..8f85d701 100644 --- a/source/Components/AvalonDock/ToggleDockingManager.cs +++ b/source/Components/AvalonDock/ToggleDockingManager.cs @@ -632,10 +632,12 @@ internal ContextMenu BuildToggleContextMenu(LayoutAnchorable anchorable) var moveToItem = new MenuItem { Header = "Move To" }; foreach (DockZone zone in Enum.GetValues(typeof(DockZone))) { +#pragma warning disable S6444 var zoneLabel = System.Text.RegularExpressions.Regex.Replace(zone.ToString(), "(\\B[A-Z])", " $1"); var z = zone; var mi = new MenuItem { Header = zoneLabel }; - mi.Click += (s, e) => MoveAnchorableToZone(anchorable, z); + mi.Click += (s, e) => MoveAnchorableToZone(anchorable, zone); +#pragma warning restore S6444 moveToItem.Items.Add(mi); }