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