Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/LogExpert.Resources/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
<value>Exportieren der Einstellungen in eine Datei</value>
</data>
<data name="HighlightDialog_UI_Export_Filter" xml:space="preserve">
<value>Einstellungen (*.json)|*.json|Alle Dateien (*.*)</value>
<value>Einstellungen (*.json)|*.json|Alle Dateien (*.*)|*.*</value>
</data>
<data name="HighlightDialog_UI_Snippet_CopyOf" xml:space="preserve">
<value>Kopie von</value>
Expand Down Expand Up @@ -2241,4 +2241,4 @@ LogExpert neu starten, um die Änderungen zu übernehmen?</value>
<data name="LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured" xml:space="preserve">
<value>Sysout für das Tool wurde konfiguriert, aber es gibt keine aktive Logdatei. Das Tool wird ohne die Sysout-Pipe gestartet</value>
</data>
</root>
</root>
4 changes: 2 additions & 2 deletions src/LogExpert.Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
<value>Export Settings to file</value>
</data>
<data name="HighlightDialog_UI_Export_Filter" xml:space="preserve">
<value>Settings (*.json)|*.json|All files (*.*)</value>
<value>Settings (*.json)|*.json|All files (*.*)|*.*</value>
</data>
<data name="HighlightDialog_UI_Snippet_CopyOf" xml:space="preserve">
<value>Copy of</value>
Expand Down Expand Up @@ -2275,4 +2275,4 @@ Restart LogExpert to apply changes?</value>
<data name="LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured" xml:space="preserve">
<value>Sysout for the Tool was configured, but there is no active logfile the Tool will be started without the Sysout Pipe</value>
</data>
</root>
</root>
6 changes: 3 additions & 3 deletions src/LogExpert.Resources/Resources.zh-CN.resx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -148,7 +148,7 @@
<value>导出设置到文件</value>
</data>
<data name="HighlightDialog_UI_Export_Filter" xml:space="preserve">
<value>设置文件 (*.json)|*.json|所有文件 (*.*)</value>
<value>设置文件 (*.json)|*.json|所有文件 (*.*)|*.*</value>
</data>
<data name="HighlightDialog_UI_Snippet_CopyOf" xml:space="preserve">
<value>复制为</value>
Expand Down Expand Up @@ -2152,4 +2152,4 @@ YY[YY] = 年
<data name="LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured" xml:space="preserve">
<value>已配置该工具的 Sysout,但没有活动日志文件,该工具将在没有 Sysout 管道的情况下启动</value>
</data>
</root>
</root>
166 changes: 166 additions & 0 deletions src/LogExpert.Tests/Controls/LogWindowPersistenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Reflection;
using System.Runtime.Versioning;

using LogExpert.Core.Classes.Filter;
using LogExpert.Core.Config;
using LogExpert.Core.Interfaces;
using LogExpert.UI.Controls.LogWindow;
using LogExpert.UI.Interface;

using Moq;

using NUnit.Framework;

namespace LogExpert.Tests.Controls;

