diff --git a/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs b/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs
new file mode 100644
index 00000000..765c8fc6
--- /dev/null
+++ b/src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs
@@ -0,0 +1,82 @@
+using LogExpert.UI.Controls.LogWindow;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.Controls;
+
+///
+/// Tests for the splitter-distance clamping used by the filter row's split container
+/// (see issue #560). The text filter (Panel1) must not be growable to a size that pushes
+/// the Panel2 controls ("Search", checkboxes, "Show advanced...") outside the app.
+///
+[TestFixture]
+public class FilterSplitterLayoutTests
+{
+ [Test]
+ public void ClampSplitterDistance_DesiredWithinBounds_ReturnedUnchanged ()
+ {
+ var result = FilterSplitterLayout.ClampSplitterDistance(
+ desiredDistance: 600,
+ containerWidth: 1855,
+ splitterWidth: 4,
+ panel1MinSize: 200,
+ panel2MinSize: 660);
+
+ Assert.That(result, Is.EqualTo(600));
+ }
+
+ [Test]
+ public void ClampSplitterDistance_DesiredTooLarge_ClampedSoPanel2KeepsMinWidth ()
+ {
+ // 1855 - 4 (splitter) - 660 (panel2 min) = 1191 is the furthest the splitter may go.
+ var result = FilterSplitterLayout.ClampSplitterDistance(
+ desiredDistance: 1800,
+ containerWidth: 1855,
+ splitterWidth: 4,
+ panel1MinSize: 200,
+ panel2MinSize: 660);
+
+ Assert.That(result, Is.EqualTo(1191));
+ }
+
+ [Test]
+ public void ClampSplitterDistance_DesiredTooSmall_ClampedUpToPanel1MinWidth ()
+ {
+ var result = FilterSplitterLayout.ClampSplitterDistance(
+ desiredDistance: 50,
+ containerWidth: 1855,
+ splitterWidth: 4,
+ panel1MinSize: 200,
+ panel2MinSize: 660);
+
+ Assert.That(result, Is.EqualTo(200));
+ }
+
+ [Test]
+ public void ClampSplitterDistance_ContainerTooSmallForBothMinimums_DoesNotThrowAndKeepsPanel1Min ()
+ {
+ // 700 - 4 - 660 = 36, which is below panel1MinSize. Must not throw (min > max for Math.Clamp).
+ var result = FilterSplitterLayout.ClampSplitterDistance(
+ desiredDistance: 500,
+ containerWidth: 700,
+ splitterWidth: 4,
+ panel1MinSize: 200,
+ panel2MinSize: 660);
+
+ Assert.That(result, Is.EqualTo(200));
+ }
+
+ [Test]
+ public void RequiredPanel2Width_LeavesRoomForRightAnchoredControlAfterRightmostControl ()
+ {
+ // "Show advanced..." ends at x=649; the right-anchored filter-count label is 71 wide.
+ // Panel2 must be at least wide enough that the label starts after the button (+ gap),
+ // otherwise the two overlap as the text filter is grown (issue #560 follow-up).
+ var result = FilterSplitterLayout.RequiredPanel2Width(
+ rightmostControlRightEdge: 649,
+ rightAnchoredControlWidth: 71,
+ gap: 6);
+
+ Assert.That(result, Is.EqualTo(726));
+ }
+}
diff --git a/src/LogExpert.UI/Controls/BufferedSplitContainer.cs b/src/LogExpert.UI/Controls/BufferedSplitContainer.cs
new file mode 100644
index 00000000..f9f5bd75
--- /dev/null
+++ b/src/LogExpert.UI/Controls/BufferedSplitContainer.cs
@@ -0,0 +1,39 @@
+using System.Runtime.Versioning;
+
+namespace LogExpert.UI.Controls;
+
+///
+/// A that paints itself and all of its descendants composited (off-screen, bottom-to-top)
+/// via the WS_EX_COMPOSITED extended window style.
+///
+/// This removes the flicker that occurs while the splitter is dragged: anchored child controls (e.g. the
+/// right-aligned filter-count label) are physically repositioned on every mouse move, and because they own their own
+/// window handle, double-buffering the parent panel is not enough to stop them flickering. Compositing buffers the
+/// entire control tree, so the drag is smooth (see issue #560).
+///
+/// https://pinvoke.net/default.aspx/Enums/WindowStyles.html
+[SupportedOSPlatform("windows")]
+internal sealed class BufferedSplitContainer : SplitContainer
+{
+ ///
+ /// Specifies a window that paints all descendants in bottom-to-top painting order using double-buffering.
+ /// This cannot be used if the window has a class style of either CS_OWNDC or CS_CLASSDC. This style is not supported in Windows 2000.
+ ///
+ ///
+ /// With WS_EX_COMPOSITED set, all descendants of a window get bottom-to-top painting order using double-buffering.
+ /// Bottom-to-top painting order allows a descendent window to have translucency (alpha) and transparency (color-key) effects,
+ /// but only if the descendent window also has the WS_EX_TRANSPARENT bit set.
+ /// Double-buffering allows the window and its descendents to be painted without flicker.
+ ///
+ private const int WS_EX_COMPOSITED = 0x02000000;
+
+ protected override CreateParams CreateParams
+ {
+ get
+ {
+ var createParams = base.CreateParams;
+ createParams.ExStyle |= WS_EX_COMPOSITED;
+ return createParams;
+ }
+ }
+}
diff --git a/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs b/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs
new file mode 100644
index 00000000..ae32259c
--- /dev/null
+++ b/src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs
@@ -0,0 +1,41 @@
+namespace LogExpert.UI.Controls.LogWindow;
+
+///
+/// Pure layout math for the filter row's split container.
+/// Keeps the text filter (Panel1) from being grown so large that the Panel2 controls
+/// ("Search", the filter checkboxes and the "Show advanced..." button) are pushed
+/// outside the visible area of the application.
+///
+internal static class FilterSplitterLayout
+{
+ ///
+ /// Clamps a desired splitter distance so that neither panel shrinks below its minimum size.
+ ///
+ /// The requested distance (Panel1 width) in pixels.
+ /// Total width of the split container.
+ /// 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)
+ {
+ // 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);
+ }
+
+ ///
+ /// Computes the minimum width Panel2 needs so that the rightmost left-anchored control
+ /// (the "Show advanced..." button) never overlaps the right-anchored control next to it
+ /// (the filter-count label) as the text filter is grown.
+ ///
+ /// Right edge (Left + Width) of the rightmost left-anchored control.
+ /// Width of the right-anchored control.
+ /// Desired gap in pixels between the two controls.
+ public static int RequiredPanel2Width (int rightmostControlRightEdge, int rightAnchoredControlWidth, int gap)
+ {
+ return rightmostControlRightEdge + gap + rightAnchoredControlWidth;
+ }
+}
diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
index 1bf73154..1405f6cf 100644
--- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
+++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
@@ -46,6 +46,7 @@ internal partial class LogWindow : DockContent, ILogPaintContextUI, ILogView, IL
private const int SPREAD_MAX = 99;
private const int PROGRESS_BAR_MODULO = 1000;
private const int FILTER_ADVANCED_SPLITTER_DISTANCE = 110;
+ private const int FILTER_PANEL2_CONTROL_GAP = 6;
private const int WAIT_TIME = 500;
private const int OVERSCAN = 20;
private const string FONT_COURIER_NEW = "Courier New";
@@ -228,6 +229,10 @@ public LogWindow (ILogWindowCoordinator logWindowCoordinator, string fileName, b
}
filterComboBox.DropDownHeight = filterComboBox.ItemHeight * configManager.Settings.Preferences.MaximumFilterEntriesDisplayed;
+
+ // Keep Panel2 wide enough that "Show advanced..." (btnAdvanced, the rightmost left-anchored
+ // control) never overlaps the right-anchored filter-count label when the text filter grows.
+ filterSplitContainer.Panel2MinSize = FilterSplitterLayout.RequiredPanel2Width(btnAdvanced.Right, lblFilterCount.Width, FILTER_PANEL2_CONTROL_GAP);
AutoResizeFilterBox();
filterRegexCheckBox.Checked = _filterParams.IsRegex;
@@ -736,7 +741,9 @@ void ILogWindow.WritePipeTab (IList lineEntryList, string title
[SupportedOSPlatform("windows")]
private void AutoResizeFilterBox ()
{
- filterSplitContainer.SplitterDistance = filterComboBox.Left + filterComboBox.GetMaxTextWidth();
+ var desired = filterComboBox.Left + filterComboBox.GetMaxTextWidth();
+ filterSplitContainer.SplitterDistance = FilterSplitterLayout.ClampSplitterDistance(
+ desired, filterSplitContainer.Width, filterSplitContainer.SplitterWidth, filterSplitContainer.Panel1MinSize, filterSplitContainer.Panel2MinSize);
}
#region Events handler
@@ -1430,22 +1437,15 @@ private void OnFilterSplitContainerMouseMove (object sender, MouseEventArgs e)
{
if (e.Button.Equals(MouseButtons.Left))
{
- if (splitContainer.Orientation.Equals(Orientation.Vertical))
- {
- if (e.X > 0 && e.X < splitContainer.Width)
- {
- splitContainer.SplitterDistance = e.X;
- splitContainer.Refresh();
- }
- }
- else
- {
- if (e.Y > 0 && e.Y < splitContainer.Height)
- {
- splitContainer.SplitterDistance = e.Y;
- splitContainer.Refresh();
- }
- }
+ var isVertical = splitContainer.Orientation.Equals(Orientation.Vertical);
+ 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();
}
else
{
diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.designer.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.designer.cs
index 7476f014..90594731 100644
--- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.designer.cs
+++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.designer.cs
@@ -96,7 +96,7 @@ private void InitializeComponent ()
filterListContextMenuStrip = new ContextMenuStrip(components);
colorToolStripMenuItem = new ToolStripMenuItem();
pnlFilterInput = new Panel();
- filterSplitContainer = new SplitContainer();
+ filterSplitContainer = new LogExpert.UI.Controls.BufferedSplitContainer();
filterComboBox = new ComboBox();
lblTextFilter = new Label();
btnAdvanced = new Button();
@@ -893,7 +893,7 @@ private void InitializeComponent ()
filterSplitContainer.Panel2.Controls.Add(filterRegexCheckBox);
filterSplitContainer.Panel2.Controls.Add(filterCaseSensitiveCheckBox);
filterSplitContainer.Panel2.Controls.Add(btnfilterSearch);
- filterSplitContainer.Panel2MinSize = 550;
+ filterSplitContainer.Panel2MinSize = 726;
filterSplitContainer.Size = new Size(1855, 46);
filterSplitContainer.SplitterDistance = 518;
filterSplitContainer.TabIndex = 11;
@@ -1271,7 +1271,7 @@ private void InitializeComponent ()
private System.Windows.Forms.Panel columnFinderPanel;
private System.Windows.Forms.ComboBox columnComboBox;
private System.Windows.Forms.Label lblColumnName;
- private System.Windows.Forms.SplitContainer filterSplitContainer;
+ private LogExpert.UI.Controls.BufferedSplitContainer filterSplitContainer;
private System.Windows.Forms.Label lblTextFilter;
private System.Windows.Forms.ComboBox filterComboBox;
private System.Windows.Forms.Button btnAdvanced;
diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index 62ae9c24..85f561bd 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-19 10:20:03 UTC
+ /// Generated: 2026-06-22 13:02:41 UTC
/// Configuration: Release
/// Plugin count: 21
///
@@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "FE5B41F852198756D6E7F565DFF531DB842260FC0AA8AC9E0FFBE39204FE5D15",
+ ["AutoColumnizer.dll"] = "4CC6296F65E54FA580455879AEB2FE549912BF29A3AF38E1B80CE9BDB3178237",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "10B5DC2422CFC4FA65D880DE0DE800138C9283AB28734A2D0F18431B40515D30",
- ["CsvColumnizer.dll (x86)"] = "10B5DC2422CFC4FA65D880DE0DE800138C9283AB28734A2D0F18431B40515D30",
- ["DefaultPlugins.dll"] = "F78E00FED77F20EA408C7D818045F7B37D2CA8BDAA3AB2BE4E15DF162615FE84",
- ["FlashIconHighlighter.dll"] = "B8BAC28BA72540CD2FA5AC189E7DF2616EB0F51B0D15DDA96688D11D2BB426C1",
- ["GlassfishColumnizer.dll"] = "90095DFB7B30B88D7477687A9BDCA1B119038AFBFF0216D0B7A852C4F41D66C6",
- ["JsonColumnizer.dll"] = "9E4F52BB6724451808B43719546CFA520B6FD17C65C231D172099D81DA8E2D0B",
- ["JsonCompactColumnizer.dll"] = "3FD84C6EB8BB3589F347C33C2B5B272A7BC13995AA32BEC0C67E596BC630B33F",
- ["Log4jXmlColumnizer.dll"] = "8EE85F16DE33AEA8BEA53B2EEEC8D032C572C5F3885CA07584DCB248D838F441",
- ["LogExpert.Resources.dll"] = "A4E16709FB669C2617AB6BC910737118B96F484E920FF06357FC85BF8A665A99",
+ ["CsvColumnizer.dll"] = "322DC4BC15604B3B5BDB4DDBEF6BAA5751B759014908C0DB574B1AB07E69FAB9",
+ ["CsvColumnizer.dll (x86)"] = "322DC4BC15604B3B5BDB4DDBEF6BAA5751B759014908C0DB574B1AB07E69FAB9",
+ ["DefaultPlugins.dll"] = "838A969AB6FAEC3ABE24387D180B1213A7399B0C9343EDAF7FB489B92B53FC44",
+ ["FlashIconHighlighter.dll"] = "056561FFA9643D07A47BFD2DF2E9FCA3253844A4395C9E0B5ECE7FAB8DAE5B13",
+ ["GlassfishColumnizer.dll"] = "D04336DD1B199871B51B8FE835223D16A1FBE8E04EADD69169CAB35FCACF5DC0",
+ ["JsonColumnizer.dll"] = "7F546E01C7DCE0612639F5C4EBA042D5FC2EF303C9E03E54D83C0D71127AA7F6",
+ ["JsonCompactColumnizer.dll"] = "02984648B7F1B3FF27E5A4F76EEBB4EAB0AD716DA25F77190926601715A6E918",
+ ["Log4jXmlColumnizer.dll"] = "E5EBA0B36BA51FE166E2662C55934C223A491B392B9F016D5BCB27D0FB293A68",
+ ["LogExpert.Resources.dll"] = "D148262DD2CAC5C6F9EEFDE2A0F73FC11206C015E36FD4FDF820B62BB608ABEF",
["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"] = "64EC13C21963405A42BBB41F42A886791B8E97910E660D0C8D244231454A8871",
- ["SftpFileSystem.dll"] = "962DBA38A676D7DA93B069F13765F9AD7C5E955F50C22BA46A356DE4B9556F88",
- ["SftpFileSystem.dll (x86)"] = "B872D63BA65261B943411098B46115A4940CC66F03E4807B561F6D20DB1F5F17",
- ["SftpFileSystem.Resources.dll"] = "E8039F5F595F9D30731ECD3A67116CEC41963CA4AA51BF4E0368CC6BE9FFE9D3",
- ["SftpFileSystem.Resources.dll (x86)"] = "E8039F5F595F9D30731ECD3A67116CEC41963CA4AA51BF4E0368CC6BE9FFE9D3",
+ ["RegexColumnizer.dll"] = "024D6660A126A64B666FDC9E8D304FD8D7254F3990E7FEAC2C8FD039F2F4C64A",
+ ["SftpFileSystem.dll"] = "BCA41A72D2DFACCC5B00B460F60CB7F5242AC199DA88A9069F453CEB12FDEE24",
+ ["SftpFileSystem.dll (x86)"] = "76456C1561F3FA0961D2711BC3F979F80627ABE78089849923E22D4609E52099",
+ ["SftpFileSystem.Resources.dll"] = "DCE7F4C96C4F296BFB346988A0E791718A652345A0E2EE795AE68384B6221174",
+ ["SftpFileSystem.Resources.dll (x86)"] = "DCE7F4C96C4F296BFB346988A0E791718A652345A0E2EE795AE68384B6221174",
};
}