Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/LogExpert.Tests/Controls/FilterSplitterLayoutTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using LogExpert.UI.Controls.LogWindow;

using NUnit.Framework;

namespace LogExpert.Tests.Controls;

/// <summary>
/// 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.
/// </summary>
[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));
}
}
39 changes: 39 additions & 0 deletions src/LogExpert.UI/Controls/BufferedSplitContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Runtime.Versioning;

namespace LogExpert.UI.Controls;

/// <summary>
/// A <see cref="SplitContainer"/> that paints itself and all of its descendants composited (off-screen, bottom-to-top)
/// via the <c>WS_EX_COMPOSITED</c> extended window style.
///
/// <para>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). </para>
/// </summary>
/// <remarks>https://pinvoke.net/default.aspx/Enums/WindowStyles.html</remarks>
[SupportedOSPlatform("windows")]
internal sealed class BufferedSplitContainer : SplitContainer
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private const int WS_EX_COMPOSITED = 0x02000000;

protected override CreateParams CreateParams
{
get
{
var createParams = base.CreateParams;
createParams.ExStyle |= WS_EX_COMPOSITED;
return createParams;
}
}
}
41 changes: 41 additions & 0 deletions src/LogExpert.UI/Controls/LogWindow/FilterSplitterLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace LogExpert.UI.Controls.LogWindow;

/// <summary>
/// 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.
/// </summary>
internal static class FilterSplitterLayout
{
/// <summary>
/// Clamps a desired splitter distance so that neither panel shrinks below its minimum size.
/// </summary>
/// <param name="desiredDistance">The requested distance (Panel1 width) in pixels.</param>
/// <param name="containerWidth">Total width of the split container.</param>
/// <param name="splitterWidth">Width of the splitter bar.</param>
/// <param name="panel1MinSize">Minimum width of Panel1 (the text filter).</param>
/// <param name="panel2MinSize">Minimum width of Panel2 (the buttons/checkboxes).</param>
/// <returns>A distance guaranteed to keep Panel2 at least <paramref name="panel2MinSize"/> wide.</returns>
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);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="rightmostControlRightEdge">Right edge (Left + Width) of the rightmost left-anchored control.</param>
/// <param name="rightAnchoredControlWidth">Width of the right-anchored control.</param>
/// <param name="gap">Desired gap in pixels between the two controls.</param>
public static int RequiredPanel2Width (int rightmostControlRightEdge, int rightAnchoredControlWidth, int gap)
{
return rightmostControlRightEdge + gap + rightAnchoredControlWidth;
}
}
34 changes: 17 additions & 17 deletions src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -736,7 +741,9 @@ void ILogWindow.WritePipeTab (IList<LineEntryMemory> 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
Expand Down Expand Up @@ -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
{
Expand Down
6 changes: 3 additions & 3 deletions src/LogExpert.UI/Controls/LogWindow/LogWindow.designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 16 additions & 16 deletions src/PluginRegistry/PluginHashGenerator.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,35 @@ public static partial class PluginValidator
{
/// <summary>
/// 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
/// </summary>
public static Dictionary<string, string> GetBuiltInPluginHashes()
{
return new Dictionary<string, string>(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",

};
}
Expand Down