[TestFixture]
[Apartment(ApartmentState.STA)]
[SupportedOSPlatform("windows")]
public sealed class LogWindowPersistenceTests : IDisposable
{
private Mock<ILogWindowCoordinator> _coordinatorMock = null!;
private Mock<IConfigManager> _configManagerMock = null!;
private Settings _settings = null!;
private WindowsFormsSynchronizationContext? _syncContext;
private LogWindow _logWindow = null!;
private bool _disposed;

[OneTimeSetUp]
public void OneTimeSetUp ()
{
var dir = Path.GetDirectoryName(typeof(LogWindowPersistenceTests).Assembly.Location)!;
_ = PluginRegistry.PluginRegistry.Create(dir, 500);
}

[SetUp]
public void SetUp ()
{
if (SynchronizationContext.Current == null)
{
_syncContext = new WindowsFormsSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(_syncContext);
}

_coordinatorMock = new Mock<ILogWindowCoordinator>();
_ = _coordinatorMock.Setup(c => c.ResolveHighlightGroup(It.IsAny<string?>(), It.IsAny<string?>()))
.Returns(new LogExpert.Core.Entities.HighlightGroup());
_ = _coordinatorMock.Setup(c => c.SearchParams).Returns(new LogExpert.Core.Entities.SearchParams());

_configManagerMock = new Mock<IConfigManager>();
_settings = new Settings();
_ = _configManagerMock.Setup(cm => cm.Settings).Returns(_settings);

_settings.FilterList.Add(new FilterParams
{
SearchText = "SHARED_FILTER_LIST_ENTRY",
IsCaseSensitive = false,
IsRegex = false,
IsFilterTail = true,
FuzzyValue = 0,
SpreadBefore = 0,
SpreadBehind = 0
});

_logWindow = new LogWindow(
_coordinatorMock.Object,
"test.log",
isTempFile: false,
forcePersistenceLoading: false,
configManager: _configManagerMock.Object);
}

[TearDown]
public void TearDown ()
{
_logWindow?.Dispose();
_syncContext?.Dispose();
_syncContext = null;
}

public void Dispose ()
{
Dispose(true);
GC.SuppressFinalize(this);
}

private void Dispose (bool disposing)
{
if (_disposed)
{
return;
}

if (disposing)
{
_logWindow?.Dispose();
_syncContext?.Dispose();
}

_disposed = true;
}

[Test]
public void GatherSessionSnapshot_SavesLiveFilterState_InsteadOfSharedFilterList ()
{
SetText("filterComboBox", "LIVE_FILTER_TEXT");
SetText("filterRangeComboBox", "LIVE_RANGE_TEXT");
SetChecked("filterCaseSensitiveCheckBox", true);
SetChecked("filterRegexCheckBox", true);
SetChecked("filterTailCheckBox", false);
SetChecked("invertFilterCheckBox", true);
SetChecked("rangeCheckBox", true);
SetChecked("columnRestrictCheckBox", true);
SetValue("knobControlFuzzy", 3);
SetValue("knobControlFilterBackSpread", 4);
SetValue("knobControlFilterForeSpread", 5);

var snapshot = _logWindow.GatherSessionSnapshot();

Assert.That(snapshot.FilterParamsList, Has.Count.EqualTo(1));

var saved = snapshot.FilterParamsList[0];
var shared = _settings.FilterList[0];

Assert.Multiple(() =>
{
Assert.That(saved, Is.Not.SameAs(shared));
Assert.That(saved.SearchText, Is.EqualTo("LIVE_FILTER_TEXT"));
Assert.That(saved.RangeSearchText, Is.EqualTo("LIVE_RANGE_TEXT"));
Assert.That(saved.IsCaseSensitive, Is.True);
Assert.That(saved.IsRegex, Is.True);
Assert.That(saved.IsFilterTail, Is.False);
Assert.That(saved.IsInvert, Is.True);
Assert.That(saved.IsRangeSearch, Is.True);
Assert.That(saved.ColumnRestrict, Is.True);
Assert.That(saved.FuzzyValue, Is.EqualTo(3));
Assert.That(saved.SpreadBefore, Is.EqualTo(4));
Assert.That(saved.SpreadBehind, Is.EqualTo(5));
Assert.That(saved.SearchText, Is.Not.EqualTo(shared.SearchText));
});
}

private void SetText (string fieldName, string value)
{
var control = GetField<System.Windows.Forms.Control>(fieldName);
control.Text = value;
}

private void SetChecked (string fieldName, bool value)
{
var control = GetField<object>(fieldName)!;
control.GetType().GetProperty("Checked")!.SetValue(control, value);
}

private void SetValue (string fieldName, int value)
{
var control = GetField<object>(fieldName)!;
control.GetType().GetProperty("Value")!.SetValue(control, value);
}

private T GetField<T> (string fieldName)
{
var field = _logWindow.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, $"Could not find private field '{fieldName}' on LogWindow");
return (T)field!.GetValue(_logWindow)!;
}
}
45 changes: 40 additions & 5 deletions src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ internal partial class LogWindow : DockContent, ILogPaintContextUI, ILogView, IL

