diff --git a/src/ColumnizerLib/IKeywordAction.cs b/src/ColumnizerLib/IKeywordAction.cs
index cfd9b36f..f42e4411 100644
--- a/src/ColumnizerLib/IKeywordAction.cs
+++ b/src/ColumnizerLib/IKeywordAction.cs
@@ -1,11 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
namespace ColumnizerLib;
///
-/// Implement this interface to execute a self defined action when LogExpert detects a
+/// Implement this interface to execute a self defined action when LogExpert detects a
/// keyword on incomig log file content.
/// These kind of plugins can be used in the "Highlight and Action Triggers" dialog.
///
@@ -21,30 +17,30 @@ public interface IKeywordAction
/// The keyword which triggered the call.
/// The parameter configured for the plugin launch (in the Highlight dialog).
/// A callback which can be used by the plugin.
- /// The current columnizer. Can be used to obtain timestamps
+ /// The current columnizer. Can be used to obtain timestamps
/// (if supported by Columnizer) or to split the log line into fields.
///
- /// This method is called in a background thread from the process' thread pool (using BeginInvoke()).
+ /// This method is called in a background thread from the process' thread pool (using BeginInvoke()).
/// So you cannot rely on state information retrieved by the given callback. E.g. the line count
/// may change during the execution of the method. The only exception from this rule is the current line number
/// retrieved from the callback. This is of course the line number of the line that has triggered
/// the keyword match.
///
- void Execute(string keyword, string param, ILogExpertCallback callback, ILogLineMemoryColumnizer columnizer);
+ void Execute (string keyword, string param, ILogExpertCallbackMemory callback, ILogLineMemoryColumnizer columnizer);
///
- /// Return the name of your plugin here. The returned name is used for displaying the plugin list
+ /// Return the name of your plugin here. The returned name is used for displaying the plugin list
/// in the settings.
///
/// The name of the plugin.
- string GetName();
+ string GetName ();
///
/// Return a description of your plugin here. E.g. a short explanation of parameters. The descriptions
/// will be displayed in the plugin chooser dialog which is used by the Highlight settings.
///
/// The description of the plugin.
- string GetDescription();
+ string GetDescription ();
#endregion
}
\ No newline at end of file
diff --git a/src/CsvColumnizer/CsvColumnizer.cs b/src/CsvColumnizer/CsvColumnizer.cs
index 8ddfb357..12b065f3 100644
--- a/src/CsvColumnizer/CsvColumnizer.cs
+++ b/src/CsvColumnizer/CsvColumnizer.cs
@@ -187,7 +187,7 @@ public void Selected (ILogLineMemoryColumnizerCallback callback)
_columnList.Clear();
var line = _config.HasFieldNames
? _firstLine
- : callback.GetLogLine(0);
+ : callback.GetLogLineMemory(0);
if (line != null)
{
diff --git a/src/DefaultPlugins/ProcessLauncher.cs b/src/DefaultPlugins/ProcessLauncher.cs
index 50bc241f..f87fd5cc 100644
--- a/src/DefaultPlugins/ProcessLauncher.cs
+++ b/src/DefaultPlugins/ProcessLauncher.cs
@@ -1,4 +1,3 @@
-using System;
using System.Diagnostics;
using ColumnizerLib;
@@ -15,9 +14,9 @@ internal class ProcessLauncher : IKeywordAction
#region IKeywordAction Member
- private readonly object _callbackLock = new();
+ private readonly Lock _callbackLock = new();
- public void Execute (string keyword, string param, ILogExpertCallback callback, ILogLineMemoryColumnizer columnizer)
+ public void Execute (string keyword, string param, ILogExpertCallbackMemory callback, ILogLineMemoryColumnizer columnizer)
{
var start = 0;
int end;
@@ -46,16 +45,16 @@ public void Execute (string keyword, string param, ILogExpertCallback callback,
parameters = parameters.Replace("%K", keyword, StringComparison.Ordinal);
var lineNumber = callback.LineNum; //Line Numbers start at 0, but are displayed (+1)
- var logline = callback.GetLogLine(lineNumber).FullLine;
- parameters = parameters.Replace("%L", string.Empty + lineNumber, System.StringComparison.Ordinal);
+ var logline = callback.GetLogLineMemory(lineNumber).FullLine;
+ parameters = parameters.Replace("%L", string.Empty + lineNumber, StringComparison.Ordinal);
parameters = parameters.Replace("%T", callback.GetTabTitle(), StringComparison.Ordinal);
- parameters = parameters.Replace("%C", logline, StringComparison.Ordinal);
+ parameters = parameters.Replace("%C", logline.ToString(), StringComparison.Ordinal);
Process explorer = new();
explorer.StartInfo.FileName = procName;
explorer.StartInfo.Arguments = parameters;
explorer.StartInfo.UseShellExecute = false;
- explorer.Start();
+ _ = explorer.Start();
}
}
diff --git a/src/FlashIconHighlighter/FlashIconHighlighter.csproj b/src/FlashIconHighlighter/FlashIconHighlighter.csproj
index 145ff159..afed49cc 100644
--- a/src/FlashIconHighlighter/FlashIconHighlighter.csproj
+++ b/src/FlashIconHighlighter/FlashIconHighlighter.csproj
@@ -7,8 +7,14 @@
true
$(SolutionDir)..\bin\$(Configuration)\plugins
true
+ FlashIconHighlighter
+ FlashIconHighlighter
+
+
+
+
diff --git a/src/FlashIconHighlighter/FlashIconPlugin.cs b/src/FlashIconHighlighter/FlashIconPlugin.cs
index 2efaab6b..6e38ac00 100644
--- a/src/FlashIconHighlighter/FlashIconPlugin.cs
+++ b/src/FlashIconHighlighter/FlashIconPlugin.cs
@@ -1,10 +1,10 @@
-using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
-using System.Windows.Forms;
using ColumnizerLib;
+using static Vanara.PInvoke.User32;
+
[assembly: SupportedOSPlatform("windows")]
namespace FlashIconHighlighter;
@@ -18,29 +18,33 @@ internal class FlashIconPlugin : IKeywordAction
#region IKeywordAction Member
- public void Execute (string keyword, string param, ILogExpertCallback callback, ILogLineMemoryColumnizer columnizer)
+ public void Execute (string keyword, string param, ILogExpertCallbackMemory callback, ILogLineMemoryColumnizer columnizer)
{
var openForms = Application.OpenForms;
foreach (Form form in openForms)
{
if (form.TopLevel && form.Name.Equals("LogTabWindow", StringComparison.OrdinalIgnoreCase) && form.Text.Contains(callback.GetFileName(), StringComparison.Ordinal))
{
- form.BeginInvoke(FlashWindow, [form]);
+ _ = form.BeginInvoke(FlashWindow, [form]);
}
}
}
+ ///
+ /// Flash Window http://blogs.x2line.com/al/archive/2008/04/19/3392.aspx
+ ///
+ ///
private void FlashWindow (Form form)
{
FLASHWINFO fw = new()
{
- cbSize = Convert.ToUInt32(Marshal.SizeOf(typeof(FLASHWINFO))),
+ cbSize = Convert.ToUInt32(Marshal.SizeOf()),
hwnd = form.Handle,
- dwFlags = 14,
+ dwFlags = FLASHW.FLASHW_TRAY | FLASHW.FLASHW_CAPTION | FLASHW.FLASHW_TIMER,
uCount = 0
};
- Win32Stuff.FlashWindowEx(ref fw);
+ _ = FlashWindowEx(fw);
}
public string GetDescription ()
diff --git a/src/FlashIconHighlighter/Win32Stuff.cs b/src/FlashIconHighlighter/Win32Stuff.cs
deleted file mode 100644
index 3f3bd754..00000000
--- a/src/FlashIconHighlighter/Win32Stuff.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Runtime.InteropServices;
-
-namespace FlashIconHighlighter;
-
-/*
- * Flash stuff stolen from http://blogs.x2line.com/al/archive/2008/04/19/3392.aspx
- */
-
-[StructLayout(LayoutKind.Sequential)]
-public struct FLASHWINFO
-{
- public uint cbSize;
- public IntPtr hwnd;
- public int dwFlags;
- public uint uCount;
- public int dwTimeout;
-}
-
-public static partial class Win32Stuff
-{
- #region Public methods
-
- [LibraryImport("user32.dll")]
- [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
- public static partial int FlashWindowEx (ref FLASHWINFO pwfi);
-
- #endregion
-}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Filter/FilterPipe.cs b/src/LogExpert.Core/Classes/Filter/FilterPipe.cs
index ff166a12..a1867dcf 100644
--- a/src/LogExpert.Core/Classes/Filter/FilterPipe.cs
+++ b/src/LogExpert.Core/Classes/Filter/FilterPipe.cs
@@ -4,6 +4,7 @@
using ColumnizerLib;
using LogExpert.Core.Interface;
+
using NLog;
namespace LogExpert.Core.Classes.Filter;
@@ -23,7 +24,7 @@ public class FilterPipe : IDisposable
#region cTor
- public FilterPipe(FilterParams filterParams, ILogWindow logWindow)
+ public FilterPipe (FilterParams filterParams, ILogWindow logWindow)
{
FilterParams = filterParams;
LogWindow = logWindow;
@@ -68,15 +69,12 @@ public void OpenFile ()
public void CloseFile ()
{
- if (_writer != null)
- {
- _writer.Close();
- _writer = null;
- }
+ _writer?.Close();
+ _writer = null;
}
//TOOD: check if the callers are checking for null before calling
- public bool WriteToPipe (ILogLine textLine, int orgLineNum)
+ public bool WriteToPipe (ILogLineMemory textLine, int orgLineNum)
{
ArgumentNullException.ThrowIfNull(textLine, nameof(textLine));
@@ -88,7 +86,7 @@ public bool WriteToPipe (ILogLine textLine, int orgLineNum)
{
try
{
- _writer.WriteLine(textLine.FullLine);
+ _writer.WriteLine(textLine.FullLine.ToString());
_lineMappingList.Add(orgLineNum);
return true;
}
diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs
index 9b81d83e..f8d64146 100644
--- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs
+++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs
@@ -1277,7 +1277,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
}
}
- AcquireDisposeReaderLock();
+ AcquireDisposeLockUpgradableReadLock();
if (logBuffer.IsDisposed)
{
UpgradeDisposeLockToWriterLock();
@@ -1285,7 +1285,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
DowngradeDisposeLockFromWriterLock();
}
- ReleaseDisposeReaderLock();
+ ReleaseDisposeUpgradeableReadLock();
}
}
finally
diff --git a/src/LogExpert.Core/EventArguments/ContextMenuPluginEventArgs.cs b/src/LogExpert.Core/EventArguments/ContextMenuPluginEventArgs.cs
index 1fae61df..a642e9a4 100644
--- a/src/LogExpert.Core/EventArguments/ContextMenuPluginEventArgs.cs
+++ b/src/LogExpert.Core/EventArguments/ContextMenuPluginEventArgs.cs
@@ -2,8 +2,8 @@
namespace LogExpert.Core.EventArguments;
-public class ContextMenuPluginEventArgs(IContextMenuEntry entry, IList logLines, ILogLineMemoryColumnizer columnizer,
- ILogExpertCallback callback) : System.EventArgs
+public class ContextMenuPluginEventArgs (IContextMenuEntry entry, IList logLines, ILogLineMemoryColumnizer columnizer,
+ ILogExpertCallbackMemory callback) : EventArgs
{
#region Properties
@@ -14,7 +14,7 @@ public class ContextMenuPluginEventArgs(IContextMenuEntry entry, IList logL
public ILogLineMemoryColumnizer Columnizer { get; } = columnizer;
- public ILogExpertCallback Callback { get; } = callback;
+ public ILogExpertCallbackMemory Callback { get; } = callback;
#endregion
}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Interface/ILogWindow.cs b/src/LogExpert.Core/Interface/ILogWindow.cs
index c644e2d3..406ce15a 100644
--- a/src/LogExpert.Core/Interface/ILogWindow.cs
+++ b/src/LogExpert.Core/Interface/ILogWindow.cs
@@ -199,6 +199,21 @@ public interface ILogWindow
///
void WritePipeTab (IList lineEntryList, string title);
+ ///
+ /// Creates a new tab containing the specified list of log line entries.
+ ///
+ ///
+ /// A list of objects containing the lines and their
+ /// original line numbers to display in the new tab.
+ ///
+ /// The title to display on the tab.
+ ///
+ /// This method is used to pipe filtered or selected content into a new tab
+ /// without creating a physical file. The new tab maintains references to the
+ /// original line numbers for context.
+ ///
+ void WritePipeTab (IList lineEntryList, string title);
+
///
/// Activates this log window and brings it to the foreground.
///
diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs
index 68f34bbe..06018dff 100644
--- a/src/LogExpert.Resources/Resources.Designer.cs
+++ b/src/LogExpert.Resources/Resources.Designer.cs
@@ -1388,6 +1388,33 @@ public static string LogExpert_Common_Error_InsufficientRights_For_Parameter_Err
}
}
+ ///
+ /// Looks up a localized string similar to {0} is already initialized.
+ ///
+ public static string LogExpert_Common_Error_Message_ServiceIsAlreadyInitialized {
+ get {
+ return ResourceManager.GetString("LogExpert_Common_Error_Message_ServiceIsAlreadyInitialized", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} must be created on UI thread.
+ ///
+ public static string LogExpert_Common_Error_Message_ServiceMustBeCreatedOnUIThread {
+ get {
+ return ResourceManager.GetString("LogExpert_Common_Error_Message_ServiceMustBeCreatedOnUIThread", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} not initialized.
+ ///
+ public static string LogExpert_Common_Error_Message_ServiceNotInitialized {
+ get {
+ return ResourceManager.GetString("LogExpert_Common_Error_Message_ServiceNotInitialized", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to &Add.
///
diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx
index 4d66d6b2..c1ec0c3d 100644
--- a/src/LogExpert.Resources/Resources.de.resx
+++ b/src/LogExpert.Resources/Resources.de.resx
@@ -2115,4 +2115,10 @@ Fortfahren?
LogExpert neu starten, um die Änderungen zu übernehmen?
+
+ {0} nicht initialisiert
+
+
+ {0} ist bereits initialisiert
+
\ No newline at end of file
diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx
index a9133a4f..efeac89a 100644
--- a/src/LogExpert.Resources/Resources.resx
+++ b/src/LogExpert.Resources/Resources.resx
@@ -2124,4 +2124,13 @@ Restart LogExpert to apply changes?
Locate: {0}
+
+ {0} not initialized
+
+
+ {0} is already initialized
+
+
+ {0} must be created on UI thread
+
\ No newline at end of file
diff --git a/src/LogExpert.Tests/Services/LedIndicatorServiceTests.cs b/src/LogExpert.Tests/Services/LedIndicatorServiceTests.cs
new file mode 100644
index 00000000..9153e21a
--- /dev/null
+++ b/src/LogExpert.Tests/Services/LedIndicatorServiceTests.cs
@@ -0,0 +1,319 @@
+using System.Runtime.Versioning;
+
+using LogExpert.UI.Services;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.Services;
+
+[TestFixture]
+[Apartment(ApartmentState.STA)] // Required for UI components
+[SupportedOSPlatform("windows")]
+public class LedIndicatorServiceTests
+{
+ private LedIndicatorService? _service;
+ private ApplicationContext? _appContext;
+ private WindowsFormsSynchronizationContext? _syncContext;
+
+ [SetUp]
+ public void Setup ()
+ {
+ // Ensure we have a WindowsFormsSynchronizationContext for the UI thread
+ if (SynchronizationContext.Current == null)
+ {
+ _syncContext = new WindowsFormsSynchronizationContext();
+ SynchronizationContext.SetSynchronizationContext(_syncContext);
+ }
+
+ // Create an application context to ensure we have a proper UI context
+ _appContext = new ApplicationContext();
+
+ // Must be created on STA thread with synchronization context
+ _service = new LedIndicatorService();
+ }
+
+ [TearDown]
+ public void TearDown ()
+ {
+ _service?.Dispose();
+ _appContext?.Dispose();
+ _syncContext?.Dispose();
+ }
+
+ [Test]
+ public void Initialize_WithValidColor_Succeeds ()
+ {
+ // Act
+ _service!.Initialize(Color.Blue);
+
+ // Assert - no exception thrown
+ Assert.That(_service, Is.Not.Null);
+ }
+
+ [Test]
+ public void Initialize_CalledTwice_ThrowsException ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // Act & Assert
+ _ = Assert.Throws(() => _service.Initialize(Color.Red));
+ }
+
+ [Test]
+ public void GetIcon_WithZeroDiff_ReturnsOffLevelIcon ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ var state = new LedState
+ {
+ IsDirty = false,
+ TailState = TailFollowState.On,
+ SyncState = TimeSyncState.NotSynced
+ };
+
+ // Act
+ var icon = _service.GetIcon(0, state);
+
+ // Assert
+ Assert.That(icon, Is.Not.Null);
+ Assert.That(icon.Width, Is.EqualTo(16));
+ Assert.That(icon.Height, Is.EqualTo(16));
+ }
+
+ [Test]
+ public void GetIcon_WithMaxDiff_ReturnsHighestLevelIcon ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ var state = new LedState
+ {
+ IsDirty = false,
+ TailState = TailFollowState.On,
+ SyncState = TimeSyncState.NotSynced
+ };
+
+ // Act
+ var icon = _service.GetIcon(100, state);
+
+ // Assert
+ Assert.That(icon, Is.Not.Null);
+ }
+
+ [Test]
+ public void GetIcon_WithDirtyState_ReturnsDirtyIcon ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ var state = new LedState
+ {
+ IsDirty = true,
+ TailState = TailFollowState.On,
+ SyncState = TimeSyncState.NotSynced
+ };
+
+ // Act
+ var icon = _service.GetIcon(50, state);
+
+ // Assert
+ Assert.That(icon, Is.Not.Null);
+ }
+
+ [Test]
+ public void GetDeadIcon_ReturnsNonNullIcon ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // Act
+ var icon = _service.GetDeadIcon();
+
+ // Assert
+ Assert.That(icon, Is.Not.Null);
+ Assert.That(icon.Width, Is.EqualTo(16));
+ }
+
+ [Test]
+ public void StartStop_DoesNotThrowException ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // Act
+ _service.Start();
+ Thread.Sleep(500); // Let timer tick a few times
+ _service.Stop();
+
+ // Assert - no exception
+ Assert.That(true, Is.True, "Service started and stopped without exceptions");
+ }
+
+ [Test]
+ public void RegisterWindow_AddsWindowToTracking ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // We can't easily mock LogWindow since it has no parameterless constructor
+ // and is internal, so we just test that registering null throws
+ // Act & Assert
+ _ = Assert.Throws(() => _service.RegisterWindow(null!));
+ }
+
+ [Test]
+ public void UpdateWindowActivity_WithoutRegisteringWindow_DoesNotThrow ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // Act & Assert - Updating an unregistered window should not throw
+ // (it just won't raise events)
+ Assert.DoesNotThrow(() => _service.UpdateWindowActivity(null, 10));
+ }
+
+ [Test]
+ public void RegenerateIcons_WithNoWindows_DoesNotThrow ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ int eventCount = 0;
+ _service.IconChanged += (s, e) => eventCount++;
+
+ // Act
+ _service.RegenerateIcons(Color.Red);
+
+ // Assert - No windows registered, so no events should be raised
+ Assert.That(eventCount, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void Dispose_DisposesAllResources ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ _service.Start();
+
+ // Act
+ _service.Dispose();
+
+ // Assert - After dispose, trying to use the service will throw an exception
+ var exception = Assert.Catch(() => _service.GetIcon(0, new LedState()));
+ Assert.That(exception, Is.Not.Null, "Should throw an exception after disposal");
+ }
+
+ [Test]
+ public void GetIcon_WithoutInitialize_ThrowsException ()
+ {
+ // Arrange - don't initialize
+
+ // Act & Assert
+ _ = Assert.Throws(() => _service!.GetIcon(0, new LedState()));
+ }
+
+ [Test]
+ public void Start_WithoutInitialize_ThrowsException ()
+ {
+ // Arrange - don't initialize
+
+ // Act & Assert
+ _ = Assert.Throws(() => _service!.Start());
+ }
+
+ [Test]
+ public void RegisterWindow_WithNullWindow_ThrowsException ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // Act & Assert
+ _ = Assert.Throws(() => _service.RegisterWindow(null!));
+ }
+
+ [Test]
+ public void UnregisterWindow_WithNullWindow_DoesNotThrow ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+
+ // Act & Assert - Unregistering null should not throw
+ Assert.DoesNotThrow(() => _service.UnregisterWindow(null));
+ }
+
+ [Test]
+ public void CurrentTailColor_AfterInitialize_ReturnsInitializedColor ()
+ {
+ // Arrange
+ var expectedColor = Color.FromArgb(50, 100, 200);
+ _service!.Initialize(expectedColor);
+
+ // Act
+ var actualColor = _service.CurrentTailColor;
+
+ // Assert
+ Assert.That(actualColor, Is.EqualTo(expectedColor));
+ }
+
+ [Test]
+ public void CurrentTailColor_AfterRegenerateIcons_ReturnsNewColor ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ var newColor = Color.FromArgb(255, 128, 0);
+
+ // Act
+ _service.RegenerateIcons(newColor);
+
+ // Assert
+ Assert.That(_service.CurrentTailColor, Is.EqualTo(newColor));
+ }
+
+ [Test]
+ public void CurrentTailColor_BeforeInitialize_ThrowsException ()
+ {
+ // Arrange - don't initialize
+
+ // Act & Assert
+ _ = Assert.Throws(() => _ = _service!.CurrentTailColor);
+ }
+
+ [Test]
+ public void CurrentTailColor_AfterDispose_ThrowsObjectDisposedException ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ _service.Dispose();
+
+ // Act & Assert
+ _ = Assert.Throws(() => _ = _service.CurrentTailColor);
+ }
+
+ [Test]
+ public void GetIcon_WithSyncedState_ReturnsSyncedIcon ()
+ {
+ // Arrange
+ _service!.Initialize(Color.Blue);
+ var stateSynced = new LedState
+ {
+ IsDirty = false,
+ TailState = TailFollowState.On,
+ SyncState = TimeSyncState.Synced
+ };
+ var stateNotSynced = new LedState
+ {
+ IsDirty = false,
+ TailState = TailFollowState.On,
+ SyncState = TimeSyncState.NotSynced
+ };
+
+ // Act
+ var iconSynced = _service.GetIcon(50, stateSynced);
+ var iconNotSynced = _service.GetIcon(50, stateNotSynced);
+
+ // Assert
+ Assert.That(iconSynced, Is.Not.Null);
+ Assert.That(iconNotSynced, Is.Not.Null);
+ // The icons should be different (synced has blue indicator on left side)
+ Assert.That(iconSynced, Is.Not.EqualTo(iconNotSynced));
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.Tests/UI/LogTabWindowResourceTests.cs b/src/LogExpert.Tests/UI/LogTabWindowResourceTests.cs
new file mode 100644
index 00000000..678a5ed9
--- /dev/null
+++ b/src/LogExpert.Tests/UI/LogTabWindowResourceTests.cs
@@ -0,0 +1,212 @@
+using System.Runtime.Versioning;
+
+using LogExpert.Core.Config;
+using LogExpert.Core.Interface;
+using LogExpert.UI.Extensions.LogWindow;
+
+using Moq;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.UI;
+
+[TestFixture]
+[Apartment(ApartmentState.STA)] // Required for WinForms components
+public class LogTabWindowResourceTests
+{
+ [Test]
+ [Category("Resource")]
+ [SupportedOSPlatform("windows")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Unit Tests")]
+ public void Dispose_DisposesAllGdiResources ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ // Create the window using the factory method
+ ILogTabWindow? window = null;
+ bool disposedSuccessfully = false;
+
+ try
+ {
+ window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ // Give time for initialization
+ Thread.Sleep(300);
+
+ // Act - Dispose via close (Form.Close calls Dispose internally)
+ if (window is Form form)
+ {
+ form.Close();
+ form.Dispose();
+ disposedSuccessfully = true;
+ }
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Test failed with exception during disposal: {ex.Message}\n{ex.StackTrace}");
+ }
+ finally
+ {
+ // Ensure cleanup even if test fails
+ if (window is IDisposable disposable && window is Form form && !form.IsDisposed)
+ {
+ try
+ {
+ disposable.Dispose();
+ }
+ catch
+ {
+ // Suppress exceptions in cleanup
+ }
+ }
+ }
+
+ // Assert - If disposal has bugs, we'd get exceptions or access violations
+ Assert.That(disposedSuccessfully, Is.True, "Window should dispose successfully without exceptions");
+ }
+
+ [Test]
+ [Category("Resource")]
+ [SupportedOSPlatform("windows")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")]
+ public void Constructor_InitializesSuccessfully ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ ILogTabWindow? window = null;
+
+ try
+ {
+ // Act
+ window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ // Give time for initialization
+ Thread.Sleep(300);
+
+ // Assert - Verify window was created and basic structure exists
+ Assert.That(window, Is.Not.Null, "Window should be created");
+
+ if (window is Form form)
+ {
+ Assert.That(form.IsDisposed, Is.False, "Window should not be disposed after creation");
+ Assert.That(form.Handle, Is.Not.EqualTo(IntPtr.Zero), "Window should have a valid handle");
+ }
+ else
+ {
+ Assert.Fail("Window should be a Form");
+ }
+ }
+ finally
+ {
+ if (window is Form form)
+ {
+ form.Close();
+ form.Dispose();
+ }
+ }
+ }
+
+ [Test]
+ [Category("Resource")]
+ [SupportedOSPlatform("windows")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Unit Tests")]
+ public void MultipleCreateDispose_DoesNotLeakResources ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ var exceptions = new List();
+
+ // Act - Create and dispose multiple windows
+ // If there's a resource leak, we'll eventually hit system limits or get exceptions
+ for (int i = 0; i < 5; i++)
+ {
+ try
+ {
+ var window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ Thread.Sleep(100); // Allow initialization
+
+ if (window is Form form)
+ {
+ form.Close();
+ form.Dispose();
+ }
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
+ }
+
+ Thread.Sleep(200); // Allow final cleanup
+
+ // Assert
+ Assert.That(exceptions, Is.Empty,
+ $"Should create and dispose multiple windows without exceptions. " +
+ $"Exceptions: {string.Join("; ", exceptions.Select(e => e.Message))}");
+ }
+
+ [Test]
+ [Category("Resource")]
+ [SupportedOSPlatform("windows")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")]
+ public void Dispose_CanBeCalledMultipleTimes ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ var window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ Thread.Sleep(200);
+
+ // Act & Assert - Multiple dispose calls should not throw
+ if (window is Form form)
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ form.Close();
+ form.Dispose();
+ form.Dispose(); // Second dispose should be safe
+ form.Dispose(); // Third dispose should be safe
+ }, "Multiple Dispose calls should not throw exceptions");
+ }
+ else
+ {
+ Assert.Fail("Window should be a Form");
+ }
+ }
+}
diff --git a/src/LogExpert.Tests/UI/LogTabWindowThreadingTests.cs b/src/LogExpert.Tests/UI/LogTabWindowThreadingTests.cs
new file mode 100644
index 00000000..5fb2301e
--- /dev/null
+++ b/src/LogExpert.Tests/UI/LogTabWindowThreadingTests.cs
@@ -0,0 +1,259 @@
+using System.Runtime.Versioning;
+
+using LogExpert.Core.Config;
+using LogExpert.Core.Interface;
+using LogExpert.UI.Extensions.LogWindow;
+
+using Moq;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.UI;
+
+[TestFixture]
+[Apartment(ApartmentState.STA)] // Required for WinForms components
+public class LogTabWindowThreadingTests
+{
+ [Test]
+ [Category("Threading")]
+ [SupportedOSPlatform("windows")]
+ public void LedThread_WithConcurrentUpdates_NoRaceCondition ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ var window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ Form? windowForm = null;
+
+ // Ensure window handle is created
+ if (window is Form f)
+ {
+ windowForm = f;
+ _ = windowForm.Handle; // Force handle creation
+ }
+
+ // Give time for LED thread to start and stabilize
+ Thread.Sleep(500);
+
+ var exceptions = new List();
+ var exceptionLock = new object();
+
+ try
+ {
+ // Act - Simulate concurrent operations on the window
+ // The LED thread runs in the background and updates icons
+ // We stress test by performing many concurrent read operations
+ var tasks = new Task[20];
+ for (int i = 0; i < tasks.Length; i++)
+ {
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ // Perform safe read operations that might race with LED thread
+ for (int j = 0; j < 10; j++)
+ {
+ if (windowForm != null && !windowForm.IsDisposed && windowForm.IsHandleCreated)
+ {
+ // Read operations that don't require Invoke
+ _ = windowForm.Text;
+ _ = windowForm.Visible;
+ _ = windowForm.IsDisposed;
+ }
+
+ Thread.Sleep(5); // Small delay to increase overlap with LED thread
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ // Expected if window is disposed during test
+ }
+ catch (InvalidOperationException ex) when (ex.Message.Contains("disposed", StringComparison.OrdinalIgnoreCase))
+ {
+ // Expected if window is disposed during test
+ }
+ catch (Exception ex)
+ {
+ lock (exceptionLock)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ // Let LED thread process any pending updates
+ Thread.Sleep(500);
+
+ // Assert - No unexpected exceptions should occur during concurrent access
+ Assert.That(exceptions, Is.Empty,
+ $"Race condition detected. Exceptions occurred: " +
+ $"{string.Join("; ", exceptions.Select(e => $"{e.GetType().Name}: {e.Message}"))}");
+ }
+ finally
+ {
+ // Cleanup
+ if (windowForm != null)
+ {
+ windowForm.Close();
+ windowForm.Dispose();
+ }
+ }
+ }
+
+ [Test]
+ [Category("Threading")]
+ [SupportedOSPlatform("windows")]
+ public void LedThread_StartsAndStopsCleanly ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ var window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ bool cleanedUpSuccessfully = false;
+
+ try
+ {
+ // Give time for LED thread to start
+ Thread.Sleep(300);
+
+ // Act - Close window (should stop LED thread gracefully)
+ if (window is Form form)
+ {
+ form.Close();
+ form.Dispose();
+ }
+
+ // Let thread cleanup complete
+ Thread.Sleep(200);
+
+ cleanedUpSuccessfully = true;
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"LED thread cleanup failed: {ex.Message}");
+ }
+
+ // Assert - If thread cleanup has issues, disposal would throw or hang
+ Assert.That(cleanedUpSuccessfully, Is.True, "LED thread should start and stop cleanly");
+ }
+
+ [Test]
+ [Category("Threading")]
+ [SupportedOSPlatform("windows")]
+ public void MultipleWindows_LedThreadsDoNotInterfere ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ var windows = new List();
+ var exceptions = new List();
+
+ try
+ {
+ // Act - Create multiple windows, each with their own LED thread
+ for (int i = 0; i < 3; i++)
+ {
+ try
+ {
+ var window = AbstractLogTabWindow.Create(
+ [],
+ i + 1,
+ false,
+ mockConfigManager.Object
+ );
+ windows.Add(window);
+ Thread.Sleep(100); // Stagger creation
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
+ }
+
+ // Let all LED threads run concurrently
+ Thread.Sleep(800);
+
+ // Assert - All windows should be created and running without interference
+ Assert.That(exceptions, Is.Empty,
+ $"Exceptions during window creation: {string.Join("; ", exceptions.Select(e => e.Message))}");
+ Assert.That(windows.Count, Is.EqualTo(3), "All windows should be created successfully");
+ }
+ finally
+ {
+ // Cleanup all windows
+ foreach (var window in windows)
+ {
+ try
+ {
+ if (window is Form form)
+ {
+ form.Close();
+ form.Dispose();
+ }
+ }
+ catch
+ {
+ // Suppress cleanup exceptions
+ }
+ }
+ }
+ }
+
+ [Test]
+ [Category("Threading")]
+ [SupportedOSPlatform("windows")]
+ [Repeat(5)] // Run multiple times to catch intermittent race conditions
+ public void LedThread_RepeatedStartStop_NoMemoryLeak ()
+ {
+ // Arrange
+ var mockConfigManager = new Mock();
+ _ = mockConfigManager.Setup(m => m.Settings).Returns(new Settings());
+
+ // Act - Create and destroy window multiple times
+ // Each time starts and stops LED thread
+ for (int i = 0; i < 5; i++)
+ {
+ var window = AbstractLogTabWindow.Create(
+ [],
+ 1,
+ false,
+ mockConfigManager.Object
+ );
+
+ Thread.Sleep(100); // Let LED thread start
+
+ if (window is Form form)
+ {
+ form.Close();
+ form.Dispose();
+ }
+
+ Thread.Sleep(100); // Let LED thread stop
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ // Assert - If there are thread leaks, we'd eventually get exceptions
+ // The fact that we got here without exceptions means the test passed
+ Assert.That(true, Is.True, "No threading issues detected during repeated create/dispose cycles");
+ }
+}
diff --git a/src/LogExpert.UI/Controls/BufferedDataGridView.cs b/src/LogExpert.UI/Controls/BufferedDataGridView.cs
index 756e98f8..8aa2df6f 100644
--- a/src/LogExpert.UI/Controls/BufferedDataGridView.cs
+++ b/src/LogExpert.UI/Controls/BufferedDataGridView.cs
@@ -214,7 +214,7 @@ private void PaintOverlays (PaintEventArgs e)
{
var currentContext = BufferedGraphicsManager.Current;
- using var myBuffer = currentContext.Allocate(CreateGraphics(), ClientRectangle);
+ using var myBuffer = currentContext.Allocate(e.Graphics, ClientRectangle);
lock (_overlayList)
{
_overlayList.Clear();
diff --git a/src/LogExpert.UI/Controls/LogWindow/LogExpertCallback.cs b/src/LogExpert.UI/Controls/LogWindow/LogExpertCallback.cs
index 22c6aa9c..0470ddf8 100644
--- a/src/LogExpert.UI/Controls/LogWindow/LogExpertCallback.cs
+++ b/src/LogExpert.UI/Controls/LogWindow/LogExpertCallback.cs
@@ -4,7 +4,7 @@
namespace LogExpert.UI.Controls.LogWindow;
-internal class LogExpertCallback (LogWindow logWindow) : ColumnizerCallback(logWindow), ILogExpertCallback
+internal class LogExpertCallback (LogWindow logWindow) : ColumnizerCallback(logWindow), ILogExpertCallbackMemory
{
#region Public methods
@@ -13,7 +13,7 @@ public void AddTempFileTab (string fileName, string title)
logWindow.AddTempFileTab(fileName, title);
}
- public void AddPipedTab (IList lineEntryList, string title)
+ public void AddPipedTab (IList lineEntryList, string title)
{
logWindow.WritePipeTab(lineEntryList, title);
}
diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
index 1d3fea78..a831ef96 100644
--- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
+++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
@@ -698,7 +698,7 @@ private void OnButtonSizeChanged (object sender, EventArgs e)
private delegate void PatternStatisticFx (PatternArgs patternArgs);
- private delegate void ActionPluginExecuteFx (string keyword, string param, ILogExpertCallback callback, ILogLineMemoryColumnizer columnizer);
+ private delegate void ActionPluginExecuteFx (string keyword, string param, ILogExpertCallbackMemory callback, ILogLineMemoryColumnizer columnizer);
private delegate void PositionAfterReloadFx (ReloadMemento reloadMemento);
@@ -735,11 +735,17 @@ void ILogWindow.AddTempFileTab (string fileName, string title)
}
[SupportedOSPlatform("windows")]
- void ILogWindow.WritePipeTab (IList lineEntryList, string title)
+ void ILogWindow.WritePipeTab (IList lineEntryList, string title)
{
WritePipeTab(lineEntryList, title);
}
+ [SupportedOSPlatform("windows")]
+ void ILogWindow.WritePipeTab (IList lineEntryList, string title)
+ {
+ //WritePipeTab(lineEntryList, title);
+ }
+
#region Event Handlers
[SupportedOSPlatform("windows")]
@@ -1472,7 +1478,7 @@ private void OnDataGridContextMenuStripOpening (object sender, CancelEventArgs e
foreach (var entry in PluginRegistry.PluginRegistry.Instance.RegisteredContextMenuPlugins)
{
LogExpertCallback callback = new(this);
- var menuText = entry.GetMenuText(lines.Count, CurrentColumnizer, callback.GetLogLine(lines[0]));
+ var menuText = entry.GetMenuText(lines.Count, CurrentColumnizer, callback.GetLogLineMemory(lines[0]));
if (menuText != null)
{
@@ -3164,7 +3170,7 @@ private void CheckFilterAndHighlight (LogEventArgs e)
for (var i = startLine; i < e.LineCount; ++i)
{
- var line = _logFileReader.GetLogLine(i);
+ var line = _logFileReader.GetLogLineMemory(i);
if (line != null)
{
var matchingList = FindMatchingHilightEntries(line);
@@ -3597,25 +3603,22 @@ private static IList MergeHighlightMatchEntries (IList
/// Returns the first HighlightEntry that matches the given line
///
- //TODO Replace with ItextvalueMemory
- private HighlightEntry FindHighlightEntry (ITextValue line)
+ private HighlightEntry FindHighlightEntry (ITextValueMemory line)
{
return FindHighlightEntry(line, false);
}
- //TODO Replace with ItextvalueMemory
- private HighlightEntry FindFirstNoWordMatchHighlightEntry (ITextValue line)
+ private HighlightEntry FindFirstNoWordMatchHighlightEntry (ITextValueMemory line)
{
return FindHighlightEntry(line, true);
}
- //TODO Replace with ItextvalueMemory
- private static bool CheckHighlightEntryMatch (HighlightEntry entry, ITextValue column)
+ private static bool CheckHighlightEntryMatch (HighlightEntry entry, ITextValueMemory column)
{
if (entry.IsRegex)
{
//Regex rex = new Regex(entry.SearchText, entry.IsCaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
- if (entry.Regex.IsMatch(column.Text))
+ if (entry.Regex.IsMatch(column.Text.ToString()))
{
return true;
}
@@ -3624,14 +3627,14 @@ private static bool CheckHighlightEntryMatch (HighlightEntry entry, ITextValue c
{
if (entry.IsCaseSensitive)
{
- if (column.Text.Contains(entry.SearchText, StringComparison.Ordinal))
+ if (column.Text.Span.Contains(entry.SearchText.AsSpan(), StringComparison.Ordinal))
{
return true;
}
}
else
{
- if (column.Text.ToUpperInvariant().Contains(entry.SearchText.ToUpperInvariant(), StringComparison.OrdinalIgnoreCase))
+ if (column.Text.Span.Contains(entry.SearchText.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -3644,8 +3647,7 @@ private static bool CheckHighlightEntryMatch (HighlightEntry entry, ITextValue c
///
/// Returns all HilightEntry entries which matches the given line
///
- //TODO Replace with ItextvalueMemory
- private IList FindMatchingHilightEntries (ITextValue line)
+ private IList FindMatchingHilightEntries (ITextValueMemory line)
{
IList resultList = [];
if (line != null)
@@ -3665,14 +3667,13 @@ private IList FindMatchingHilightEntries (ITextValue line)
return resultList;
}
- //TODO Replace with ItextvalueMemory
- private static void GetHighlightEntryMatches (ITextValue line, IList hilightEntryList, IList resultList)
+ private static void GetHighlightEntryMatches (ITextValueMemory line, IList hilightEntryList, IList resultList)
{
foreach (var entry in hilightEntryList)
{
if (entry.IsWordMatch)
{
- var matches = entry.Regex.Matches(line.Text);
+ var matches = entry.Regex.Matches(line.Text.ToString());
foreach (Match match in matches)
{
HighlightMatchEntry me = new()
@@ -4095,7 +4096,7 @@ private void SelectPrevHighlightLine ()
while (lineNum > 0)
{
lineNum--;
- var line = _logFileReader.GetLogLine(lineNum);
+ var line = _logFileReader.GetLogLineMemory(lineNum);
if (line != null)
{
var entry = FindHighlightEntry(line);
@@ -4115,7 +4116,7 @@ private void SelectNextHighlightLine ()
while (lineNum < _logFileReader.LineCount)
{
lineNum++;
- var line = _logFileReader.GetLogLine(lineNum);
+ var line = _logFileReader.GetLogLineMemory(lineNum);
if (line != null)
{
var entry = FindHighlightEntry(line);
@@ -5019,11 +5020,11 @@ private void WritePipeToTab (FilterPipe pipe, List lineNumberList, string n
break;
}
- var line = _logFileReader.GetLogLine(i);
- if (CurrentColumnizer is ILogLineXmlColumnizer)
+ var line = _logFileReader.GetLogLineMemory(i);
+ if (CurrentColumnizer is ILogLineMemoryXmlColumnizer)
{
callback.LineNum = i;
- line = (CurrentColumnizer as ILogLineXmlColumnizer).GetLineTextForClipboard(line, callback);
+ line = (CurrentColumnizer as ILogLineMemoryXmlColumnizer).GetLineTextForClipboard(line, callback);
}
_ = pipe.WriteToPipe(line, i);
@@ -5046,7 +5047,7 @@ private void WriteFilterToTabFinished (FilterPipe pipe, string name, Persistence
{
var title = name;
ILogLineMemoryColumnizer preProcessColumnizer = null;
- if (CurrentColumnizer is not ILogLineXmlColumnizer)
+ if (CurrentColumnizer is not ILogLineMemoryXmlColumnizer)
{
preProcessColumnizer = CurrentColumnizer;
}
@@ -5075,7 +5076,7 @@ private void WriteFilterToTabFinished (FilterPipe pipe, string name, Persistence
///
///
[SupportedOSPlatform("windows")]
- internal void WritePipeTab (IList lineEntryList, string title)
+ internal void WritePipeTab (IList lineEntryList, string title)
{
FilterPipe pipe = new(new FilterParams(), this)
{
@@ -5150,7 +5151,7 @@ private void ProcessFilterPipes (int lineNum)
pipe.LastLinesHistoryList.RemoveAt(0);
}
- var textLine = _logFileReader.GetLogLine(line);
+ var textLine = _logFileReader.GetLogLineMemory(line);
var fileOk = pipe.WriteToPipe(textLine, line);
if (!fileOk)
{
@@ -6557,44 +6558,6 @@ public void OnDataGridViewCellPainting (object sender, DataGridViewCellPaintingE
///
///
///
- public HighlightEntry FindHighlightEntry (ITextValue line, bool noWordMatches)
- {
- // first check the temp entries
- lock (_tempHighlightEntryListLock)
- {
- foreach (var entry in _tempHighlightEntryList)
- {
- if (noWordMatches && entry.IsWordMatch)
- {
- continue;
- }
-
- if (CheckHighlightEntryMatch(entry, line))
- {
- return entry;
- }
- }
- }
-
- lock (_currentHighlightGroupLock)
- {
- foreach (var entry in _currentHighlightGroup.HighlightEntryList)
- {
- if (noWordMatches && entry.IsWordMatch)
- {
- continue;
- }
-
- if (CheckHighlightEntryMatch(entry, line))
- {
- return entry;
- }
- }
-
- return null;
- }
- }
-
public HighlightEntry FindHighlightEntry (ITextValueMemory line, bool noWordMatches)
{
// first check the temp entries
@@ -6613,6 +6576,7 @@ public HighlightEntry FindHighlightEntry (ITextValueMemory line, bool noWordMatc
}
}
}
+
lock (_currentHighlightGroupLock)
{
foreach (var entry in _currentHighlightGroup.HighlightEntryList)
@@ -6632,26 +6596,6 @@ public HighlightEntry FindHighlightEntry (ITextValueMemory line, bool noWordMatc
}
}
- public IList FindHighlightMatches (ITextValue line)
- {
- IList resultList = [];
-
- if (line != null)
- {
- lock (_currentHighlightGroupLock)
- {
- GetHighlightEntryMatches(line, _currentHighlightGroup.HighlightEntryList, resultList);
- }
-
- lock (_tempHighlightEntryList)
- {
- GetHighlightEntryMatches(line, _tempHighlightEntryList, resultList);
- }
- }
-
- return resultList;
- }
-
public IList FindHighlightMatches (ITextValueMemory line)
{
IList resultList = [];
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
index 85fecd78..edffd6d0 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
@@ -23,6 +23,7 @@
using LogExpert.UI.Entities;
using LogExpert.UI.Extensions;
using LogExpert.UI.Extensions.LogWindow;
+using LogExpert.UI.Services;
using NLog;
@@ -38,46 +39,33 @@ internal partial class LogTabWindow : Form, ILogTabWindow
#region Fields
private const int MAX_COLUMNIZER_HISTORY = 40;
- private const int MAX_COLOR_HISTORY = 40;
+ //private const int MAX_COLOR_HISTORY = 40;
private const int DIFF_MAX = 100;
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
private readonly Icon _deadIcon;
+ private readonly ILedIndicatorService _ledService;
+ private readonly Lock _windowListLock = new();
+
+ private bool _disposed;
private readonly Color _defaultTabColor = Color.FromArgb(255, 192, 192, 192);
- private readonly Brush _dirtyLedBrush;
private readonly int _instanceNumber;
- private readonly Brush[] _ledBrushes = new Brush[5];
- private readonly Icon[,,,] _ledIcons = new Icon[6, 2, 4, 2];
-
- private readonly Rectangle[] _leds = new Rectangle[5];
private readonly IList _logWindowList = [];
- private readonly Brush _offLedBrush;
private readonly bool _showInstanceNumbers;
private readonly string[] _startupFileNames;
- private readonly EventWaitHandle _statusLineEventHandle = new AutoResetEvent(false);
- private readonly EventWaitHandle _statusLineEventWakeupHandle = new ManualResetEvent(false);
- private readonly Brush _syncLedBrush;
-
[SupportedOSPlatform("windows")]
private readonly StringFormat _tabStringFormat = new();
- private readonly Brush[] _tailLedBrush = new Brush[3];
-
private BookmarkWindow _bookmarkWindow;
private LogWindow.LogWindow _currentLogWindow;
private bool _firstBookmarkWindowShow = true;
- private Thread _ledThread;
-
- //Settings settings;
-
- private bool _shouldStop;
-
private bool _skipEvents;
private bool _wasMaximized;
@@ -115,31 +103,12 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
Rectangle led = new(0, 0, 8, 2);
- for (var i = 0; i < _leds.Length; ++i)
- {
- _leds[i] = led;
- led.Offset(0, led.Height + 0);
- }
-
- var grayAlpha = 50;
-
- _ledBrushes[0] = new SolidBrush(Color.FromArgb(255, 220, 0, 0));
- _ledBrushes[1] = new SolidBrush(Color.FromArgb(255, 220, 220, 0));
- _ledBrushes[2] = new SolidBrush(Color.FromArgb(255, 0, 220, 0));
- _ledBrushes[3] = new SolidBrush(Color.FromArgb(255, 0, 220, 0));
- _ledBrushes[4] = new SolidBrush(Color.FromArgb(255, 0, 220, 0));
+ _ledService = new LedIndicatorService();
+ _ledService.Initialize(ConfigManager.Settings.Preferences.ShowTailColor);
+ _ledService.IconChanged += OnLedIconChanged;
+ _ledService.Start();
- _offLedBrush = new SolidBrush(Color.FromArgb(grayAlpha, 160, 160, 160));
-
- _dirtyLedBrush = new SolidBrush(Color.FromArgb(255, 220, 0, 00));
-
- _tailLedBrush[0] = new SolidBrush(Color.FromArgb(255, 50, 100, 250)); // Follow tail: blue-ish
- _tailLedBrush[1] = new SolidBrush(Color.FromArgb(grayAlpha, 160, 160, 160)); // Don't follow tail: gray
- _tailLedBrush[2] = new SolidBrush(Color.FromArgb(255, 220, 220, 0)); // Stop follow tail (trigger): yellow-ish
-
- _syncLedBrush = new SolidBrush(Color.FromArgb(255, 250, 145, 30));
-
- CreateIcons();
+ _deadIcon = _ledService.GetDeadIcon();
_tabStringFormat.LineAlignment = StringAlignment.Center;
_tabStringFormat.Alignment = StringAlignment.Near;
@@ -167,9 +136,6 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
dragControlDateTime.Visible = false;
loadProgessBar.Visible = false;
- using var bmp = Resources.Deceased;
- _deadIcon = Icon.FromHandle(bmp.GetHicon());
-
FormClosing += OnLogTabWindowFormClosing;
InitToolWindows();
@@ -251,37 +217,6 @@ internal HighlightGroup FindHighlightGroup (string groupName)
#endregion
- private class LogWindowData
- {
- #region Fields
-
- // public MdiTabControl.TabPage tabPage;
-
- public Color Color { get; set; } = Color.FromKnownColor(KnownColor.Gray);
-
- public int DiffSum { get; set; }
-
- public bool Dirty { get; set; }
-
- // tailState:
- ///
- /// 0 = on
- /// 1 = off
- /// 2 = off by Trigger
- ///
- public int TailState { get; set; }
-
- public ToolTip ToolTip { get; set; }
-
- ///
- /// 0 = off
- /// 1 = timeSynced
- ///
- public int SyncMode { get; set; }
-
- #endregion
- }
-
#region Public methods
[SupportedOSPlatform("windows")]
@@ -721,7 +656,7 @@ public void ScrollAllTabsToTimestamp (DateTime timestamp, LogWindow.LogWindow se
{
if (logWindow.ScrollToTimestamp(timestamp, false, false))
{
- ShowLedPeak(logWindow);
+ _ledService.UpdateWindowActivity(logWindow, DIFF_MAX);
}
}
}
@@ -802,15 +737,15 @@ public void FollowTailChanged (LogWindow.LogWindow logWindow, bool isEnabled, bo
return;
}
- data.TailState = isEnabled
- ? 0
+ data.LedState.TailState = isEnabled
+ ? TailFollowState.On
: offByTrigger
- ? 2
- : 1;
+ ? TailFollowState.Paused
+ : TailFollowState.Off;
if (Preferences.ShowTailState)
{
- var icon = GetLedIcon(data.DiffSum, data);
+ var icon = GetLedIcon(data.LedState.DiffSum, data);
_ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWindow, icon);
}
}
@@ -842,6 +777,29 @@ public IList GetListOfOpenFiles ()
#region Private Methods
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose (bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing && (components != null))
+ {
+ _ledService?.Stop();
+ _ledService?.Dispose();
+ components.Dispose();
+ _tabStringFormat?.Dispose();
+ }
+
+ _disposed = true;
+ base.Dispose(disposing);
+ }
+
///
/// Creates a temp file with the text content of the clipboard and opens the temp file in a new tab.
///
@@ -999,12 +957,12 @@ private void AddLogWindow (LogWindow.LogWindow logWindow, string title, bool doN
LogWindowData data = new()
{
- DiffSum = 0
+ LedState = new LedState()
};
logWindow.Tag = data;
- lock (_logWindowList)
+ lock (_windowListLock)
{
_logWindowList.Add(logWindow);
}
@@ -1019,6 +977,8 @@ private void AddLogWindow (LogWindow.LogWindow logWindow, string title, bool doN
logWindow.SyncModeChanged += OnLogWindowSyncModeChanged;
logWindow.Visible = true;
+
+ _ledService.RegisterWindow(logWindow);
}
[SupportedOSPlatform("windows")]
@@ -1084,9 +1044,10 @@ private void FillHistoryMenu ()
[SupportedOSPlatform("windows")]
private void RemoveLogWindow (LogWindow.LogWindow logWindow)
{
- lock (_logWindowList)
+ lock (_windowListLock)
{
_ = _logWindowList.Remove(logWindow);
+ _ledService.UnregisterWindow(logWindow);
}
DisconnectEventHandlers(logWindow);
@@ -1473,103 +1434,6 @@ private void StatusLineEventWorker (StatusLineEventArgs e)
}
}
- // tailState: 0,1,2 = on/off/off by Trigger
- // syncMode: 0 = normal (no), 1 = time synced
- [SupportedOSPlatform("windows")]
- private Icon CreateLedIcon (int level, bool dirty, int tailState, int syncMode)
- {
- var iconRect = _leds[0];
- iconRect.Height = 16; // (DockPanel's damn hardcoded height) // this.leds[this.leds.Length - 1].Bottom;
- iconRect.Width = iconRect.Right + 6;
- Bitmap bmp = new(iconRect.Width, iconRect.Height);
- var gfx = Graphics.FromImage(bmp);
-
- var offsetFromTop = 4;
-
- for (var i = 0; i < _leds.Length; ++i)
- {
- var ledRect = _leds[i];
- ledRect.Offset(0, offsetFromTop);
-
- if (level >= _leds.Length - i)
- {
- gfx.FillRectangle(_ledBrushes[i], ledRect);
- }
- else
- {
- gfx.FillRectangle(_offLedBrush, ledRect);
- }
- }
-
- var ledSize = 3;
- var ledGap = 1;
- var lastLed = _leds[^1];
- Rectangle dirtyLed = new(lastLed.Right + 2, lastLed.Bottom - ledSize, ledSize, ledSize);
- Rectangle tailLed = new(dirtyLed.Location, dirtyLed.Size);
- tailLed.Offset(0, -(ledSize + ledGap));
- Rectangle syncLed = new(tailLed.Location, dirtyLed.Size);
- syncLed.Offset(0, -(ledSize + ledGap));
-
- syncLed.Offset(0, offsetFromTop);
- tailLed.Offset(0, offsetFromTop);
- dirtyLed.Offset(0, offsetFromTop);
-
- if (dirty)
- {
- gfx.FillRectangle(_dirtyLedBrush, dirtyLed);
- }
- else
- {
- gfx.FillRectangle(_offLedBrush, dirtyLed);
- }
-
- // tailMode 4 means: don't show
- if (tailState < 3)
- {
- gfx.FillRectangle(_tailLedBrush[tailState], tailLed);
- }
-
- if (syncMode == 1)
- {
- gfx.FillRectangle(_syncLedBrush, syncLed);
- }
- //else
- //{
- // gfx.FillRectangle(this.offLedBrush, syncLed);
- //}
-
- // see http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=345656
- // GetHicon() creates an unmanaged handle which must be destroyed. The Clone() workaround creates
- // a managed copy of icon. then the unmanaged win32 handle is destroyed
- var iconHandle = bmp.GetHicon();
- var icon = Icon.FromHandle(iconHandle).Clone() as Icon;
- _ = Vanara.PInvoke.User32.DestroyIcon(iconHandle);
-
- gfx.Dispose();
- bmp.Dispose();
- return icon;
- }
-
- [SupportedOSPlatform("windows")]
- private void CreateIcons ()
- {
- for (var syncMode = 0; syncMode <= 1; syncMode++) // LED indicating time synced tabs
- {
- for (var tailMode = 0; tailMode < 4; tailMode++)
- {
- for (var i = 0; i < 6; ++i)
- {
- _ledIcons[i, 0, tailMode, syncMode] = CreateLedIcon(i, false, tailMode, syncMode);
- }
-
- for (var i = 0; i < 6; ++i)
- {
- _ledIcons[i, 1, tailMode, syncMode] = CreateLedIcon(i, true, tailMode, syncMode);
- }
- }
- }
- }
-
[SupportedOSPlatform("windows")]
private void FileNotFound (LogWindow.LogWindow logWin)
{
@@ -1582,80 +1446,11 @@ private void FileNotFound (LogWindow.LogWindow logWin)
private void FileRespawned (LogWindow.LogWindow logWin)
{
var data = logWin.Tag as LogWindowData;
+ data.LedState.DiffSum = 0;
var icon = GetLedIcon(0, data);
_ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWin, icon);
}
- [SupportedOSPlatform("windows")]
- private void ShowLedPeak (LogWindow.LogWindow logWin)
- {
- var data = logWin.Tag as LogWindowData;
- lock (data)
- {
- data.DiffSum = DIFF_MAX;
- }
-
- var icon = GetLedIcon(data.DiffSum, data);
- _ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWin, icon);
- }
-
- private static int GetLevelFromDiff (int diff)
- {
- if (diff > 60)
- {
- diff = 60;
- }
-
- var level = diff / 10;
- if (diff > 0 && level == 0)
- {
- level = 2;
- }
- else if (level == 0)
- {
- level = 1;
- }
-
- return level - 1;
- }
-
- [SupportedOSPlatform("windows")]
- //TODO Task based
- private void LedThreadProc ()
- {
- Thread.CurrentThread.Name = "LED Thread";
- while (!_shouldStop)
- {
- try
- {
- Thread.Sleep(200);
- }
- catch
- {
- return;
- }
-
- lock (_logWindowList)
- {
- foreach (var logWindow in _logWindowList)
- {
- var data = logWindow.Tag as LogWindowData;
- if (data.DiffSum > 0)
- {
- data.DiffSum -= 10;
- if (data.DiffSum < 0)
- {
- data.DiffSum = 0;
- }
-
- var icon = GetLedIcon(data.DiffSum, data);
- _ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWindow, icon);
- }
- }
- }
- }
- }
-
[SupportedOSPlatform("windows")]
private void SetTabIcon (LogWindow.LogWindow logWindow, Icon icon)
{
@@ -1666,14 +1461,15 @@ private void SetTabIcon (LogWindow.LogWindow logWindow, Icon icon)
}
}
- private Icon GetLedIcon (int diff, LogWindowData data)
+ ///
+ /// Gets the appropriate LED icon based on the difference sum and LED state.
+ ///
+ /// The difference sum value used to determine the icon state.
+ /// The log window data containing the LED state information.
+ /// An representing the current LED state.
+ private Icon GetLedIcon (int diffSum, LogWindowData data)
{
- var icon =
- _ledIcons[
- GetLevelFromDiff(diff), data.Dirty ? 1 : 0, Preferences.ShowTailState ? data.TailState : 3,
- data.SyncMode
- ];
- return icon;
+ return _ledService.GetIcon(diffSum, data.LedState);
}
[SupportedOSPlatform("windows")]
@@ -1791,14 +1587,13 @@ private void ApplySettings (Settings settings, SettingsFlags flags)
[SupportedOSPlatform("windows")]
private void SetTabIcons (Preferences preferences)
{
- _tailLedBrush[0] = new SolidBrush(preferences.ShowTailColor);
- CreateIcons();
+ _ledService.RegenerateIcons(preferences.ShowTailColor);
lock (_logWindowList)
{
foreach (var logWindow in _logWindowList)
{
var data = logWindow.Tag as LogWindowData;
- var icon = GetLedIcon(data.DiffSum, data);
+ var icon = GetLedIcon(data.LedState.DiffSum, data);
_ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWindow, icon);
}
}
@@ -2254,12 +2049,6 @@ private void OnLogTabWindowLoad (object sender, EventArgs e)
LoadFiles(_startupFileNames, false);
}
- _ledThread = new Thread(LedThreadProc)
- {
- IsBackground = true
- };
- _ledThread.Start();
-
FillHighlightComboBox();
FillToolLauncherBar();
@@ -2273,11 +2062,6 @@ private void OnLogTabWindowFormClosing (object sender, CancelEventArgs e)
{
try
{
- _shouldStop = true;
- _ = _statusLineEventHandle.Set();
- _ = _statusLineEventWakeupHandle.Set();
- _ledThread.Join();
-
IList deleteLogWindowList = [];
ConfigManager.Settings.AlwaysOnTop = TopMost && ConfigManager.Settings.Preferences.AllowOnlyOneInstance;
SaveLastOpenFilesList();
@@ -2620,36 +2404,23 @@ private void OnAlwaysOnTopToolStripMenuItemClick (object sender, EventArgs e)
private void OnFileSizeChanged (object sender, LogEventArgs e)
{
- if (sender.GetType().IsAssignableFrom(typeof(LogWindow.LogWindow)))
+ if (sender is not LogWindow.LogWindow logWindow)
{
- var diff = e.LineCount - e.PrevLineCount;
- if (diff < 0)
- {
- return;
- }
-
- if (((LogWindow.LogWindow)sender).Tag is LogWindowData data)
- {
- lock (data)
- {
- data.DiffSum += diff;
- if (data.DiffSum > DIFF_MAX)
- {
- data.DiffSum = DIFF_MAX;
- }
- }
+ return;
+ }
- //if (this.dockPanel.ActiveContent != null &&
- // this.dockPanel.ActiveContent != sender || data.tailState != 0)
- if (CurrentLogWindow != null && CurrentLogWindow != sender || data.TailState != 0)
- {
- data.Dirty = true;
- }
+ if (logWindow.Tag is not LogWindowData data)
+ {
+ return;
+ }
- var icon = GetLedIcon(diff, data);
- _ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), (LogWindow.LogWindow)sender, icon);
- }
+ var diff = e.LineCount - e.PrevLineCount;
+ if (diff < 0)
+ {
+ return;
}
+
+ _ledService.UpdateWindowActivity(logWindow, diff);
}
private void OnLogWindowFileNotFound (object sender, EventArgs e)
@@ -2697,8 +2468,8 @@ private void OnTailFollowed (object sender, EventArgs e)
if (dockPanel.ActiveContent == sender)
{
var data = ((LogWindow.LogWindow)sender).Tag as LogWindowData;
- data.Dirty = false;
- var icon = GetLedIcon(data.DiffSum, data);
+ data.LedState.IsDirty = false;
+ var icon = GetLedIcon(data.LedState.DiffSum, data);
_ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), (LogWindow.LogWindow)sender, icon);
}
}
@@ -2710,14 +2481,13 @@ private void OnLogWindowSyncModeChanged (object sender, SyncModeEventArgs e)
if (!Disposing)
{
var data = ((LogWindow.LogWindow)sender).Tag as LogWindowData;
- data.SyncMode = e.IsTimeSynced ? 1 : 0;
- var icon = GetLedIcon(data.DiffSum, data);
+ data.LedState.SyncState = e.IsTimeSynced
+ ? TimeSyncState.Synced
+ : TimeSyncState.NotSynced;
+
+ var icon = GetLedIcon(data.LedState.DiffSum, data);
_ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), (LogWindow.LogWindow)sender, icon);
}
- //else
- //{
- // _logger.Warn("Received SyncModeChanged event while disposing. Event ignored.");
- //}
}
[SupportedOSPlatform("windows")]
@@ -3180,6 +2950,11 @@ private void OnThrowExceptionBackgroundThreadToolStripMenuItemClick (object send
thread.Start();
}
+ private void OnLedIconChanged (object sender, IconChangedEventArgs e)
+ {
+ SetTabIcon(e.Window, e.NewIcon);
+ }
+
private void OnWarnToolStripMenuItemClick (object sender, EventArgs e)
{
//_logger.GetLogger().LogLevel = _logger.Level.WARN;
@@ -3309,4 +3084,19 @@ private void OnTabRenameToolStripMenuItemClick (object sender, EventArgs e)
}
#endregion
+
+ private class LogWindowData
+ {
+ #region Fields
+
+ // public MdiTabControl.TabPage tabPage;
+
+ public LedState LedState { get; set; } = new();
+
+ public Color Color { get; set; } = Color.FromKnownColor(KnownColor.Gray);
+
+ public ToolTip ToolTip { get; set; }
+
+ #endregion
+ }
}
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs
index 19ed773d..f173f5c7 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs
@@ -11,20 +11,7 @@ partial class LogTabWindow
/// Required designer variable.
///
private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
+
#region Windows Form Designer generated code
///
diff --git a/src/LogExpert.UI/Entities/PaintHelper.cs b/src/LogExpert.UI/Entities/PaintHelper.cs
index a31f8d32..2993618f 100644
--- a/src/LogExpert.UI/Entities/PaintHelper.cs
+++ b/src/LogExpert.UI/Entities/PaintHelper.cs
@@ -330,10 +330,9 @@ private static void PaintCell (ILogPaintContextUI logPaintCtx, DataGridViewCellP
[SupportedOSPlatform("windows")]
private static void PaintHighlightedCell (ILogPaintContextUI logPaintCtx, DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry)
{
- //TODO Refactor if possible since Column is ITextValue
var value = e.Value ?? string.Empty;
- var matchList = logPaintCtx.FindHighlightMatches(value as ITextValue);
+ var matchList = logPaintCtx.FindHighlightMatches(value as ITextValueMemory);
// too many entries per line seem to cause problems with the GDI
while (matchList.Count > 50)
{
diff --git a/src/LogExpert.UI/Interface/ILogPaintContextUI.cs b/src/LogExpert.UI/Interface/ILogPaintContextUI.cs
index 349e1036..b95af50d 100644
--- a/src/LogExpert.UI/Interface/ILogPaintContextUI.cs
+++ b/src/LogExpert.UI/Interface/ILogPaintContextUI.cs
@@ -33,12 +33,8 @@ internal interface ILogPaintContextUI : ILogPaintContext
Bookmark GetBookmarkForLine (int lineNum);
- HighlightEntry FindHighlightEntry (ITextValue line, bool noWordMatches);
-
HighlightEntry FindHighlightEntry (ITextValueMemory line, bool noWordMatches);
- IList FindHighlightMatches (ITextValue line);
-
IList FindHighlightMatches (ITextValueMemory line);
#endregion
diff --git a/src/LogExpert.UI/Properties/AssemblyInfo.cs b/src/LogExpert.UI/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..635f1c8a
--- /dev/null
+++ b/src/LogExpert.UI/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("LogExpert.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100619e9beea345a3bb5e15f55b29ddf40d96e9bb473ae58304fc63dfb3e9c94d8944bb7e45324ee0bef3e345dccba79b0bf64b85a128a7f261861899add639218ddaeb2acc6fcc746d6acb5bb212d375a0967756af192cfdb6cf0bff666a0fe535600abda860d3eafaff4ef1c9b5710181f72d996ca9c29ed64bae4a5fd916dea5")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/LogExpert.UI/Services/ILedIndicatorService.cs b/src/LogExpert.UI/Services/ILedIndicatorService.cs
new file mode 100644
index 00000000..d6520a09
--- /dev/null
+++ b/src/LogExpert.UI/Services/ILedIndicatorService.cs
@@ -0,0 +1,79 @@
+using LogExpert.UI.Controls.LogWindow;
+
+namespace LogExpert.UI.Services;
+
+///
+/// Service for managing LED indicator icons on log window tabs
+///
+///
+/// This service is thread-safe and can be called from any thread.
+/// Icon updates are automatically marshaled to the UI thread via events.
+///
+internal interface ILedIndicatorService : IDisposable
+{
+ ///
+ /// Initializes the LED service with the specified tail color
+ ///
+ /// Color to use for the tail-follow indicator
+ /// Thrown if already initialized
+ void Initialize (Color tailColor);
+
+ ///
+ /// Gets the appropriate icon for the specified diff level and state
+ ///
+ /// Activity level (0-100)
+ /// Current LED state (dirty, tail, sync)
+ /// Icon representing the current state
+ Icon GetIcon (int diffLevel, LedState state);
+
+ ///
+ /// Gets the "dead" icon shown when a file is missing
+ ///
+ /// Dead file icon
+ Icon GetDeadIcon ();
+
+ ///
+ /// Starts the LED animation thread
+ ///
+ void Start ();
+
+ ///
+ /// Stops the LED animation thread
+ ///
+ void Stop ();
+
+ ///
+ /// Registers a window for LED state tracking
+ ///
+ /// LogWindow to track
+ void RegisterWindow (LogWindow window);
+
+ ///
+ /// Unregisters a window from LED state tracking
+ ///
+ /// LogWindow to stop tracking
+ void UnregisterWindow (LogWindow window);
+
+ ///
+ /// Updates the activity level for a window
+ ///
+ /// Window to update
+ /// Number of new lines added
+ void UpdateWindowActivity (LogWindow window, int lineDiff);
+
+ ///
+ /// Regenerates all icons with new color
+ ///
+ /// New tail color
+ void RegenerateIcons (Color tailColor);
+
+ ///
+ /// Gets the current tail color used for LED indicators
+ ///
+ Color CurrentTailColor { get; }
+
+ ///
+ /// Event fired when a window's icon should be updated
+ ///
+ event EventHandler IconChanged;
+}
diff --git a/src/LogExpert.UI/Services/IconChangedEventArgs.cs b/src/LogExpert.UI/Services/IconChangedEventArgs.cs
new file mode 100644
index 00000000..79962397
--- /dev/null
+++ b/src/LogExpert.UI/Services/IconChangedEventArgs.cs
@@ -0,0 +1,13 @@
+using LogExpert.UI.Controls.LogWindow;
+
+namespace LogExpert.UI.Services;
+
+///
+/// Event arguments for icon change notifications
+///
+internal class IconChangedEventArgs (LogWindow window, Icon newIcon) : EventArgs
+{
+ public LogWindow Window { get; } = window;
+
+ public Icon NewIcon { get; } = newIcon;
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Services/LedIndicatorService.cs b/src/LogExpert.UI/Services/LedIndicatorService.cs
new file mode 100644
index 00000000..ad241ed3
--- /dev/null
+++ b/src/LogExpert.UI/Services/LedIndicatorService.cs
@@ -0,0 +1,626 @@
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.Globalization;
+using System.Runtime.Versioning;
+
+using LogExpert.UI.Controls.LogWindow;
+
+using NLog;
+
+namespace LogExpert.UI.Services;
+
+[SupportedOSPlatform("windows")]
+internal sealed class LedIndicatorService : ILedIndicatorService, IDisposable
+{
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ // Constants
+ private const int DIFF_MAX = 100;
+ private const int LED_DECAY_RATE = 10;
+ private const int ANIMATION_INTERVAL_MS = 200;
+ private const int ICON_SIZE = 16;
+ private const int ICON_LEVELS = 6; // Activity levels 0-5
+ private const int ICON_DIRTY_STATES = 2; // Clean/Dirty
+ private const int ICON_TAIL_STATES = 4; // On/Off/Paused/Hidden
+ private const int ICON_SYNC_STATES = 2; // Not synced/Synced
+
+ // Icon cache: [level][dirty][tail][sync]
+ private Icon[][][][] _iconCache;
+ private Icon _deadIcon;
+
+ // Drawing resources
+ private readonly Rectangle[] _leds = new Rectangle[5];
+ private Brush[] _ledBrushes;
+ private Brush[] _tailLedBrush;
+ private Brush _offLedBrush;
+ private Brush _dirtyLedBrush;
+ private Brush _syncLedBrush;
+
+ // Window tracking
+ private readonly Dictionary _windowStates = [];
+ private readonly Lock _stateLock = new();
+
+ // Animation
+ private System.Windows.Forms.Timer _animationTimer;
+ private readonly SynchronizationContext _uiContext;
+ private bool _isInitialized;
+ private bool _disposed;
+
+ public event EventHandler IconChanged;
+
+ ///
+ /// Gets the current tail color used for LED indicators
+ ///
+ public Color CurrentTailColor
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(_disposed, nameof(LedIndicatorService));
+
+ return !_isInitialized
+ ? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceNotInitialized, nameof(LedIndicatorService)))
+ : (field);
+ }
+
+ private set;
+ }
+
+ public LedIndicatorService ()
+ {
+ _uiContext = SynchronizationContext.Current
+ ?? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceMustBeCreatedOnUIThread, nameof(LedIndicatorService)));
+
+ InitializeLedRectangles();
+ }
+
+ ///
+ /// Initializes LED rectangles for icon generation
+ ///
+ private void InitializeLedRectangles ()
+ {
+ _leds[0] = new Rectangle(0, 0, 3, 3);
+ _leds[1] = new Rectangle(3, 0, 3, 3);
+ _leds[2] = new Rectangle(6, 0, 3, 3);
+ _leds[3] = new Rectangle(9, 0, 3, 3);
+ _leds[4] = new Rectangle(12, 0, 3, 3);
+ }
+
+ ///
+ /// Disposes all resources
+ ///
+ public void Dispose ()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _logger.Info("Disposing LedIndicatorService");
+
+ Stop();
+
+ lock (_stateLock)
+ {
+ DisposeBrushes();
+ DisposeIcons();
+ _windowStates.Clear();
+ }
+
+ _disposed = true;
+ }
+
+ ///
+ /// Disposes all brushes
+ ///
+ private void DisposeBrushes ()
+ {
+ if (_ledBrushes != null)
+ {
+ foreach (var brush in _ledBrushes.Where(b => b != null))
+ {
+ brush.Dispose();
+ }
+
+ _ledBrushes = null;
+ }
+
+ if (_tailLedBrush != null)
+ {
+ foreach (var brush in _tailLedBrush.Where(b => b != null))
+ {
+ brush.Dispose();
+ }
+
+ _tailLedBrush = null;
+ }
+
+ _offLedBrush?.Dispose();
+ _offLedBrush = null;
+
+ _dirtyLedBrush?.Dispose();
+ _dirtyLedBrush = null;
+
+ _syncLedBrush?.Dispose();
+ _syncLedBrush = null;
+ }
+
+ ///
+ /// Disposes all icons
+ ///
+ private void DisposeIcons ()
+ {
+ if (_iconCache != null)
+ {
+ for (int level = 0; level < ICON_LEVELS; level++)
+ {
+ if (_iconCache[level] == null)
+ {
+ continue;
+ }
+
+ for (int dirty = 0; dirty < ICON_DIRTY_STATES; dirty++)
+ {
+ if (_iconCache[level][dirty] == null)
+ {
+ continue;
+ }
+
+ for (int tail = 0; tail < ICON_TAIL_STATES; tail++)
+ {
+ if (_iconCache[level][dirty][tail] == null)
+ {
+ continue;
+ }
+
+ for (int sync = 0; sync < ICON_SYNC_STATES; sync++)
+ {
+ _iconCache[level][dirty][tail][sync]?.Dispose();
+ _iconCache[level][dirty][tail][sync] = null;
+ }
+ }
+ }
+ }
+
+ _iconCache = null;
+ }
+
+ _deadIcon?.Dispose();
+ _deadIcon = null;
+ }
+
+ ///
+ /// Gets the dead icon
+ ///
+ public Icon GetDeadIcon ()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, nameof(LedIndicatorService));
+
+ return !_isInitialized
+ ? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceNotInitialized, nameof(LedIndicatorService)))
+ : _deadIcon;
+ }
+
+ ///
+ /// Gets the appropriate icon for the specified state
+ ///
+ public Icon GetIcon (int diffLevel, LedState state)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, nameof(LedIndicatorService));
+
+ if (!_isInitialized)
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceNotInitialized, nameof(LedIndicatorService)));
+ }
+
+ int level = GetLevelFromDiff(diffLevel);
+ int dirty = state.IsDirty ? 1 : 0;
+ int tail = (int)state.TailState;
+ int sync = (int)state.SyncState;
+
+ return _iconCache[level][dirty][tail][sync];
+ }
+
+ public void Initialize (Color tailColor)
+ {
+ if (_isInitialized)
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceIsAlreadyInitialized, nameof(LedIndicatorService)));
+ }
+
+ _logger.Info("Initializing LedIndicatorService with tail color: {Color}", tailColor);
+
+ CurrentTailColor = tailColor;
+
+ // Create brushes
+ CreateBrushes(tailColor);
+
+ // Generate all icons
+ GenerateIcons();
+
+ // Create dead icon
+ CreateDeadIcon();
+
+ _isInitialized = true;
+ _logger.Info("LedIndicatorService initialized successfully");
+ }
+
+ ///
+ /// Creates all brushes needed for icon generation
+ ///
+ private void CreateBrushes (Color tailColor)
+ {
+ _ledBrushes = new Brush[5];
+ _ledBrushes[0] = new SolidBrush(Color.FromArgb(255, 0, 0)); // Red (highest activity)
+ _ledBrushes[1] = new SolidBrush(Color.FromArgb(255, 100, 0)); // Orange
+ _ledBrushes[2] = new SolidBrush(Color.FromArgb(255, 190, 0)); // Yellow
+ _ledBrushes[3] = new SolidBrush(Color.FromArgb(190, 255, 0)); // Yellow-green
+ _ledBrushes[4] = new SolidBrush(Color.FromArgb(0, 255, 0)); // Green (lowest activity)
+
+ _tailLedBrush = new Brush[3];
+ _tailLedBrush[0] = new SolidBrush(tailColor); // Tail on
+ _tailLedBrush[1] = new SolidBrush(Color.FromArgb(160, 160, 160)); // Tail off
+ _tailLedBrush[2] = new SolidBrush(Color.FromArgb(220, 220, 0)); // Tail trigger
+
+ _offLedBrush = new SolidBrush(Color.FromArgb(100, 100, 100));
+ _dirtyLedBrush = new SolidBrush(Color.FromArgb(220, 0, 220)); // Magenta for dirty
+ _syncLedBrush = new SolidBrush(Color.FromArgb(0, 100, 220)); // Blue for synced
+ }
+
+ ///
+ /// Generates all possible icon combinations
+ ///
+ private void GenerateIcons ()
+ {
+ _logger.Debug("Generating LED icon cache");
+
+ // Initialize jagged array
+ _iconCache = new Icon[ICON_LEVELS][][][];
+
+ int iconCount = 0;
+ for (int level = 0; level < ICON_LEVELS; level++)
+ {
+ _iconCache[level] = new Icon[ICON_DIRTY_STATES][][];
+
+ for (int dirty = 0; dirty < ICON_DIRTY_STATES; dirty++)
+ {
+ _iconCache[level][dirty] = new Icon[ICON_TAIL_STATES][];
+
+ for (int tail = 0; tail < ICON_TAIL_STATES; tail++)
+ {
+ _iconCache[level][dirty][tail] = new Icon[ICON_SYNC_STATES];
+
+ for (int sync = 0; sync < ICON_SYNC_STATES; sync++)
+ {
+ _iconCache[level][dirty][tail][sync] = CreateLedIcon(
+ level,
+ dirty == 1,
+ (TailFollowState)tail,
+ (TimeSyncState)sync);
+ iconCount++;
+ }
+ }
+ }
+ }
+
+ _logger.Info("Generated {Count} LED icons", iconCount);
+ }
+
+ ///
+ /// Creates a single LED icon with the specified state
+ ///
+ /// Activity level (0-5)
+ /// Whether content has changed
+ /// Tail follow state
+ /// Time synchronization state
+ /// The generated icon
+ private Icon CreateLedIcon (int level, bool dirty, TailFollowState tailState, TimeSyncState syncState)
+ {
+ using var bmp = new Bitmap(ICON_SIZE, ICON_SIZE, PixelFormat.Format32bppArgb);
+ using (var g = Graphics.FromImage(bmp))
+ {
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ // Draw 5 LED segments
+ for (int i = 0; i < 5; i++)
+ {
+ Brush brush = i < level
+ ? _ledBrushes[i]
+ : _offLedBrush;
+ g.FillRectangle(brush, _leds[i]);
+ }
+
+ // Draw dirty indicator (top-right pixel)
+ if (dirty)
+ {
+ g.FillRectangle(_dirtyLedBrush, new Rectangle(13, 0, 3, 3));
+ }
+
+ // Draw tail indicator (bottom row) - hidden if state is Hidden
+ if (tailState != TailFollowState.Hidden)
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ g.FillRectangle(_tailLedBrush[(int)tailState], new Rectangle(i * 3, 13, 3, 3));
+ }
+ }
+
+ // Draw sync indicator (left side)
+ if (syncState == TimeSyncState.Synced)
+ {
+ g.FillRectangle(_syncLedBrush, new Rectangle(0, 4, 2, 9));
+ }
+ }
+
+ // Convert to icon
+ IntPtr hIcon = bmp.GetHicon();
+ try
+ {
+ var icon = (Icon)Icon.FromHandle(hIcon).Clone();
+ return icon;
+ }
+ finally
+ {
+ // Destroy native icon handle
+ _ = Vanara.PInvoke.User32.DestroyIcon(hIcon);
+ }
+ }
+
+ ///
+ /// Creates the "dead" icon for missing files
+ ///
+ private void CreateDeadIcon ()
+ {
+ using var bmp = new Bitmap(ICON_SIZE, ICON_SIZE, PixelFormat.Format32bppArgb);
+ using (var g = Graphics.FromImage(bmp))
+ {
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ // Draw X in red
+ using var pen = new Pen(Color.Red, 2);
+ g.DrawLine(pen, 2, 2, 14, 14);
+ g.DrawLine(pen, 14, 2, 2, 14);
+ }
+
+ IntPtr hIcon = bmp.GetHicon();
+ try
+ {
+ //using var bmp = Resources.Deceased;
+ _deadIcon = (Icon)Icon.FromHandle(hIcon).Clone();
+ }
+ finally
+ {
+ _ = Vanara.PInvoke.User32.DestroyIcon(hIcon);
+ }
+ }
+
+ ///
+ /// Calculates LED level from diff value
+ ///
+ private static int GetLevelFromDiff (int diff)
+ {
+ return diff < 1
+ ? 0
+ : diff < 10
+ ? 1
+ : diff < 20
+ ? 2
+ : diff < 40
+ ? 3
+ : diff < 80
+ ? 4
+ : 5;
+ }
+
+ ///
+ /// Regenerates all icons with new color
+ ///
+ public void RegenerateIcons (Color tailColor)
+ {
+ _logger.Info("Regenerating icons with new tail color: {Color}", tailColor);
+
+ lock (_stateLock)
+ {
+ // Dispose old resources
+ DisposeBrushes();
+ DisposeIcons();
+
+ // Create new ones
+ CurrentTailColor = tailColor;
+ CreateBrushes(tailColor);
+ GenerateIcons();
+ CreateDeadIcon();
+
+ // Update all windows
+ var updates = new List<(LogWindow window, Icon icon)>();
+ foreach (var kvp in _windowStates)
+ {
+ var icon = GetIcon(kvp.Value.DiffSum, kvp.Value);
+ updates.Add((kvp.Key, icon));
+ }
+
+ // Raise events outside lock on UI thread
+ foreach (var (window, icon) in updates)
+ {
+ OnIconChanged(window, icon);
+ }
+ }
+ }
+
+ ///
+ /// Registers a window for tracking
+ ///
+ public void RegisterWindow (LogWindow window)
+ {
+ ArgumentNullException.ThrowIfNull(window);
+
+ lock (_stateLock)
+ {
+ if (!_windowStates.ContainsKey(window))
+ {
+ _windowStates[window] = new LedState();
+ _logger.Debug("Registered window for LED tracking: {Window}", window.Text);
+ }
+ }
+ }
+
+ ///
+ /// Starts the LED animation timer
+ ///
+ public void Start ()
+ {
+ if (!_isInitialized)
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceNotInitialized, nameof(LedIndicatorService)));
+ }
+
+ if (_animationTimer != null)
+ {
+ _logger.Warn("Animation timer already started");
+ return;
+ }
+
+ _logger.Info("Starting LED animation timer");
+
+ _animationTimer = new System.Windows.Forms.Timer
+ {
+ Interval = ANIMATION_INTERVAL_MS
+ };
+
+ _animationTimer.Tick += OnAnimationTick;
+ _animationTimer.Start();
+ }
+
+ ///
+ /// Stops the LED animation timer
+ ///
+ public void Stop ()
+ {
+ if (_animationTimer == null)
+ {
+ return;
+ }
+
+ _logger.Info("Stopping LED animation timer");
+
+ _animationTimer.Stop();
+ _animationTimer.Tick -= OnAnimationTick;
+ _animationTimer.Dispose();
+ _animationTimer = null;
+ }
+
+ ///
+ /// Animation tick handler - decrements activity levels
+ ///
+ private void OnAnimationTick (object sender, EventArgs e)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ List<(LogWindow window, Icon icon)> updates = [];
+
+ lock (_stateLock)
+ {
+ foreach (var kvp in _windowStates.ToList())
+ {
+ var window = kvp.Key;
+ var state = kvp.Value;
+
+ if (state.DiffSum > 0)
+ {
+ state.DiffSum -= LED_DECAY_RATE;
+ if (state.DiffSum < 0)
+ {
+ state.DiffSum = 0;
+ }
+
+ var icon = GetIcon(state.DiffSum, state);
+ updates.Add((window, icon));
+ }
+ }
+ }
+
+ foreach (var (window, icon) in updates)
+ {
+ OnIconChanged(window, icon);
+ }
+ }
+
+ ///
+ /// Unregisters a window from tracking
+ ///
+ public void UnregisterWindow (LogWindow window)
+ {
+ if (window == null)
+ {
+ return;
+ }
+
+ lock (_stateLock)
+ {
+ if (_windowStates.Remove(window))
+ {
+ _logger.Debug("Unregistered window from LED tracking: {Window}", window.Text);
+ }
+ }
+ }
+
+ ///
+ /// Updates window activity level
+ ///
+ public void UpdateWindowActivity (LogWindow window, int lineDiff)
+ {
+ if (window == null || lineDiff < 0)
+ {
+ return;
+ }
+
+ Icon newIcon = null;
+
+ lock (_stateLock)
+ {
+ if (_windowStates.TryGetValue(window, out var state))
+ {
+ state.DiffSum += lineDiff;
+ if (state.DiffSum > DIFF_MAX)
+ {
+ state.DiffSum = DIFF_MAX;
+ }
+
+ newIcon = GetIcon(state.DiffSum, state);
+ }
+ }
+
+ if (newIcon != null)
+ {
+ OnIconChanged(window, newIcon);
+ }
+ }
+
+ ///
+ /// Raises the IconChanged event on the UI thread
+ ///
+ /// The window whose icon changed
+ /// The new icon
+ private void OnIconChanged (LogWindow window, Icon icon)
+ {
+ if (_disposed || IconChanged == null)
+ {
+ return;
+ }
+
+ var args = new IconChangedEventArgs(window, icon);
+
+ // Marshal to UI thread if needed
+ if (SynchronizationContext.Current != _uiContext)
+ {
+ _uiContext.Post(_ => IconChanged?.Invoke(this, args), null);
+ }
+ else
+ {
+ // Already on UI thread
+ IconChanged.Invoke(this, args);
+ }
+ }
+}
diff --git a/src/LogExpert.UI/Services/LedState.cs b/src/LogExpert.UI/Services/LedState.cs
new file mode 100644
index 00000000..14a648eb
--- /dev/null
+++ b/src/LogExpert.UI/Services/LedState.cs
@@ -0,0 +1,41 @@
+namespace LogExpert.UI.Services;
+
+///
+/// LED state information
+///
+public class LedState
+{
+ ///
+ /// Current activity level (0-100)
+ ///
+ public int DiffSum { get; set; }
+
+ ///
+ /// Whether the tab content has changed since last viewed
+ ///
+ public bool IsDirty { get; set; }
+
+ ///
+ /// Tail follow state
+ ///
+ public TailFollowState TailState { get; set; }
+
+ ///
+ /// Time synchronization state
+ ///
+ public TimeSyncState SyncState { get; set; }
+
+ ///
+ /// Creates a copy of this state
+ ///
+ public LedState Clone ()
+ {
+ return new LedState
+ {
+ DiffSum = DiffSum,
+ IsDirty = IsDirty,
+ TailState = TailState,
+ SyncState = SyncState
+ };
+ }
+}
diff --git a/src/LogExpert.UI/Services/TailFollowState.cs b/src/LogExpert.UI/Services/TailFollowState.cs
new file mode 100644
index 00000000..080d59c1
--- /dev/null
+++ b/src/LogExpert.UI/Services/TailFollowState.cs
@@ -0,0 +1,27 @@
+namespace LogExpert.UI.Services;
+
+///
+/// Represents the tail follow state for a log window
+///
+public enum TailFollowState
+{
+ ///
+ /// Tail following is active
+ ///
+ On = 0,
+
+ ///
+ /// Tail following is disabled
+ ///
+ Off = 1,
+
+ ///
+ /// Tail following is paused (e.g., by trigger)
+ ///
+ Paused = 2,
+
+ ///
+ /// Tail state indicator is hidden (not shown in icon)
+ ///
+ Hidden = 3
+}
diff --git a/src/LogExpert.UI/Services/TimeSyncState.cs b/src/LogExpert.UI/Services/TimeSyncState.cs
new file mode 100644
index 00000000..1ebdc147
--- /dev/null
+++ b/src/LogExpert.UI/Services/TimeSyncState.cs
@@ -0,0 +1,17 @@
+namespace LogExpert.UI.Services;
+
+///
+/// Represents the time synchronization state for a log window
+///
+public enum TimeSyncState
+{
+ ///
+ /// Time synchronization is not active (normal mode)
+ ///
+ NotSynced = 0,
+
+ ///
+ /// Time synchronization is active (synced with other windows)
+ ///
+ Synced = 1
+}
diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index 7a878dc0..2749eb7b 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: 2025-12-16 15:51:20 UTC
+ /// Generated: 2026-01-07 12:21:59 UTC
/// Configuration: Release
/// Plugin count: 22
///
@@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "FBFCB4C9FEBF8DA0DDB1822BDF7CFCADF1185574917D8705C597864D1ACBFE0C",
+ ["AutoColumnizer.dll"] = "5DAC7B256A2513C787AB6E86AA4AA1F1C4785C8F1F14DDAFE17DCF1E116ECBD1",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "672B48664FDBDB7E71A01AEE6704D34544257B6A977E01EC0A514FAEE030E982",
- ["CsvColumnizer.dll (x86)"] = "672B48664FDBDB7E71A01AEE6704D34544257B6A977E01EC0A514FAEE030E982",
- ["DefaultPlugins.dll"] = "53FA9690E55C7A1C90601CCCCA4FFC808BB783DFAA1FDF42EB924DF3CDB65D40",
- ["FlashIconHighlighter.dll"] = "AC101CC62538DB565C462D5A9A6EAA57D85AEA4954FAAEF271FEAC7F98FD1CB1",
- ["GlassfishColumnizer.dll"] = "915BDD327C531973F3355BC5B24AE70B0EA618269E9367DA4FA919B4BA08D171",
- ["JsonColumnizer.dll"] = "8A8B8021A4D146424947473A5FC2F0C07248BD4FAC079C00E586ADAA153CA9ED",
- ["JsonCompactColumnizer.dll"] = "6B26775A81C6FC4232C08BBAFDCE35A0EFA80A367FC00271DF00E89AF2F74802",
- ["Log4jXmlColumnizer.dll"] = "B645973F639B23A51D066DC283A92E3BDAF058934C6C6378B6F13247B75077CB",
- ["LogExpert.Core.dll"] = "A370D1AEF2A790D16622CD15C67270799A87555996A7A0D0A4D98B950B9C3CB7",
- ["LogExpert.Resources.dll"] = "1D761591625ED49FD0DE29ABDB2800D865BD99B359397CF7790004B3CC9E6738",
+ ["CsvColumnizer.dll"] = "B80D07374268B2B3E0EE0DF8347CFC4664ABE8E3E3C68A34A5814512E6F48ACD",
+ ["CsvColumnizer.dll (x86)"] = "B80D07374268B2B3E0EE0DF8347CFC4664ABE8E3E3C68A34A5814512E6F48ACD",
+ ["DefaultPlugins.dll"] = "EF3682CFB968FE6E3FFA4906E01335E6FC827BCF2DE161D1F3D6870138B0A819",
+ ["FlashIconHighlighter.dll"] = "C0A02F519C9E2AB6047DEFE6059BE2A5F8A3A419FA0E9FEF5E6AD4E677AA47AD",
+ ["GlassfishColumnizer.dll"] = "D8C77BA6D373D5200900110F86919B03182CB66A8B577E6724813FD53C23F017",
+ ["JsonColumnizer.dll"] = "A19E95BB92F0C98970A6B6CDC0C878A4B3E7B8A26F1E1AA19B05FE5CD12F5073",
+ ["JsonCompactColumnizer.dll"] = "2DC5FAB57725A6F87DC4CC7CD3400108116B5547979E6D594DD47367D7E1D183",
+ ["Log4jXmlColumnizer.dll"] = "ED382EACD39DF7097D4922A739B751CE85158F6EE75DA259B0FCAFADE1E043BA",
+ ["LogExpert.Core.dll"] = "A0C9CC6735C00A94D62467D7EE5183A2EE33BE758717B387396D2E7614F82C9C",
+ ["LogExpert.Resources.dll"] = "F4B788E1EDD4F0CFDB21232DEC3111FD3F5AC11E67A31D52AA463960F0B18DF0",
["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"] = "DD44507671520B3E2CD93311A651EFC7632C757FDA85750470A6FFCB74F1AB65",
- ["SftpFileSystem.dll"] = "16818A6B8B7178372CCB1CD6ED8C4B0542FDD7D10C8C9AAE76704F0ED2CC4362",
- ["SftpFileSystem.dll (x86)"] = "94B725554D2BBF79BBB0B4776C01AE0D1E8F06C158F5D234C2D94F5E62A900BC",
- ["SftpFileSystem.Resources.dll"] = "FEFD8C274032A70FDB03EA8363D7F877BEBAF2C454238820D6D17ECEF79FC8A4",
- ["SftpFileSystem.Resources.dll (x86)"] = "FEFD8C274032A70FDB03EA8363D7F877BEBAF2C454238820D6D17ECEF79FC8A4",
+ ["RegexColumnizer.dll"] = "1EBFD7BBE46F4C24A289D0690E57D7C83B2554FF7C73F6718D169BC2CFB51676",
+ ["SftpFileSystem.dll"] = "27138E267FD711D1C71C427958032BE3B9EB7ECBAC8E18B16349D2D8191ACBCB",
+ ["SftpFileSystem.dll (x86)"] = "6D90ED0359F5D8FADB243E40C69D0B065E6A7B654C1452D948968C89062859D8",
+ ["SftpFileSystem.Resources.dll"] = "BD74E37E1043811A19A00F312FAC93BFE163ABC45AF874ADA85206CE8F2B4045",
+ ["SftpFileSystem.Resources.dll (x86)"] = "BD74E37E1043811A19A00F312FAC93BFE163ABC45AF874ADA85206CE8F2B4045",
};
}