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 { 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", }; }