From a3f0dcb4553f12b4b1ef5719c3b8730355c886d4 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 25 Jun 2026 17:06:18 +0200 Subject: [PATCH 1/2] Fix InvalidOperationException when resizing the filter splitter on a narrow window Double-clicking or dragging the filter row's splitter while the LogExpert window is narrower than ~730px threw: InvalidOperationException: SplitterDistance must be between Panel1MinSize and Width - Panel2MinSize at SplitContainer.set_SplitterDistance at LogWindow.AutoResizeFilterBox The #560 fix set filterSplitContainer.Panel2MinSize = 726. Under Dock=Fill the parent layout engine forces the container below Panel1MinSize + SplitterWidth + Panel2MinSize, so no splitter distance can honour both minimums. The setter throws (it does not clamp) when handed a value that can't fit, and the old ClampSplitterDistance returned Panel1MinSize anyway via Math.Max. Replace ClampSplitterDistance with TryClampSplitterDistance, which returns false when both minimums can't fit. AutoResizeFilterBox and OnFilterSplitContainerMouseMove now skip the assignment in that degenerate state, leaving the splitter put. Tests ported to the Try form plus a regression case pinning the crash geometry (Width 684, Panel2MinSize 726). --- .vscode/launch.json | 26 ++++++++ .vscode/tasks.json | 41 +++++++++++++ .../Controls/FilterSplitterLayoutTests.cs | 61 +++++++++++++------ .../LogWindow/FilterSplitterLayout.cs | 27 +++++--- .../Controls/LogWindow/LogWindow.cs | 16 ++--- 5 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..53ee15f3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/LogExpert.Tests/bin/Debug/LogExpert.Tests.dll", + "args": [], + "cwd": "${workspaceFolder}/src/LogExpert.Tests", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..47a33bc2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/LogExpert.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/LogExpert.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/LogExpert.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs b/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs index 765c8fc6..85d899bd 100644 --- a/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs +++ b/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs @@ -13,57 +13,84 @@ namespace LogExpert.Tests.Controls; public class FilterSplitterLayoutTests { [Test] - public void ClampSplitterDistance_DesiredWithinBounds_ReturnedUnchanged () + public void TryClampSplitterDistance_DesiredWithinBounds_ReturnedUnchanged () { - var result = FilterSplitterLayout.ClampSplitterDistance( + var ok = FilterSplitterLayout.TryClampSplitterDistance( desiredDistance: 600, containerWidth: 1855, splitterWidth: 4, panel1MinSize: 200, - panel2MinSize: 660); + panel2MinSize: 660, + out var distance); - Assert.That(result, Is.EqualTo(600)); + Assert.That(ok, Is.True); + Assert.That(distance, Is.EqualTo(600)); } [Test] - public void ClampSplitterDistance_DesiredTooLarge_ClampedSoPanel2KeepsMinWidth () + public void TryClampSplitterDistance_DesiredTooLarge_ClampedSoPanel2KeepsMinWidth () { // 1855 - 4 (splitter) - 660 (panel2 min) = 1191 is the furthest the splitter may go. - var result = FilterSplitterLayout.ClampSplitterDistance( + var ok = FilterSplitterLayout.TryClampSplitterDistance( desiredDistance: 1800, containerWidth: 1855, splitterWidth: 4, panel1MinSize: 200, - panel2MinSize: 660); + panel2MinSize: 660, + out var distance); - Assert.That(result, Is.EqualTo(1191)); + Assert.That(ok, Is.True); + Assert.That(distance, Is.EqualTo(1191)); } [Test] - public void ClampSplitterDistance_DesiredTooSmall_ClampedUpToPanel1MinWidth () + public void TryClampSplitterDistance_DesiredTooSmall_ClampedUpToPanel1MinWidth () { - var result = FilterSplitterLayout.ClampSplitterDistance( + var ok = FilterSplitterLayout.TryClampSplitterDistance( desiredDistance: 50, containerWidth: 1855, splitterWidth: 4, panel1MinSize: 200, - panel2MinSize: 660); + panel2MinSize: 660, + out var distance); - Assert.That(result, Is.EqualTo(200)); + Assert.That(ok, Is.True); + Assert.That(distance, Is.EqualTo(200)); } [Test] - public void ClampSplitterDistance_ContainerTooSmallForBothMinimums_DoesNotThrowAndKeepsPanel1Min () + public void TryClampSplitterDistance_ContainerTooSmallForBothMinimums_ReturnsFalse () { - // 700 - 4 - 660 = 36, which is below panel1MinSize. Must not throw (min > max for Math.Clamp). - var result = FilterSplitterLayout.ClampSplitterDistance( + // 700 - 4 - 660 = 36, which is below panel1MinSize: no distance honours both minimums, + // so the caller must skip assignment. Returning a value here is what previously made the + // SplitContainer.SplitterDistance setter throw on a narrow window. + var ok = FilterSplitterLayout.TryClampSplitterDistance( desiredDistance: 500, containerWidth: 700, splitterWidth: 4, panel1MinSize: 200, - panel2MinSize: 660); + panel2MinSize: 660, + out _); - Assert.That(result, Is.EqualTo(200)); + Assert.That(ok, Is.False); + } + + [Test] + public void TryClampSplitterDistance_NarrowWindowRegression_ReturnsFalseInsteadOfThrowingValue () + { + // Regression for the #560 follow-up crash: with the real filter-row minimums (Panel2 = 726), + // shrinking the LogExpert window forces the Dock=Fill container below ~730px. The old + // ClampSplitterDistance returned Panel1MinSize (200), and assigning it threw + // InvalidOperationException ("SplitterDistance must be between Panel1MinSize and Width - Panel2MinSize"). + var ok = FilterSplitterLayout.TryClampSplitterDistance( + desiredDistance: 5000, + containerWidth: 684, + splitterWidth: 4, + panel1MinSize: 200, + panel2MinSize: 726, + out _); + + Assert.That(ok, Is.False); } [Test] diff --git a/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs b/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs index ae32259c..639aee85 100644 --- a/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs +++ b/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs @@ -16,14 +16,27 @@ internal static class FilterSplitterLayout /// Width of the splitter bar. /// Minimum width of Panel1 (the text filter). /// Minimum width of Panel2 (the buttons/checkboxes). - /// A distance guaranteed to keep Panel2 at least wide. - public static int ClampSplitterDistance (int desiredDistance, int containerWidth, int splitterWidth, int panel1MinSize, int panel2MinSize) + /// The clamped distance to apply; only meaningful when this method returns true. + /// + /// true when a distance honouring both minimums exists and was set; + /// false when the container is too narrow to honour both minimums at once. In that degenerate + /// state there is no valid distance, and the caller must NOT assign one: WinForms' + /// SplitContainer.SplitterDistance setter throws when the + /// value cannot fit between Panel1MinSize and Width - Panel2MinSize - SplitterWidth. + /// + public static bool TryClampSplitterDistance (int desiredDistance, int containerWidth, int splitterWidth, int panel1MinSize, int panel2MinSize, out int distance) { - // When the container is too small to honour both minimums there is no perfect answer; - // keep Panel1 at its minimum (matching the SplitContainer's own preference) and avoid - // an invalid (min > max) clamp range. - var maxDistance = Math.Max(containerWidth - splitterWidth - panel2MinSize, panel1MinSize); - return Math.Clamp(desiredDistance, panel1MinSize, maxDistance); + var maxDistance = containerWidth - splitterWidth - panel2MinSize; + if (maxDistance < panel1MinSize) + { + // Both minimums cannot fit (a narrow window forced the container below their sum). + // No splitter distance is valid here; the caller leaves the splitter where it is. + distance = panel1MinSize; + return false; + } + + distance = Math.Clamp(desiredDistance, panel1MinSize, maxDistance); + return true; } /// diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index 4ba8dd4a..76d42c08 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -744,8 +744,10 @@ void ILogWindow.WritePipeTab (IList lineEntryList, string title private void AutoResizeFilterBox () { var desired = filterComboBox.Left + filterComboBox.GetMaxTextWidth(); - filterSplitContainer.SplitterDistance = FilterSplitterLayout.ClampSplitterDistance( - desired, filterSplitContainer.Width, filterSplitContainer.SplitterWidth, filterSplitContainer.Panel1MinSize, filterSplitContainer.Panel2MinSize); + if (FilterSplitterLayout.TryClampSplitterDistance(desired, filterSplitContainer.Width, filterSplitContainer.SplitterWidth, filterSplitContainer.Panel1MinSize, filterSplitContainer.Panel2MinSize, out var distance)) + { + filterSplitContainer.SplitterDistance = distance; + } } #region Events handler @@ -1449,11 +1451,11 @@ private void OnFilterSplitContainerMouseMove (object sender, MouseEventArgs e) var desired = isVertical ? e.X : e.Y; var containerSize = isVertical ? splitContainer.Width : splitContainer.Height; - // Keep the splitter inside the panels' min sizes so the text filter (Panel1) can - // never be grown large enough to push the Panel2 controls outside the app (issue #560). - splitContainer.SplitterDistance = FilterSplitterLayout.ClampSplitterDistance( - desired, containerSize, splitContainer.SplitterWidth, splitContainer.Panel1MinSize, splitContainer.Panel2MinSize); - splitContainer.Refresh(); + if (FilterSplitterLayout.TryClampSplitterDistance(desired, containerSize, splitContainer.SplitterWidth, splitContainer.Panel1MinSize, splitContainer.Panel2MinSize, out var distance)) + { + splitContainer.SplitterDistance = distance; + splitContainer.Refresh(); + } } else { From 87312d7e81125f13041a0474728e367eff4ea000 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 25 Jun 2026 15:09:23 +0000 Subject: [PATCH 2/2] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index c47db8ff..2640e4fd 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-06-25 14:06:29 UTC + /// Generated: 2026-06-25 15:09:22 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "E6BBCB994A140BC595E67D19A7E6E9975469718628C5BA8E423DCF13E076FA81", + ["AutoColumnizer.dll"] = "ED598729A6BCB3175F02FF54576DDD5DAD7CBBC0E47A1E07ADC8A9B4AB330675", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "024575D2CBD09E58AE7CFF058B92C3785006AD2280301B0DE0724409934F4BCA", - ["CsvColumnizer.dll (x86)"] = "024575D2CBD09E58AE7CFF058B92C3785006AD2280301B0DE0724409934F4BCA", - ["DefaultPlugins.dll"] = "19B0A154B48F99A616A8468E8AC45ACCF86B4402F9F4E7DC860C6FE1D7D92B13", - ["FlashIconHighlighter.dll"] = "4A488895316B01053DD3287604DC4344CCEA918EB09EB664009BC419F29FC021", - ["GlassfishColumnizer.dll"] = "93DF4EEDC1A695F06E7E09AB483782FA0FDC867BED61FB63BD25458389FA8E0E", - ["JsonColumnizer.dll"] = "C9538CA60C8E1ADFEAC74C75CAE7F1627311FF500804C6FD8B2EFEF068C6FFFE", - ["JsonCompactColumnizer.dll"] = "1F30ABDC119D4459A2272D96080F4BF0683D7420A624CC256C2F07EEE02B192A", - ["Log4jXmlColumnizer.dll"] = "D99A998A0E0C3969EB8B49CB6E8BD9CCC934641EEE18852B202244C837ADC7F5", - ["LogExpert.Resources.dll"] = "43FB2C2C6D53D1E2CDB0D8FD663AEE5CF3E4FC51A05D69DBFBBF2123E8F67935", + ["CsvColumnizer.dll"] = "D2200DE16E125F7B3FD4A0603F094455712987773C98AE71B46EBEB1F181D5FC", + ["CsvColumnizer.dll (x86)"] = "D2200DE16E125F7B3FD4A0603F094455712987773C98AE71B46EBEB1F181D5FC", + ["DefaultPlugins.dll"] = "9E7CD9B83067F83273D4D0BDD0E50DEBD3073015B55F63E7683AAF768FFB913F", + ["FlashIconHighlighter.dll"] = "0E9DB7648FF41A3AAF7924A2A8A01476B803A0471711B9A3FE1D3C927EC02199", + ["GlassfishColumnizer.dll"] = "F65B6305AF29B1F883C2523140C230CBB900046022572618A41C3C128703FF3D", + ["JsonColumnizer.dll"] = "514EDAC4465FF75D604989F20B0B12ADBFD2092E514B4E80FE8CBF2272CF7C9A", + ["JsonCompactColumnizer.dll"] = "04B454BDBA03DD8683F9065AF8080E7ED5A443A63611D738AC3B2B11A912E537", + ["Log4jXmlColumnizer.dll"] = "7CEB38D227528C08C21D403F7CA91DFE689655022EFB7280761167C7F7C2AFBC", + ["LogExpert.Resources.dll"] = "A4AC70A6FCB3997821127C3F59BF78933794CF0B07F26DFFCD9004FD506F03C5", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "926229B7C704E367BA768E44981D133E6B26356CCEC4BCF0108EC77B2521C33A", - ["SftpFileSystem.dll"] = "066BDA9245EDCECCD49E3C36B6ACC05E52C8C34D25A1A9406E403987E603FC57", - ["SftpFileSystem.dll (x86)"] = "A30EB5C2D2E8A403C0ADF179AB626278689198E64935DA8BA108B33FB164AECA", - ["SftpFileSystem.Resources.dll"] = "E68455B141D714A19779E717558DEEA6C9A9829ECF5A3C5EDCD65F18C06E6D33", - ["SftpFileSystem.Resources.dll (x86)"] = "E68455B141D714A19779E717558DEEA6C9A9829ECF5A3C5EDCD65F18C06E6D33", + ["RegexColumnizer.dll"] = "7D343457626A42062C885716047E6DC6649983E2AD612B7C38D73F695250355A", + ["SftpFileSystem.dll"] = "3011E98941B793FBBC891AA38939058CC057F3CBDC0AD1E410A7B1715821FF3B", + ["SftpFileSystem.dll (x86)"] = "5D79B06F7109C61C5A5A9049DB355627F4E4EB85E557DB7786C0412954A85B72", + ["SftpFileSystem.Resources.dll"] = "0CE8807D06B4027FA3358773581C0B1DBC646BB2B607F4F358853AC8640C4976", + ["SftpFileSystem.Resources.dll (x86)"] = "0CE8807D06B4027FA3358773581C0B1DBC646BB2B607F4F358853AC8640C4976", }; }