private bool _waitingForClose;

/// <summary>
/// Set by the application shutdown path after a pre-save pass has already captured the
/// current persistence state. Prevents the per-window close handler from writing an older
/// snapshot after child filter tabs have already been torn down.
/// </summary>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
internal bool SkipPersistenceSaveOnClose { get; set; }

#endregion

#region cTor
Expand Down Expand Up @@ -905,7 +914,10 @@ private void OnLogWindowClosing (object sender, CancelEventArgs e)
return;
}

SavePersistenceData(false);
if (!SkipPersistenceSaveOnClose)
{
SavePersistenceData(false);
}
CloseLogWindow();
}

Expand Down Expand Up @@ -5953,9 +5965,14 @@ public SessionSnapshot GatherSessionSnapshot ()

if (Preferences.SaveFilters)
{
//when a filter is added, its added to the Configmanager.Settings.FilterList and not to the _filterParams, this is probably an oversight and maybe a bug
//but for the consistency the FilterList should be saved as whole for every file
snapshot.FilterParamsList = [.. ConfigManager.Settings.FilterList];
// Persist the live filter state for this window. The active filter lives in the window
// controls / _filterParams, while ConfigManager.Settings.FilterList is only the shared
// saved-filter list UI. The session loader restores the first entry as the active
// filter, so that first slot must be the current filter state.
snapshot.FilterParamsList =
[
CaptureCurrentFilterParams(),
];

foreach (var filterPipe in _filterPipeList)
{
Expand Down Expand Up @@ -5983,6 +6000,24 @@ public SessionSnapshot GatherSessionSnapshot ()
return snapshot;
}

private FilterParams CaptureCurrentFilterParams ()
{
var filterParams = _filterParams.Clone();
filterParams.SearchText = filterComboBox.Text;
filterParams.IsRangeSearch = rangeCheckBox.Checked;
filterParams.RangeSearchText = filterRangeComboBox.Text;
filterParams.IsCaseSensitive = filterCaseSensitiveCheckBox.Checked;
filterParams.IsRegex = filterRegexCheckBox.Checked;
filterParams.IsFilterTail = filterTailCheckBox.Checked;
filterParams.IsInvert = invertFilterCheckBox.Checked;
filterParams.FuzzyValue = knobControlFuzzy.Value;
filterParams.SpreadBefore = knobControlFilterBackSpread.Value;
filterParams.SpreadBehind = knobControlFilterForeSpread.Value;
filterParams.ColumnRestrict = columnRestrictCheckBox.Checked;
filterParams.CurrentColumnizer = CurrentColumnizer;
return filterParams;
}

public void Close (bool dontAsk)
{
Preferences.AskForClose = !dontAsk;
Expand Down Expand Up @@ -7957,4 +7992,4 @@ public void RefreshLogView ()
}

#endregion
}
}
18 changes: 16 additions & 2 deletions src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,11 +1705,25 @@ private void OnLogTabWindowFormClosing (object sender, CancelEventArgs e)
{
try
{
IList<LogWindow.LogWindow> deleteLogWindowList = [];
var windowsToClose = _tabController.GetAllWindows().ToList();

ConfigManager.Settings.AlwaysOnTop = TopMost && ConfigManager.Settings.Preferences.AllowOnlyOneInstance;
_fileOperationService.SaveLastOpenFilesList();

foreach (var logWindow in _tabController.GetAllWindows())
// Capture the current persistence state before any tab teardown starts. Closing a child
// filter tab removes it from its parent window's filter-tab list, so saving during the
// close cascade can overwrite the parent's .lxp with an incomplete snapshot.
foreach (var logWindow in windowsToClose)
{
logWindow.SkipPersistenceSaveOnClose = true;
}

foreach (var logWindow in windowsToClose)
{
logWindow.SavePersistenceData(false);
}

foreach (var logWindow in windowsToClose)
{
RemoveAndDisposeLogWindow(logWindow, true);
}
Expand Down