From 7b0b70f13065dfda16095ff8a0b63837a9faabf7 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 14 Jan 2026 15:21:57 +0100 Subject: [PATCH 1/9] tabcontroller --- src/ColumnizerLib/Extensions/Extensions.cs | 17 + .../Extensions/LogLineExtensions.cs | 16 - .../Classes/Log/LogfileReader.cs | 1 + .../Services/TabControllerTests.cs | 187 ++++++++ .../Dialogs/LogTabWindow/LogTabWindow.cs | 3 +- src/LogExpert.UI/Dialogs/SettingsDialog.cs | 1 - .../Extensions/ComboBoxExtensions.cs | 25 - src/LogExpert.UI/Extensions/FormExtensions.cs | 29 -- .../Extensions/LogexpertUIExtensions.cs | 55 +++ src/LogExpert.UI/Extensions/ResourceHelper.cs | 4 +- src/LogExpert.UI/Services/ITabController.cs | 55 +++ src/LogExpert.UI/Services/TabController.cs | 447 ++++++++++++++++++ .../Services/WindowActivatedEventArgs.cs | 10 + .../Services/WindowAddedEventArgs.cs | 10 + .../Services/WindowClosingEventArgs.cs | 13 + .../Services/WindowRemovedEventArgs.cs | 8 + 16 files changed, 805 insertions(+), 76 deletions(-) create mode 100644 src/ColumnizerLib/Extensions/Extensions.cs delete mode 100644 src/ColumnizerLib/Extensions/LogLineExtensions.cs create mode 100644 src/LogExpert.Tests/Services/TabControllerTests.cs delete mode 100644 src/LogExpert.UI/Extensions/ComboBoxExtensions.cs delete mode 100644 src/LogExpert.UI/Extensions/FormExtensions.cs create mode 100644 src/LogExpert.UI/Extensions/LogexpertUIExtensions.cs create mode 100644 src/LogExpert.UI/Services/ITabController.cs create mode 100644 src/LogExpert.UI/Services/TabController.cs create mode 100644 src/LogExpert.UI/Services/WindowActivatedEventArgs.cs create mode 100644 src/LogExpert.UI/Services/WindowAddedEventArgs.cs create mode 100644 src/LogExpert.UI/Services/WindowClosingEventArgs.cs create mode 100644 src/LogExpert.UI/Services/WindowRemovedEventArgs.cs diff --git a/src/ColumnizerLib/Extensions/Extensions.cs b/src/ColumnizerLib/Extensions/Extensions.cs new file mode 100644 index 000000000..f0071ee64 --- /dev/null +++ b/src/ColumnizerLib/Extensions/Extensions.cs @@ -0,0 +1,17 @@ +namespace ColumnizerLib.Extensions; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1708:Identifiers should differ by more than case", Justification = "Intentionally")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Intentionally")] +public static class Extensions +{ + extension(ILogLine logLine) + { + public string ToClipBoardText () => logLine == null ? string.Empty : $"\t{logLine.LineNumber + 1}\t{logLine.FullLine}"; + } + + extension(ILogLineMemory logLine) + { + public string ToClipBoardText () => logLine == null ? string.Empty : $"\t{logLine.LineNumber + 1}\t{logLine.FullLine}"; + + } +} \ No newline at end of file diff --git a/src/ColumnizerLib/Extensions/LogLineExtensions.cs b/src/ColumnizerLib/Extensions/LogLineExtensions.cs deleted file mode 100644 index 698d9e5f9..000000000 --- a/src/ColumnizerLib/Extensions/LogLineExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ColumnizerLib.Extensions; - -//TODO: Move this to LogExpert.UI, change to internal and fix tests -public static class LogLineExtensions -{ - //TOOD: check if the callers are checking for null before calling - public static string ToClipBoardText (this ILogLine logLine) - { - return logLine == null ? string.Empty : $"\t{logLine.LineNumber + 1}\t{logLine.FullLine}"; - } - - public static string ToClipBoardText (this ILogLineMemory logLine) - { - return logLine == null ? string.Empty : $"\t{logLine.LineNumber + 1}\t{logLine.FullLine}"; - } -} \ No newline at end of file diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index f8d64146f..1c3730655 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -531,6 +531,7 @@ public ILogLine GetLogLine (int lineNum) return GetLogLineInternal(lineNum).Result; } + //TODO Make Task Based public ILogLineMemory GetLogLineMemory (int lineNum) { return GetLogLineMemoryInternal(lineNum).Result; diff --git a/src/LogExpert.Tests/Services/TabControllerTests.cs b/src/LogExpert.Tests/Services/TabControllerTests.cs new file mode 100644 index 000000000..300310a70 --- /dev/null +++ b/src/LogExpert.Tests/Services/TabControllerTests.cs @@ -0,0 +1,187 @@ +using System.Runtime.Versioning; + +using LogExpert.UI.Controls.LogWindow; +using LogExpert.UI.Services; + +using Moq; + +using NUnit.Framework; + +using WeifenLuo.WinFormsUI.Docking; + +namespace LogExpert.Tests.Services; + +[TestFixture] +[SupportedOSPlatform("windows")] +internal class TabControllerTests +{ + private Mock _mockDockPanel; + private TabController _TabController; + + [SetUp] + public void Setup () + { + _mockDockPanel = new Mock(); + _TabController = new TabController(_mockDockPanel.Object); + } + + [TearDown] + public void TearDown () + { + _TabController?.Dispose(); + } + + // Window Management Tests + [Test] + public void AddWindow_WithValidWindow_AddsToTracking () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + + // Act + _TabController.AddWindow(mockWindow, "Test Window"); + + // Assert + Assert.That(_TabController.GetWindowCount(), Is.EqualTo(1)); + Assert.That(_TabController.HasWindow(mockWindow), Is.True); + } + + [Test] + public void AddWindow_SameWindowTwice_ThrowsException () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + _TabController.AddWindow(mockWindow, "Test Window"); + + // Act & Assert + _ = Assert.Throws(() => _TabController.AddWindow(mockWindow, "Test Window")); + } + + [Test] + public void RemoveWindow_ExistingWindow_RemovesFromTracking () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + _TabController.AddWindow(mockWindow, "Test Window"); + + // Act + _TabController.RemoveWindow(mockWindow); + + // Assert + Assert.That(_TabController.GetWindowCount(), Is.EqualTo(0)); + Assert.That(_TabController.HasWindow(mockWindow), Is.False); + } + + // Event Tests + [Test] + public void AddWindow_RaisesWindowAddedEvent () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + bool eventRaised = false; + LogWindow eventWindow = null; + + _TabController.WindowAdded += (s, e) => + { + eventRaised = true; + eventWindow = e.Window; + }; + + // Act + _TabController.AddWindow(mockWindow, "Test Window"); + + // Assert + Assert.That(eventRaised, Is.True); + Assert.That(eventWindow, Is.EqualTo(mockWindow)); + } + + // Window Finding Tests + [Test] + public void FindWindowByFileName_ExistingFile_ReturnsWindow () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + _TabController.AddWindow(mockWindow, "Test Window"); + + // Act + var found = _TabController.FindWindowByFileName("test.log"); + + // Assert + Assert.That(found, Is.EqualTo(mockWindow)); + } + + [Test] + public void FindWindowByFileName_CaseInsensitive_ReturnsWindow () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + _TabController.AddWindow(mockWindow, "Test Window"); + + // Act + var found = _TabController.FindWindowByFileName("TEST.LOG"); + + // Assert + Assert.That(found, Is.EqualTo(mockWindow)); + } + + // Tab Switching Tests + [Test] + public void SwitchToNextWindow_MultipleWindows_ActivatesNextWindow () + { + // Arrange + var window1 = CreateMockWindow("test1.log"); + var window2 = CreateMockWindow("test2.log"); + _TabController.AddWindow(window1, "Window 1"); + _TabController.AddWindow(window2, "Window 2"); + window1.Activate(); + + // Act + _TabController.SwitchToNextWindow(); + + // Assert + // Verify window2.Activate() was called + Mock.Get(window2).Verify(w => w.Activate(), Times.Once); + } + + // Thread Safety Tests + [Test] + public void AddWindow_ConcurrentCalls_AllWindowsTracked () + { + // Arrange + var windows = Enumerable.Range(0, 100) + .Select(i => CreateMockWindow($"test{i}.log")) + .ToList(); + + // Act + _ = Parallel.ForEach(windows, (window, state, index) => + { + _TabController.AddWindow(window, $"Window {index}"); + }); + + // Assert + Assert.That(_TabController.GetWindowCount(), Is.EqualTo(100)); + } + + // Disposal Tests + [Test] + public void Dispose_UnsubscribesFromAllEvents () + { + // Arrange + var mockWindow = CreateMockWindow("test.log"); + _TabController.AddWindow(mockWindow, "Test Window"); + + // Act + _TabController.Dispose(); + + // Assert + // Verify no event subscriptions remain + Mock.Get(mockWindow).VerifyRemove(w => w.Disposed -= It.IsAny()); + } + + private static LogWindow CreateMockWindow (string fileName) + { + var mock = new Mock(); + _ = mock.Setup(w => w.FileName).Returns(fileName); + return mock.Object; + } +} diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 3285cd191..b57f6a7a7 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -32,7 +32,6 @@ namespace LogExpert.UI.Controls.LogTabWindow; // Data shared over all LogTabWindow instances -//TODO: Can we get rid of this class? [SupportedOSPlatform("windows")] internal partial class LogTabWindow : Form, ILogTabWindow { @@ -44,7 +43,7 @@ internal partial class LogTabWindow : Form, ILogTabWindow private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly Icon _deadIcon; - private readonly ILedIndicatorService _ledService; + private readonly LedIndicatorService _ledService; private readonly Lock _windowListLock = new(); private bool _disposed; diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index d56b1e01c..3f3248729 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -10,7 +10,6 @@ using LogExpert.Core.Entities; using LogExpert.Core.Enums; using LogExpert.Core.Interface; -using LogExpert.Extensions; using LogExpert.UI.Controls.LogTabWindow; using LogExpert.UI.Dialogs; using LogExpert.UI.Extensions; diff --git a/src/LogExpert.UI/Extensions/ComboBoxExtensions.cs b/src/LogExpert.UI/Extensions/ComboBoxExtensions.cs deleted file mode 100644 index 9cd24a7a9..000000000 --- a/src/LogExpert.UI/Extensions/ComboBoxExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Runtime.Versioning; - -namespace LogExpert.UI.Extensions; - -[SupportedOSPlatform("windows")] -internal static class ComboBoxExtensions -{ - /// - public static int GetMaxTextWidth(this ComboBox comboBox) - { - var maxTextWidth = comboBox.Width; - - foreach (var item in comboBox.Items) - { - var textWidthInPixels = TextRenderer.MeasureText(item.ToString(), comboBox.Font).Width; - - if (textWidthInPixels > maxTextWidth) - { - maxTextWidth = textWidthInPixels; - } - } - - return maxTextWidth; - } -} diff --git a/src/LogExpert.UI/Extensions/FormExtensions.cs b/src/LogExpert.UI/Extensions/FormExtensions.cs deleted file mode 100644 index b4f4af879..000000000 --- a/src/LogExpert.UI/Extensions/FormExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Runtime.Versioning; - -namespace LogExpert.UI.Extensions; - -internal static class FormExtensions -{ - /// - /// Enumerates all controls within the specified parent control, including nested child controls. - /// - /// The parent control whose child controls are to be enumerated. Cannot be . - /// An of objects representing all controls within the parent, - /// including nested children. - [SupportedOSPlatform("windows")] - public static IEnumerable ControlsRecursive (this Control parent) - { - ArgumentNullException.ThrowIfNull(parent, nameof(parent)); - - foreach (Control control in parent.Controls) - { - yield return control; - - // recurse into children - foreach (var child in ControlsRecursive(control)) - { - yield return child; - } - } - } -} diff --git a/src/LogExpert.UI/Extensions/LogexpertUIExtensions.cs b/src/LogExpert.UI/Extensions/LogexpertUIExtensions.cs new file mode 100644 index 000000000..13b31df81 --- /dev/null +++ b/src/LogExpert.UI/Extensions/LogexpertUIExtensions.cs @@ -0,0 +1,55 @@ +using System.Runtime.Versioning; + +namespace LogExpert.UI.Extensions; + +[SupportedOSPlatform("windows")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1708:Identifiers should differ by more than case", Justification = "Intentionally")] +internal static class LogexpertUIExtensions +{ + /// + extension(ComboBox comboBox) + { + public int GetMaxTextWidth () + { + var maxTextWidth = comboBox.Width; + + foreach (var item in comboBox.Items) + { + var textWidthInPixels = TextRenderer.MeasureText(item.ToString(), comboBox.Font).Width; + + if (textWidthInPixels > maxTextWidth) + { + maxTextWidth = textWidthInPixels; + } + } + + return maxTextWidth; + } + } + + extension(Control parent) + { + /// + /// Enumerates all controls within the specified parent control, including nested child controls. + /// + /// The parent control whose child controls are to be enumerated. Cannot be . + /// An of objects representing all controls within the parent, + /// including nested children. + [SupportedOSPlatform("windows")] + public IEnumerable ControlsRecursive () + { + ArgumentNullException.ThrowIfNull(parent, nameof(parent)); + + foreach (Control control in parent.Controls) + { + yield return control; + + // recurse into children + foreach (var child in ControlsRecursive(control)) + { + yield return child; + } + } + } + } +} diff --git a/src/LogExpert.UI/Extensions/ResourceHelper.cs b/src/LogExpert.UI/Extensions/ResourceHelper.cs index f792a0e5a..060645c96 100644 --- a/src/LogExpert.UI/Extensions/ResourceHelper.cs +++ b/src/LogExpert.UI/Extensions/ResourceHelper.cs @@ -1,9 +1,7 @@ using System.Reflection; using System.Runtime.Versioning; -using LogExpert.UI.Extensions; - -namespace LogExpert.Extensions; +namespace LogExpert.UI.Extensions; internal static class ResourceHelper { diff --git a/src/LogExpert.UI/Services/ITabController.cs b/src/LogExpert.UI/Services/ITabController.cs new file mode 100644 index 000000000..265add796 --- /dev/null +++ b/src/LogExpert.UI/Services/ITabController.cs @@ -0,0 +1,55 @@ +using LogExpert.UI.Controls.LogWindow; + +using WeifenLuo.WinFormsUI.Docking; + +namespace LogExpert.UI.Services; + +internal interface ITabController : IDisposable +{ + /// + /// + /// + /// + /// + /// + void AddWindow (LogWindow window, string title, bool doNotAddToDockPanel = false); + + /// + /// + /// + /// + void RemoveWindow (LogWindow window); + + void CloseWindow (LogWindow window, bool skipConfirmation = false); + + void CloseAllWindows (); + + void CloseAllExcept (LogWindow window); + + // Window Activation + void ActivateWindow (LogWindow window); + + LogWindow GetActiveWindow (); + + void SwitchToNextWindow (); + + void SwitchToPreviousWindow (); + + // Window Queries + LogWindow FindWindowByFileName (string fileName); + + IReadOnlyList GetAllWindows (); + + int GetWindowCount (); + + bool HasWindow (LogWindow window); + + // DockPanel Integration + void InitializeDockPanel (DockPanel dockPanel); + + // Events + event EventHandler WindowAdded; + event EventHandler WindowRemoved; + event EventHandler WindowActivated; + event EventHandler WindowClosing; +} diff --git a/src/LogExpert.UI/Services/TabController.cs b/src/LogExpert.UI/Services/TabController.cs new file mode 100644 index 000000000..cf1e66cc9 --- /dev/null +++ b/src/LogExpert.UI/Services/TabController.cs @@ -0,0 +1,447 @@ +using System.Runtime.Versioning; + +using LogExpert.UI.Controls.LogWindow; +using LogExpert.UI.Entities; + +using WeifenLuo.WinFormsUI.Docking; + +namespace LogExpert.UI.Services; + +[SupportedOSPlatform("windows")] +internal class TabController : ITabController +{ + private DockPanel _dockPanel; + private readonly Dictionary _windows; + private readonly Lock _windowsLock = new(); + private LogWindow _activeWindow; + private bool _disposed; + private bool _initialized; + + public event EventHandler WindowAdded; + public event EventHandler WindowRemoved; + public event EventHandler WindowActivated; + public event EventHandler WindowClosing; + + /// + /// Creates a new TabController instance + /// + /// The DockPanel to manage tabs in + public TabController (DockPanel dockPanel) + { + _dockPanel = dockPanel ?? throw new ArgumentNullException(nameof(dockPanel)); + _windows = []; + _initialized = true; + + // Subscribe to DockPanel events + _dockPanel.ActiveContentChanged += OnDockPanelActiveContentChanged; + } + + /// + /// Creates a new TabController instance without a DockPanel + /// Use InitializeDockPanel to set the DockPanel later + /// + public TabController () + { + _windows = []; + _initialized = false; + } + + #region DockPanel Integration + + /// + /// Initializes the TabController with a DockPanel + /// Use this when the DockPanel is not available at construction time + /// + /// The DockPanel to manage tabs in + /// If dockPanel is null + /// If already initialized + public void InitializeDockPanel (DockPanel dockPanel) + { + ArgumentNullException.ThrowIfNull(dockPanel, nameof(dockPanel)); + + if (_initialized) + { + throw new InvalidOperationException("TabController is already initialized with a DockPanel"); + } + + _dockPanel = dockPanel; + _dockPanel.ActiveContentChanged += OnDockPanelActiveContentChanged; + _initialized = true; + } + + private void OnDockPanelActiveContentChanged (object sender, EventArgs e) + { + if (_dockPanel.ActiveContent is LogWindow newWindow) + { + var previousWindow = _activeWindow; + _activeWindow = newWindow; + + WindowActivated?.Invoke(this, new WindowActivatedEventArgs(newWindow, previousWindow)); + } + } + + #endregion + + #region Window Management + + /// + /// Adds a new LogWindow to the tab system + /// + /// Window to add + /// Tab title + /// Skip adding to DockPanel (for deferred loading) + /// If window is null + /// If window already tracked or not initialized + public void AddWindow (LogWindow window, string title, bool doNotAddToDockPanel = false) + { + ArgumentNullException.ThrowIfNull(window, nameof(window)); + + if (!_initialized) + { + throw new InvalidOperationException("TabController is not initialized. Call InitializeDockPanel first."); + } + + lock (_windowsLock) + { + if (_windows.ContainsKey(window)) + { + throw new InvalidOperationException("Window already tracked"); + } + + var metadata = new LogWindowMetadata + { + Window = window, + Title = title, + FileName = window.FileName, + CreatedAt = DateTime.Now, + IsTempFile = window.IsTempFile, + TabColor = Color.Gray + }; + + _windows.Add(window, metadata); + } + + if (!doNotAddToDockPanel) + { + window.Show(_dockPanel); + } + + // Subscribe to window events + window.Disposed += OnWindowDisposed; + window.Activated += OnWindowActivated; + + WindowAdded?.Invoke(this, new WindowAddedEventArgs(window, title)); + } + + /// + /// Removes a window from tracking (does not close it) + /// + /// Window to remove + public void RemoveWindow (LogWindow window) + { + if (window == null) + { + return; + } + + lock (_windowsLock) + { + if (!_windows.Remove(window)) + { + return; + } + } + + window.Disposed -= OnWindowDisposed; + window.Activated -= OnWindowActivated; + + if (_activeWindow == window) + { + _activeWindow = null; + } + + WindowRemoved?.Invoke(this, new WindowRemovedEventArgs(window)); + } + + /// + /// Closes a window with optional confirmation + /// + /// Window to close + /// Skip user confirmation dialog + public void CloseWindow (LogWindow window, bool skipConfirmation = false) + { + if (window == null) + { + return; + } + + var windowClosingEventArgs = new WindowClosingEventArgs(window, skipConfirmation); + WindowClosing?.Invoke(this, windowClosingEventArgs); + + if (windowClosingEventArgs.Cancel) + { + return; + } + + window.Close(skipConfirmation); + // Note: RemoveWindow will be called by OnWindowDisposed event handler + } + + /// + /// Closes all tracked windows + /// + public void CloseAllWindows () + { + // Create a copy to avoid collection modification during iteration + var windowsToClose = GetAllWindows(); + + foreach (var window in windowsToClose) + { + CloseWindow(window, skipConfirmation: true); + } + } + + /// + /// Closes all windows except the specified one + /// + /// Window to keep open + public void CloseAllExcept (LogWindow window) + { + var windowsToClose = GetAllWindows() + .Where(w => w != window) + .ToList(); + + foreach (var win in windowsToClose) + { + CloseWindow(win, skipConfirmation: false); + } + } + + #endregion + + #region Window Activation + + /// + /// Activates (brings to front) the specified window + /// + /// Window to activate + public void ActivateWindow (LogWindow window) + { + if (window == null) + { + return; + } + + lock (_windowsLock) + { + if (!_windows.ContainsKey(window)) + { + return; // Window not tracked + } + } + + // Activate the window - this will trigger OnDockPanelActiveContentChanged + window.Activate(); + } + + /// + /// Gets the currently active window + /// + /// The active LogWindow, or null if none is active + public LogWindow GetActiveWindow () + { + return _activeWindow; + } + + /// + /// Switches to the next window in the tab order (Ctrl+Tab behavior) + /// + public void SwitchToNextWindow () + { + lock (_windowsLock) + { + if (_windows.Count == 0) + { + return; + } + + var windows = _windows.Keys.ToList(); + var currentIndex = _activeWindow != null + ? windows.IndexOf(_activeWindow) + : -1; + + // Move forward, wrap around to beginning if at end + var nextIndex = (currentIndex + 1) % windows.Count; + + windows[nextIndex].Activate(); + } + } + + /// + /// Switches to the previous window in the tab order (Ctrl+Shift+Tab behavior) + /// + public void SwitchToPreviousWindow () + { + lock (_windowsLock) + { + if (_windows.Count == 0) + { + return; + } + + var windows = _windows.Keys.ToList(); + var currentIndex = _activeWindow != null + ? windows.IndexOf(_activeWindow) + : 0; + + // Move backward, wrap around to end if at beginning + var previousIndex = currentIndex - 1; + if (previousIndex < 0) + { + previousIndex = windows.Count - 1; + } + + windows[previousIndex].Activate(); + } + } + + /// + /// Event handler for when a window is activated directly (not via DockPanel) + /// + private void OnWindowActivated (object sender, EventArgs e) + { + if (sender is LogWindow window) + { + var previousWindow = _activeWindow; + + // Only update and raise event if the window actually changed + if (_activeWindow != window) + { + _activeWindow = window; + WindowActivated?.Invoke(this, new WindowActivatedEventArgs(window, previousWindow)); + } + } + } + + #endregion + + #region Window Queries + + /// + /// Finds a window by its file name (case-insensitive) + /// + /// File name to search for + /// The matching LogWindow, or null if not found + public LogWindow FindWindowByFileName (string fileName) + { + if (string.IsNullOrEmpty(fileName)) + { + return null; + } + + lock (_windowsLock) + { + return _windows + .Where(kvp => kvp.Value.FileName.Equals(fileName, StringComparison.OrdinalIgnoreCase)) + .Select(kvp => kvp.Key) + .FirstOrDefault(); + } + } + + /// + /// Gets all tracked windows as a read-only list + /// + /// Read-only list of all LogWindows + public IReadOnlyList GetAllWindows () + { + lock (_windowsLock) + { + return _windows.Keys.ToList().AsReadOnly(); + } + } + + /// + /// Gets the count of tracked windows + /// + /// Number of tracked windows + public int GetWindowCount () + { + lock (_windowsLock) + { + return _windows.Count; + } + } + + /// + /// Checks if a window is currently being tracked + /// + /// Window to check + /// True if window is tracked, false otherwise + public bool HasWindow (LogWindow window) + { + if (window == null) + { + return false; + } + + lock (_windowsLock) + { + return _windows.ContainsKey(window); + } + } + + #endregion + + #region Event Handlers + + private void OnWindowDisposed (object sender, EventArgs e) + { + if (sender is LogWindow window) + { + RemoveWindow(window); + } + } + + #endregion + + #region Disposal + + public void Dispose () + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose (bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // Unsubscribe from DockPanel + if (_dockPanel != null) + { + _dockPanel.ActiveContentChanged -= OnDockPanelActiveContentChanged; + } + + // Unsubscribe from all windows + lock (_windowsLock) + { + foreach (var window in _windows.Keys) + { + window.Disposed -= OnWindowDisposed; + window.Activated -= OnWindowActivated; + } + + _windows.Clear(); + } + } + + _disposed = true; + } + + #endregion +} diff --git a/src/LogExpert.UI/Services/WindowActivatedEventArgs.cs b/src/LogExpert.UI/Services/WindowActivatedEventArgs.cs new file mode 100644 index 000000000..806f99cb2 --- /dev/null +++ b/src/LogExpert.UI/Services/WindowActivatedEventArgs.cs @@ -0,0 +1,10 @@ +using LogExpert.UI.Controls.LogWindow; + +namespace LogExpert.UI.Services; + +internal class WindowActivatedEventArgs (LogWindow window, LogWindow previousWindow) : EventArgs +{ + public LogWindow Window { get; } = window; + + public LogWindow PreviousWindow { get; } = previousWindow; +} diff --git a/src/LogExpert.UI/Services/WindowAddedEventArgs.cs b/src/LogExpert.UI/Services/WindowAddedEventArgs.cs new file mode 100644 index 000000000..4277efe70 --- /dev/null +++ b/src/LogExpert.UI/Services/WindowAddedEventArgs.cs @@ -0,0 +1,10 @@ +using LogExpert.UI.Controls.LogWindow; + +namespace LogExpert.UI.Services; + +internal class WindowAddedEventArgs (LogWindow window, string title) : EventArgs +{ + public LogWindow Window { get; } = window; + + public string Title { get; } = title; +} diff --git a/src/LogExpert.UI/Services/WindowClosingEventArgs.cs b/src/LogExpert.UI/Services/WindowClosingEventArgs.cs new file mode 100644 index 000000000..df073838b --- /dev/null +++ b/src/LogExpert.UI/Services/WindowClosingEventArgs.cs @@ -0,0 +1,13 @@ + +using System.ComponentModel; + +using LogExpert.UI.Controls.LogWindow; + +namespace LogExpert.UI.Services; + +internal class WindowClosingEventArgs (LogWindow window, bool skipConfirmation) : CancelEventArgs +{ + public LogWindow Window { get; } = window; + + public bool SkipConfirmation { get; } = skipConfirmation; +} diff --git a/src/LogExpert.UI/Services/WindowRemovedEventArgs.cs b/src/LogExpert.UI/Services/WindowRemovedEventArgs.cs new file mode 100644 index 000000000..305d4a8d1 --- /dev/null +++ b/src/LogExpert.UI/Services/WindowRemovedEventArgs.cs @@ -0,0 +1,8 @@ +using LogExpert.UI.Controls.LogWindow; + +namespace LogExpert.UI.Services; + +internal class WindowRemovedEventArgs (LogWindow window) : EventArgs +{ + public LogWindow Window { get; } = window; +} From fb6a68e909c343c82eff39eb3c1b6cd424b48758 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Fri, 16 Jan 2026 13:05:14 +0100 Subject: [PATCH 2/9] update tabcontroller --- src/LogExpert.Core/Interface/ILogView.cs | 9 +- .../Services/TabControllerTests.cs | 425 ++++++++++++++---- .../Controls/LogWindow/LogWindow.cs | 2 + .../Dialogs/LogTabWindow/LogTabWindow.cs | 368 ++++++++------- .../Entities/LogWindowMetadata.cs | 20 + src/LogExpert.UI/Services/ITabController.cs | 24 +- src/LogExpert.UI/Services/TabController.cs | 14 + 7 files changed, 607 insertions(+), 255 deletions(-) create mode 100644 src/LogExpert.UI/Entities/LogWindowMetadata.cs diff --git a/src/LogExpert.Core/Interface/ILogView.cs b/src/LogExpert.Core/Interface/ILogView.cs index 5dbc0e80c..8427d0f14 100644 --- a/src/LogExpert.Core/Interface/ILogView.cs +++ b/src/LogExpert.Core/Interface/ILogView.cs @@ -10,16 +10,17 @@ public interface ILogView #region Properties ILogLineMemoryColumnizer CurrentColumnizer { get; } + string FileName { get; } #endregion #region Public methods - void SelectLogLine(int lineNumber); - void SelectAndEnsureVisible(int line, bool triggerSyncCall); - void RefreshLogView(); - void DeleteBookmarks(List lineNumList); + void SelectLogLine (int lineNumber); + void SelectAndEnsureVisible (int line, bool triggerSyncCall); + void RefreshLogView (); + void DeleteBookmarks (List lineNumList); #endregion } \ No newline at end of file diff --git a/src/LogExpert.Tests/Services/TabControllerTests.cs b/src/LogExpert.Tests/Services/TabControllerTests.cs index 300310a70..13e65d4b8 100644 --- a/src/LogExpert.Tests/Services/TabControllerTests.cs +++ b/src/LogExpert.Tests/Services/TabControllerTests.cs @@ -1,187 +1,436 @@ using System.Runtime.Versioning; +using System.Windows.Forms; using LogExpert.UI.Controls.LogWindow; using LogExpert.UI.Services; -using Moq; - using NUnit.Framework; using WeifenLuo.WinFormsUI.Docking; namespace LogExpert.Tests.Services; +/// +/// Unit tests for TabController. +/// +/// Note: Many tests are limited because LogWindow is a complex WinForms control +/// that cannot be easily mocked or subclassed. Tests that require actual LogWindow +/// instances would need to be run as integration tests with full UI infrastructure. +/// +/// These tests focus on the core TabController functionality that can be tested +/// without instantiating LogWindow objects. +/// [TestFixture] [SupportedOSPlatform("windows")] -internal class TabControllerTests +[Apartment(ApartmentState.STA)] // Required for WinForms controls +internal class TabControllerTests : IDisposable { - private Mock _mockDockPanel; - private TabController _TabController; + private Form _testForm; + private DockPanel _dockPanel; + private TabController _tabController; + private bool _disposed; [SetUp] - public void Setup () + public void Setup() { - _mockDockPanel = new Mock(); - _TabController = new TabController(_mockDockPanel.Object); + // Create a real Form and DockPanel for testing + // This is necessary because DockPanel requires WinForms infrastructure + _testForm = new Form(); + _dockPanel = new DockPanel + { + Dock = DockStyle.Fill, + DocumentStyle = DocumentStyle.DockingMdi + }; + _testForm.Controls.Add(_dockPanel); + _testForm.Show(); // Must show form for DockPanel to work + + _tabController = new TabController(_dockPanel); } [TearDown] - public void TearDown () + public void TearDown() + { + _tabController?.Dispose(); + _testForm?.Close(); + _testForm?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { - _TabController?.Dispose(); + if (_disposed) + { + return; + } + + if (disposing) + { + _tabController?.Dispose(); + _testForm?.Dispose(); + } + + _disposed = true; } - // Window Management Tests + #region Constructor Tests + [Test] - public void AddWindow_WithValidWindow_AddsToTracking () + public void Constructor_WithDockPanel_InitializesSuccessfully() + { + // Arrange & Act - already done in Setup + + // Assert + Assert.That(_tabController, Is.Not.Null); + Assert.That(_tabController.GetWindowCount(), Is.EqualTo(0)); + } + + [Test] + public void Constructor_WithNullDockPanel_ThrowsArgumentNullException() + { + // Arrange & Act & Assert + Assert.Throws(() => new TabController(null)); + } + + [Test] + public void Constructor_WithoutDockPanel_CreatesUninitializedController() + { + // Arrange & Act + using var controller = new TabController(); + + // Assert - controller should be created but not initialized + // Calling GetWindowCount should still work (returns 0) + Assert.That(controller.GetWindowCount(), Is.EqualTo(0)); + } + + #endregion + + #region InitializeDockPanel Tests + + [Test] + public void InitializeDockPanel_WithValidDockPanel_Succeeds() { // Arrange - var mockWindow = CreateMockWindow("test.log"); + using var controller = new TabController(); // Act - _TabController.AddWindow(mockWindow, "Test Window"); + controller.InitializeDockPanel(_dockPanel); - // Assert - Assert.That(_TabController.GetWindowCount(), Is.EqualTo(1)); - Assert.That(_TabController.HasWindow(mockWindow), Is.True); + // Assert - no exception thrown, and controller should work + Assert.That(controller.GetWindowCount(), Is.EqualTo(0)); + } + + [Test] + public void InitializeDockPanel_WithNullDockPanel_ThrowsArgumentNullException() + { + // Arrange + using var controller = new TabController(); + + // Act & Assert + Assert.Throws(() => controller.InitializeDockPanel(null)); } [Test] - public void AddWindow_SameWindowTwice_ThrowsException () + public void InitializeDockPanel_WhenAlreadyInitialized_ThrowsInvalidOperationException() { // Arrange - var mockWindow = CreateMockWindow("test.log"); - _TabController.AddWindow(mockWindow, "Test Window"); + using var controller = new TabController(_dockPanel); + + // Create a new form and dock panel for the second initialization attempt + using var form2 = new Form(); + using var dockPanel2 = new DockPanel(); + form2.Controls.Add(dockPanel2); // Act & Assert - _ = Assert.Throws(() => _TabController.AddWindow(mockWindow, "Test Window")); + Assert.Throws(() => controller.InitializeDockPanel(dockPanel2)); } + #endregion + + #region GetAllWindowsFromDockPanel Tests + [Test] - public void RemoveWindow_ExistingWindow_RemovesFromTracking () + public void GetAllWindowsFromDockPanel_WhenNotInitialized_ReturnsEmptyList() { // Arrange - var mockWindow = CreateMockWindow("test.log"); - _TabController.AddWindow(mockWindow, "Test Window"); + using var controller = new TabController(); // Act - _TabController.RemoveWindow(mockWindow); + var result = controller.GetAllWindowsFromDockPanel(); // Assert - Assert.That(_TabController.GetWindowCount(), Is.EqualTo(0)); - Assert.That(_TabController.HasWindow(mockWindow), Is.False); + Assert.That(result, Is.Empty); } - // Event Tests [Test] - public void AddWindow_RaisesWindowAddedEvent () + public void GetAllWindowsFromDockPanel_WhenInitializedButEmpty_ReturnsEmptyList() { - // Arrange - var mockWindow = CreateMockWindow("test.log"); - bool eventRaised = false; - LogWindow eventWindow = null; + // Arrange - already done in Setup - _TabController.WindowAdded += (s, e) => - { - eventRaised = true; - eventWindow = e.Window; - }; + // Act + var result = _tabController.GetAllWindowsFromDockPanel(); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public void GetAllWindowsFromDockPanel_ReturnsReadOnlyList() + { + // Arrange - already done in Setup // Act - _TabController.AddWindow(mockWindow, "Test Window"); + var result = _tabController.GetAllWindowsFromDockPanel(); + + // Assert - ReadOnlyCollection implements IReadOnlyList + Assert.That(result, Is.InstanceOf>()); + } + + #endregion + + #region GetAllWindows Tests + + [Test] + public void GetAllWindows_WhenEmpty_ReturnsEmptyList() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.GetAllWindows(); // Assert - Assert.That(eventRaised, Is.True); - Assert.That(eventWindow, Is.EqualTo(mockWindow)); + Assert.That(result, Is.Empty); + Assert.That(result, Is.InstanceOf>()); } - // Window Finding Tests + #endregion + + #region GetWindowCount Tests + [Test] - public void FindWindowByFileName_ExistingFile_ReturnsWindow () + public void GetWindowCount_WhenEmpty_ReturnsZero() { - // Arrange - var mockWindow = CreateMockWindow("test.log"); - _TabController.AddWindow(mockWindow, "Test Window"); + // Arrange - already done in Setup // Act - var found = _TabController.FindWindowByFileName("test.log"); + var result = _tabController.GetWindowCount(); // Assert - Assert.That(found, Is.EqualTo(mockWindow)); + Assert.That(result, Is.EqualTo(0)); } + #endregion + + #region HasWindow Tests + [Test] - public void FindWindowByFileName_CaseInsensitive_ReturnsWindow () + public void HasWindow_WithNullWindow_ReturnsFalse() { - // Arrange - var mockWindow = CreateMockWindow("test.log"); - _TabController.AddWindow(mockWindow, "Test Window"); + // Arrange - already done in Setup // Act - var found = _TabController.FindWindowByFileName("TEST.LOG"); + var result = _tabController.HasWindow(null); // Assert - Assert.That(found, Is.EqualTo(mockWindow)); + Assert.That(result, Is.False); } - // Tab Switching Tests + #endregion + + #region GetActiveWindow Tests + [Test] - public void SwitchToNextWindow_MultipleWindows_ActivatesNextWindow () + public void GetActiveWindow_WhenNoWindowActive_ReturnsNull() { - // Arrange - var window1 = CreateMockWindow("test1.log"); - var window2 = CreateMockWindow("test2.log"); - _TabController.AddWindow(window1, "Window 1"); - _TabController.AddWindow(window2, "Window 2"); - window1.Activate(); + // Arrange - already done in Setup // Act - _TabController.SwitchToNextWindow(); + var result = _tabController.GetActiveWindow(); // Assert - // Verify window2.Activate() was called - Mock.Get(window2).Verify(w => w.Activate(), Times.Once); + Assert.That(result, Is.Null); } - // Thread Safety Tests + #endregion + + #region FindWindowByFileName Tests + [Test] - public void AddWindow_ConcurrentCalls_AllWindowsTracked () + public void FindWindowByFileName_WithNullFileName_ReturnsNull() { - // Arrange - var windows = Enumerable.Range(0, 100) - .Select(i => CreateMockWindow($"test{i}.log")) - .ToList(); + // Arrange - already done in Setup // Act - _ = Parallel.ForEach(windows, (window, state, index) => - { - _TabController.AddWindow(window, $"Window {index}"); - }); + var result = _tabController.FindWindowByFileName(null); // Assert - Assert.That(_TabController.GetWindowCount(), Is.EqualTo(100)); + Assert.That(result, Is.Null); } - // Disposal Tests [Test] - public void Dispose_UnsubscribesFromAllEvents () + public void FindWindowByFileName_WithEmptyFileName_ReturnsNull() { - // Arrange - var mockWindow = CreateMockWindow("test.log"); - _TabController.AddWindow(mockWindow, "Test Window"); + // Arrange - already done in Setup // Act - _TabController.Dispose(); + var result = _tabController.FindWindowByFileName(string.Empty); // Assert - // Verify no event subscriptions remain - Mock.Get(mockWindow).VerifyRemove(w => w.Disposed -= It.IsAny()); + Assert.That(result, Is.Null); } - private static LogWindow CreateMockWindow (string fileName) + [Test] + public void FindWindowByFileName_WhenNoWindowsExist_ReturnsNull() { - var mock = new Mock(); - _ = mock.Setup(w => w.FileName).Returns(fileName); - return mock.Object; + // Arrange - already done in Setup + + // Act + var result = _tabController.FindWindowByFileName("test.log"); + + // Assert + Assert.That(result, Is.Null); + } + + #endregion + + #region AddWindow Tests + + [Test] + public void AddWindow_WithNullWindow_ThrowsArgumentNullException() + { + // Arrange - already done in Setup + + // Act & Assert + Assert.Throws(() => _tabController.AddWindow(null, "Test Window")); + } + + [Test] + public void AddWindow_WhenNotInitialized_ThrowsInvalidOperationException() + { + // Arrange + using var controller = new TabController(); + + // Create a mock-like object that's not null to avoid ArgumentNullException + // We need to test that the "not initialized" check happens + // Unfortunately, LogWindow cannot be instantiated without its dependencies + // So we can only verify the ArgumentNullException is thrown first for null + var ex = Assert.Throws(() => controller.AddWindow(null, "Test")); + Assert.That(ex.ParamName, Is.EqualTo("window")); } + + #endregion + + #region RemoveWindow Tests + + [Test] + public void RemoveWindow_WithNullWindow_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.RemoveWindow(null)); + } + + #endregion + + #region CloseWindow Tests + + [Test] + public void CloseWindow_WithNullWindow_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.CloseWindow(null)); + } + + #endregion + + #region CloseAllWindows Tests + + [Test] + public void CloseAllWindows_WhenEmpty_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.CloseAllWindows()); + } + + #endregion + + #region CloseAllExcept Tests + + [Test] + public void CloseAllExcept_WithNullWindow_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.CloseAllExcept(null)); + } + + #endregion + + #region ActivateWindow Tests + + [Test] + public void ActivateWindow_WithNullWindow_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.ActivateWindow(null)); + } + + #endregion + + #region SwitchToNextWindow Tests + + [Test] + public void SwitchToNextWindow_WhenEmpty_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.SwitchToNextWindow()); + } + + #endregion + + #region SwitchToPreviousWindow Tests + + [Test] + public void SwitchToPreviousWindow_WhenEmpty_DoesNotThrow() + { + // Arrange - already done in Setup + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _tabController.SwitchToPreviousWindow()); + } + + #endregion + + #region Dispose Tests + + [Test] + public void Dispose_MultipleCallsDoNotThrow() + { + // Arrange + using var controller = new TabController(_dockPanel); + + // Act & Assert - multiple dispose calls should not throw + Assert.DoesNotThrow(() => + { + controller.Dispose(); + controller.Dispose(); + controller.Dispose(); + }); + } + + #endregion } diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index 4752bc3b6..1094b0981 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -112,12 +112,14 @@ internal partial class LogWindow : DockContent, ILogPaintContextUI, ILogView, IL private ILogLineMemoryColumnizer _forcedColumnizer; private ILogLineMemoryColumnizer _forcedColumnizerForLoading; + private bool _isDeadFile; private bool _isErrorShowing; private bool _isLoadError; private bool _isLoading; private bool _isSearching; private bool _isTimestampDisplaySyncing; + private List _lastFilterLinesList = []; private int _lineHeight; diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index b57f6a7a7..944a4f450 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -44,6 +44,9 @@ internal partial class LogTabWindow : Form, ILogTabWindow private readonly Icon _deadIcon; private readonly LedIndicatorService _ledService; + + private ITabController _tabController; + private readonly Lock _windowListLock = new(); private bool _disposed; @@ -83,6 +86,12 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu ConfigureDockPanel(); + _tabController = new TabController(dockPanel); + _tabController.WindowAdded += OnTabControllerWindowAdded; + _tabController.WindowRemoved += OnTabControllerWindowRemoved; + _tabController.WindowActivated += OnTabControllerWindowActivated; + _tabController.WindowClosing += OnTabControllerWindowClosing; + ApplyTextResources(); ConfigManager = configManager; @@ -616,49 +625,146 @@ public ILogLineMemoryColumnizer GetColumnizerHistoryEntry (string fileName) public void SwitchTab (bool shiftPressed) { - var index = dockPanel.Contents.IndexOf(dockPanel.ActiveContent); if (shiftPressed) { - index--; - if (index < 0) - { - index = dockPanel.Contents.Count - 1; - } - - if (index < 0) - { - return; - } + _tabController.SwitchToPreviousWindow(); } else { - index++; - if (index >= dockPanel.Contents.Count) + _tabController.SwitchToNextWindow(); + } + } + + public void ScrollAllTabsToTimestamp (DateTime timestamp, LogWindow.LogWindow senderWindow) + { + foreach (var logWindow in _tabController.GetAllWindows()) + { + if (logWindow != senderWindow) { - index = 0; + if (logWindow.ScrollToTimestamp(timestamp, false, false)) + { + _ledService.UpdateWindowActivity(logWindow, DIFF_MAX); + } } } + } - if (index < dockPanel.Contents.Count) + /// + /// Handles the WindowActivated event from TabController. + /// Updates CurrentLogWindow and connects tool windows to the newly activated window. + /// + /// The TabController that raised the event + /// Event args containing the activated window and previous window + [SupportedOSPlatform("windows")] + private void OnTabControllerWindowActivated (object sender, WindowActivatedEventArgs e) + { + var newWindow = e.Window; + var previousWindow = e.PreviousWindow; + + if (newWindow == _currentLogWindow) + { + return; + } + + // Update CurrentLogWindow - this triggers ChangeCurrentLogWindow internally + // which handles disconnecting from previous window and connecting to new window + CurrentLogWindow = newWindow; + + // Clear dirty state for the newly activated window + if (newWindow?.Tag is LogWindowData data) { - (dockPanel.Contents[index] as DockContent).Activate(); + data.LedState.IsDirty = false; + + // Update the tab icon to reflect cleared dirty state + var icon = GetLedIcon(data.LedState.DiffSum, data); + _ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), newWindow, icon); + } + + // Notify the window it has been activated + newWindow?.LogWindowActivated(); + + // Connect tool windows (bookmark window, etc.) to new window + if (newWindow != null) + { + ConnectToolWindows(newWindow); } } - public void ScrollAllTabsToTimestamp (DateTime timestamp, LogWindow.LogWindow senderWindow) + /// + /// Handles the WindowAdded event from TabController. + /// Performs additional setup for newly added windows that LogTabWindow needs. + /// + /// The TabController that raised the event + /// Event args containing the added window and title + [SupportedOSPlatform("windows")] + private void OnTabControllerWindowAdded (object sender, WindowAddedEventArgs e) { - lock (_logWindowList) + var logWindow = e.Window; + var title = e.Title; + + if (logWindow.Tag is not LogWindowData) { - foreach (var logWindow in _logWindowList) + LogWindowData data = new() { - if (logWindow != senderWindow) - { - if (logWindow.ScrollToTimestamp(timestamp, false, false)) - { - _ledService.UpdateWindowActivity(logWindow, DIFF_MAX); - } - } - } + LedState = new LedState(), + Color = _defaultTabColor + }; + + logWindow.Tag = data; + } + + _ledService.RegisterWindow(logWindow); + + ConnectEventHandlers(logWindow); + } + + /// + /// Handles the WindowClosing event from TabController. + /// Performs pre-close validation and cleanup. Can cancel the close operation. + /// + /// The TabController that raised the event + /// Event args containing the window being closed and cancellation support + [SupportedOSPlatform("windows")] + private void OnTabControllerWindowClosing (object sender, WindowClosingEventArgs e) + { + var logWindow = e.Window; + var skipConfirmation = e.SkipConfirmation; + + if (_tabController.GetWindowCount() == 1 && !skipConfirmation) + { + //TODO Add logic to confirm closing the last tab if desired + } + + if (logWindow.Tag is LogWindowData data) + { + data.ToolTip?.Hide(logWindow); + } + } + + /// + /// Handles the WindowRemoved event from TabController. + /// Cleans up resources and event subscriptions for the removed window. + /// + /// The TabController that raised the event + /// Event args containing the removed window + [SupportedOSPlatform("windows")] + private void OnTabControllerWindowRemoved (object sender, WindowRemovedEventArgs e) + { + var logWindow = e.Window; + + _ledService.UnregisterWindow(logWindow); + + DisconnectEventHandlers(logWindow); + + if (logWindow.Tag is LogWindowData data) + { + data.ToolTip?.Dispose(); + logWindow.Tag = null; + } + + if (CurrentLogWindow == logWindow) + { + ChangeCurrentLogWindow(null); } } @@ -712,7 +818,7 @@ public HighlightGroup FindHighlightGroupByFileMask (string fileName) public void SelectTab (ILogWindow logWindow) { - logWindow.Activate(); + _tabController.ActivateWindow(logWindow as LogWindow.LogWindow); } [SupportedOSPlatform("windows")] @@ -761,12 +867,10 @@ public void NotifySettingsChanged (object sender, SettingsFlags flags) public IList GetListOfOpenFiles () { IList list = []; - lock (_logWindowList) + + foreach (var logWindow in _tabController.GetAllWindows()) { - foreach (var logWindow in _logWindowList) - { - list.Add(new WindowFileEntry(logWindow)); - } + list.Add(new WindowFileEntry(logWindow)); } return list; @@ -866,14 +970,11 @@ private void DestroyBookmarkWindow () private void SaveLastOpenFilesList () { - foreach (DockContent content in dockPanel.Contents.Cast()) + foreach (var logWin in _tabController.GetAllWindowsFromDockPanel()) { - if (content is LogWindow.LogWindow logWin) + if (!logWin.IsTempFile) { - if (!logWin.IsTempFile) - { - ConfigManager.Settings.LastOpenFilesList.Add(logWin.GivenFileName); - } + ConfigManager.Settings.LastOpenFilesList.Add(logWin.GivenFileName); } } } @@ -941,6 +1042,13 @@ private void AddFileTabs (string[] fileNames) Activate(); } + /// + /// Adds a LogWindow to the tab system. + /// Sets up window properties, delegates to TabController, and performs additional setup. + /// + /// The window to add + /// Tab title + /// Skip adding to DockPanel (for deferred loading) [SupportedOSPlatform("windows")] private void AddLogWindow (LogWindow.LogWindow logWindow, string title, bool doNotAddToPanel) { @@ -949,23 +1057,13 @@ private void AddLogWindow (LogWindow.LogWindow logWindow, string title, bool doN SetTooltipText(logWindow, title); logWindow.DockAreas = DockAreas.Document | DockAreas.Float; - if (!doNotAddToPanel) - { - logWindow.Show(dockPanel); - } - - LogWindowData data = new() - { - LedState = new LedState() - }; - - logWindow.Tag = data; + _tabController.AddWindow(logWindow, title, doNotAddToPanel); - lock (_windowListLock) - { - _logWindowList.Add(logWindow); - } + logWindow.Visible = true; + } + private void ConnectEventHandlers (LogWindow.LogWindow logWindow) + { logWindow.FileSizeChanged += OnFileSizeChanged; logWindow.TailFollowed += OnTailFollowed; logWindow.Disposed += OnLogWindowDisposed; @@ -974,10 +1072,6 @@ private void AddLogWindow (LogWindow.LogWindow logWindow, string title, bool doN logWindow.FilterListChanged += OnLogWindowFilterListChanged; logWindow.CurrentHighlightGroupChanged += OnLogWindowCurrentHighlightGroupChanged; logWindow.SyncModeChanged += OnLogWindowSyncModeChanged; - - logWindow.Visible = true; - - _ledService.RegisterWindow(logWindow); } [SupportedOSPlatform("windows")] @@ -992,7 +1086,7 @@ private void DisconnectEventHandlers (LogWindow.LogWindow logWindow) logWindow.CurrentHighlightGroupChanged -= OnLogWindowCurrentHighlightGroupChanged; logWindow.SyncModeChanged -= OnLogWindowSyncModeChanged; - var data = logWindow.Tag as LogWindowData; + //var data = logWindow.Tag as LogWindowData; //data.tabPage.MouseClick -= tabPage_MouseClick; //data.tabPage.TabDoubleClick -= tabPage_TabDoubleClick; //data.tabPage.ContextMenuStrip = null; @@ -1006,21 +1100,15 @@ private void AddToFileHistory (string fileName) FillHistoryMenu(); } + /// + /// Finds an existing window for a file. + /// + /// File name to search for + /// The LogWindow for the file, or null if not found [SupportedOSPlatform("windows")] private LogWindow.LogWindow FindWindowForFile (string fileName) { - lock (_logWindowList) - { - foreach (var logWindow in _logWindowList) - { - if (logWindow.FileName.ToUpperInvariant().Equals(fileName.ToUpperInvariant(), StringComparison.Ordinal)) - { - return logWindow; - } - } - } - - return null; + return _tabController.FindWindowByFileName(fileName); } [SupportedOSPlatform("windows")] @@ -1040,30 +1128,21 @@ private void FillHistoryMenu () lastUsedToolStripMenuItem.DropDown = strip; } + /// + /// Removes a LogWindow from the tab system. + /// Delegates to TabController for removal and cleanup. + /// + /// The window to remove [SupportedOSPlatform("windows")] private void RemoveLogWindow (LogWindow.LogWindow logWindow) { - lock (_windowListLock) - { - _ = _logWindowList.Remove(logWindow); - _ledService.UnregisterWindow(logWindow); - } - - DisconnectEventHandlers(logWindow); + _tabController.RemoveWindow(logWindow); } [SupportedOSPlatform("windows")] private void RemoveAndDisposeLogWindow (LogWindow.LogWindow logWindow, bool dontAsk) { - if (CurrentLogWindow == logWindow) - { - ChangeCurrentLogWindow(null); - } - - lock (_logWindowList) - { - _ = _logWindowList.Remove(logWindow); - } + _tabController.RemoveWindow(logWindow); logWindow.Close(dontAsk); } @@ -1535,13 +1614,13 @@ private void NotifyWindowsForChangedPrefs (SettingsFlags flags) var fontName = ConfigManager.Settings.Preferences.FontName; var fontSize = ConfigManager.Settings.Preferences.FontSize; - lock (_logWindowList) + //lock (_logWindowList) + //{ + foreach (var logWindow in _tabController.GetAllWindows()) { - foreach (var logWindow in _logWindowList) - { - logWindow.PreferencesChanged(fontName, fontSize, setLastColumnWidth, lastColumnWidth, false, flags); - } + logWindow.PreferencesChanged(fontName, fontSize, setLastColumnWidth, lastColumnWidth, false, flags); } + //} _bookmarkWindow.PreferencesChanged(fontName, fontSize, setLastColumnWidth, lastColumnWidth, flags); @@ -1587,15 +1666,15 @@ private void ApplySettings (Settings settings, SettingsFlags flags) private void SetTabIcons (Preferences preferences) { _ledService.RegenerateIcons(preferences.ShowTailColor); - lock (_logWindowList) + //lock (_logWindowList) + //{ + foreach (var logWindow in _tabController.GetAllWindows()) { - foreach (var logWindow in _logWindowList) - { - var data = logWindow.Tag as LogWindowData; - var icon = GetLedIcon(data.LedState.DiffSum, data); - _ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWindow, icon); - } + var data = logWindow.Tag as LogWindowData; + var icon = GetLedIcon(data.LedState.DiffSum, data); + _ = BeginInvoke(new SetTabIconDelegate(SetTabIcon), logWindow, icon); } + //} } [SupportedOSPlatform("windows")] @@ -1719,22 +1798,7 @@ ObjectDisposedException or [SupportedOSPlatform("windows")] private void CloseAllTabs () { - IList
closeList = []; - lock (_logWindowList) - { - foreach (var content in dockPanel.Contents.Cast()) - { - if (content is LogWindow.LogWindow window) - { - closeList.Add(window); - } - } - } - - foreach (var form in closeList) - { - form.Close(); - } + _tabController.CloseAllWindows(); } //TODO Reimplementation needs a new UI Framework since, DockpanelSuite has no easy way to change TabColor @@ -1836,7 +1900,7 @@ private void LoadProject (string projectFileName, bool restoreLayout) } // Restore layout only if we loaded at least one file - if (hasLayoutData && restoreLayout && _logWindowList.Count > 0) + if (hasLayoutData && restoreLayout && _tabController.GetWindowCount() > 0) { _logger.Info("Restoring layout"); // Re-creating tool (non-document) windows is needed because the DockPanel control would throw strange errors @@ -1844,7 +1908,7 @@ private void LoadProject (string projectFileName, bool restoreLayout) InitToolWindows(); RestoreLayout(projectData.TabLayoutXml); } - else if (_logWindowList.Count == 0) + else if (_tabController.GetWindowCount() == 0) { _logger.Warn("No files loaded, skipping layout restoration"); } @@ -2065,7 +2129,7 @@ private void OnLogTabWindowFormClosing (object sender, CancelEventArgs e) ConfigManager.Settings.AlwaysOnTop = TopMost && ConfigManager.Settings.Preferences.AllowOnlyOneInstance; SaveLastOpenFilesList(); - foreach (var logWindow in _logWindowList.ToArray()) + foreach (var logWindow in _tabController.GetAllWindows()) { RemoveAndDisposeLogWindow(logWindow, true); } @@ -2142,23 +2206,20 @@ private void OnSelectFilterToolStripMenuItemClick (object sender, EventArgs e) { if (form.ApplyToAll) { - lock (_logWindowList) + foreach (var logWindow in _tabController.GetAllWindows()) { - foreach (var logWindow in _logWindowList) + if (logWindow.CurrentColumnizer.GetType() != form.SelectedColumnizer.GetType()) { - if (logWindow.CurrentColumnizer.GetType() != form.SelectedColumnizer.GetType()) - { - //logWindow.SetColumnizer(form.SelectedColumnizer); - SetColumnizerFx fx = logWindow.ForceColumnizer; - _ = logWindow.Invoke(fx, form.SelectedColumnizer); - SetColumnizerHistoryEntry(logWindow.FileName, form.SelectedColumnizer); - } - else + //logWindow.SetColumnizer(form.SelectedColumnizer); + SetColumnizerFx fx = logWindow.ForceColumnizer; + _ = logWindow.Invoke(fx, form.SelectedColumnizer); + SetColumnizerHistoryEntry(logWindow.FileName, form.SelectedColumnizer); + } + else + { + if (form.IsConfigPressed) { - if (form.IsConfigPressed) - { - logWindow.ColumnizerConfigChanged(); - } + logWindow.ColumnizerConfigChanged(); } } } @@ -2174,16 +2235,14 @@ private void OnSelectFilterToolStripMenuItemClick (object sender, EventArgs e) if (form.IsConfigPressed) { - lock (_logWindowList) + foreach (var logWindow in _tabController.GetAllWindows()) { - foreach (var logWindow in _logWindowList) + if (logWindow.CurrentColumnizer.GetType() == form.SelectedColumnizer.GetType()) { - if (logWindow.CurrentColumnizer.GetType() == form.SelectedColumnizer.GetType()) - { - logWindow.ColumnizerConfigChanged(); - } + logWindow.ColumnizerConfigChanged(); } } + } } } @@ -2434,14 +2493,11 @@ private void OnLogWindowFileRespawned (object sender, EventArgs e) private void OnLogWindowFilterListChanged (object sender, FilterListChangedEventArgs e) { - lock (_logWindowList) + foreach (var logWindow in _tabController.GetAllWindows()) { - foreach (var logWindow in _logWindowList) + if (logWindow != e.LogWindow) { - if (logWindow != e.LogWindow) - { - logWindow.HandleChangedFilterList(); - } + logWindow.HandleChangedFilterList(); } } @@ -2670,12 +2726,9 @@ private void OnHideLineColumnToolStripMenuItemClick (object sender, EventArgs e) { ConfigManager.Settings.HideLineColumn = hideLineColumnToolStripMenuItem.Checked; - lock (_logWindowList) + foreach (var logWin in _tabController.GetAllWindows()) { - foreach (var logWin in _logWindowList) - { - logWin.ShowLineColumn(!ConfigManager.Settings.HideLineColumn); - } + logWin.ShowLineColumn(!ConfigManager.Settings.HideLineColumn); } _bookmarkWindow.LineColumnVisible = ConfigManager.Settings.HideLineColumn; @@ -2694,9 +2747,9 @@ private void OnCloseThisTabToolStripMenuItemClick (object sender, EventArgs e) [SupportedOSPlatform("windows")] private void OnCloseOtherTabsToolStripMenuItemClick (object sender, EventArgs e) { - var closeList = dockPanel.Contents - .OfType() - .Where(content => content != dockPanel.ActiveContent) + var activeWindow = _tabController.GetActiveWindow(); + var closeList = _tabController.GetAllWindowsFromDockPanel() + .Where(window => window != activeWindow) .ToList(); foreach (var logWindow in closeList) @@ -2779,15 +2832,12 @@ private void OnSaveProjectToolStripMenuItemClick (object sender, EventArgs e) var fileName = dlg.FileName; List fileNames = []; - lock (_logWindowList) + foreach (var logWin in _tabController.GetAllWindowsFromDockPanel()) { - foreach (var logWindow in dockPanel.Contents.OfType()) + var persistenceFileName = logWin?.SavePersistenceDataAndReturnFileName(true); + if (persistenceFileName != null) { - var persistenceFileName = logWindow?.SavePersistenceDataAndReturnFileName(true); - if (persistenceFileName != null) - { - fileNames.Add(persistenceFileName); - } + fileNames.Add(persistenceFileName); } } diff --git a/src/LogExpert.UI/Entities/LogWindowMetadata.cs b/src/LogExpert.UI/Entities/LogWindowMetadata.cs new file mode 100644 index 000000000..40e3d8652 --- /dev/null +++ b/src/LogExpert.UI/Entities/LogWindowMetadata.cs @@ -0,0 +1,20 @@ +using LogExpert.UI.Controls.LogWindow; + +namespace LogExpert.UI.Entities; + +internal class LogWindowMetadata +{ + public LogWindow Window { get; set; } + + public string Title { get; set; } + + public string FileName { get; set; } + + public DateTime CreatedAt { get; set; } + + public bool IsTempFile { get; set; } + + public Color TabColor { get; set; } + + public object Tag { get; set; } +} diff --git a/src/LogExpert.UI/Services/ITabController.cs b/src/LogExpert.UI/Services/ITabController.cs index 265add796..c9f015ddd 100644 --- a/src/LogExpert.UI/Services/ITabController.cs +++ b/src/LogExpert.UI/Services/ITabController.cs @@ -26,7 +26,6 @@ internal interface ITabController : IDisposable void CloseAllExcept (LogWindow window); - // Window Activation void ActivateWindow (LogWindow window); LogWindow GetActiveWindow (); @@ -35,19 +34,36 @@ internal interface ITabController : IDisposable void SwitchToPreviousWindow (); - // Window Queries LogWindow FindWindowByFileName (string fileName); IReadOnlyList GetAllWindows (); + /// + /// Gets all LogWindow instances from the DockPanel's Contents collection. + /// This returns windows that are currently displayed in the DockPanel, + /// which may include windows not explicitly tracked by TabController + /// (e.g., windows restored from layout serialization). + /// + /// + /// Use this method when you need to iterate over all visible LogWindows, + /// particularly for operations like: + /// - Saving project/session data + /// - Saving last open files list + /// - Closing all tabs + /// - Applying settings to all windows + /// + /// For most other operations, prefer which + /// returns only explicitly tracked windows. + /// + /// Read-only list of all LogWindows in the DockPanel + IReadOnlyList GetAllWindowsFromDockPanel (); + int GetWindowCount (); bool HasWindow (LogWindow window); - // DockPanel Integration void InitializeDockPanel (DockPanel dockPanel); - // Events event EventHandler WindowAdded; event EventHandler WindowRemoved; event EventHandler WindowActivated; diff --git a/src/LogExpert.UI/Services/TabController.cs b/src/LogExpert.UI/Services/TabController.cs index cf1e66cc9..a552e532a 100644 --- a/src/LogExpert.UI/Services/TabController.cs +++ b/src/LogExpert.UI/Services/TabController.cs @@ -443,5 +443,19 @@ protected virtual void Dispose (bool disposing) _disposed = true; } + /// + /// Gets all LogWindow instances from the DockPanel's Contents collection. + /// + /// Read-only list of all LogWindows in the DockPanel + public IReadOnlyList GetAllWindowsFromDockPanel () + { + return !_initialized || _dockPanel == null + ? [] + : (IReadOnlyList)_dockPanel.Contents + .OfType() + .ToList() + .AsReadOnly(); + } + #endregion } From 0d52e7578519e0d94b770c8760160d290a72f221 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Jan 2026 12:12:40 +0000 Subject: [PATCH 3/9] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 76362dd77..7158f6b17 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-01-07 16:22:23 UTC + /// Generated: 2026-01-16 12:12:39 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "2B9AF25F395E12C119B097B8F3ACADBE4E39D0644CBC9C76C6F9D455A048D06B", + ["AutoColumnizer.dll"] = "6111BC3429644CE16B5BB86A87986B6EADD7156E7191BE81C83C36A4956382E4", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "7818AB956F804C99635121E9E1D5D2FB10787FA11FFCB932295329D0FCB62A9A", - ["CsvColumnizer.dll (x86)"] = "7818AB956F804C99635121E9E1D5D2FB10787FA11FFCB932295329D0FCB62A9A", - ["DefaultPlugins.dll"] = "844D7A95AE73061DE281FFFF0F7375337288D7B54143C8B9D710F8019285BF2A", - ["FlashIconHighlighter.dll"] = "24D5E000AB0C47699E7BD9D229A87EDAB13072207B1AD63BC54C342F65892B24", - ["GlassfishColumnizer.dll"] = "4BD2970019C0C21A12D7BC2AF379851345EFABA957B53BDB119202D1E463CA33", - ["JsonColumnizer.dll"] = "D293EF6E1AB1144F55008A1A312833C1CABAE9C2064E506D93044C78341F6EDA", - ["JsonCompactColumnizer.dll"] = "62F278970C0EDB07434E089F5452284252D0E9F72868C1C526AF60A5251937A0", - ["Log4jXmlColumnizer.dll"] = "E961F9472FAA8E5557BA8565CAD23379B8D4127F9D5231DE35F8021380BB9997", - ["LogExpert.Core.dll"] = "9876974732087663FBD01D348A0388333398BA51650F17C52994B3033A0635E9", - ["LogExpert.Resources.dll"] = "E4198972B4058C59056FD844BBF74DFBE0EC44827AE3FF565321F6750E0CABDD", + ["CsvColumnizer.dll"] = "75899CB62D3F636F38ABFAF58DF99186981B87A9D3BE53E8193D555C28492DB0", + ["CsvColumnizer.dll (x86)"] = "75899CB62D3F636F38ABFAF58DF99186981B87A9D3BE53E8193D555C28492DB0", + ["DefaultPlugins.dll"] = "3B5C8FC27A5D27C3A49CE97CF7FD14D5D4008233747ADBFECCB23C955906AC39", + ["FlashIconHighlighter.dll"] = "0A49A40CE82F21A973CC2C5F86C8A9D45ABA734FA6B84AFC42C224FA675D291A", + ["GlassfishColumnizer.dll"] = "409952CBF67C7A43947ED62F6E317067201215C146B3E9EB3F3319C0E70094AD", + ["JsonColumnizer.dll"] = "C2DA798DABF72021E38CFAE4DA2C1C90186A129010717E33C110AE19A1B73C08", + ["JsonCompactColumnizer.dll"] = "109F5F4809CEC83F05811078E0ADBE2BE8800D4CBBD98851CD0A3F18496489D4", + ["Log4jXmlColumnizer.dll"] = "E4C7F352DA04CD96EF271251D9939E28979D15C18E0B1D7DD608FB4B7E2CCAE5", + ["LogExpert.Core.dll"] = "6496DF67818195305861B089CFB95B60E1FF79DFCE3A461911C8FEFC42465541", + ["LogExpert.Resources.dll"] = "B2AE047E4AF84338E6CCA03A5AF1C0E148883E93C001FA18A7667AB59EB070F1", ["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"] = "E61F23C064D42E714383D2B948AA342A54E8A57FC94976C7BF53B7A4F4D9D78C", - ["SftpFileSystem.dll"] = "4B34EF6D27630302FB5EAC5E24E2E351D34C1A35517ACD8CC78D982324D801F9", - ["SftpFileSystem.dll (x86)"] = "B01F7467A14018CB1FF5A3E919A68E8C58CA98F93820AE17C77E47898376AA98", - ["SftpFileSystem.Resources.dll"] = "01DB02CA8CE8047FD4552A359C31DDD3100A8CE4A471A1E1FB7FE67CAA1B546D", - ["SftpFileSystem.Resources.dll (x86)"] = "01DB02CA8CE8047FD4552A359C31DDD3100A8CE4A471A1E1FB7FE67CAA1B546D", + ["RegexColumnizer.dll"] = "B6673C107F6C909531C7515F79FD274E83112E11DAEF64F2C0590A63B072929C", + ["SftpFileSystem.dll"] = "5392F9CF20B5BD1CEC4CFDD8B9277FDC4A9CE841C16CD67133BD86FC71E4EB9E", + ["SftpFileSystem.dll (x86)"] = "C69DBE2F98F359AE78CE3E394C3A35DFE58F77ED32C1F0A490097E33B97ED2E4", + ["SftpFileSystem.Resources.dll"] = "D043CFCAD9F415FAF8727D3A52F2C72C083A3968E4B663381E615C7E4F2D6339", + ["SftpFileSystem.Resources.dll (x86)"] = "D043CFCAD9F415FAF8727D3A52F2C72C083A3968E4B663381E615C7E4F2D6339", }; } From ec82d506fc086b12f2d1af39b38e66a3a4519f04 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Fri, 16 Jan 2026 13:17:53 +0100 Subject: [PATCH 4/9] comments --- .../Dialogs/LogTabWindow/LogTabWindow.cs | 1 - src/LogExpert.UI/Services/ITabController.cs | 81 +++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 944a4f450..4e61270e8 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -55,7 +55,6 @@ internal partial class LogTabWindow : Form, ILogTabWindow private readonly int _instanceNumber; - private readonly IList _logWindowList = []; private readonly bool _showInstanceNumbers; private readonly string[] _startupFileNames; diff --git a/src/LogExpert.UI/Services/ITabController.cs b/src/LogExpert.UI/Services/ITabController.cs index c9f015ddd..319cfe069 100644 --- a/src/LogExpert.UI/Services/ITabController.cs +++ b/src/LogExpert.UI/Services/ITabController.cs @@ -4,38 +4,77 @@ namespace LogExpert.UI.Services; +/// +/// Controls the management of LogWindow tabs in the application. +/// Provides methods for adding, removing, activating, and navigating between log windows. +/// internal interface ITabController : IDisposable { /// - /// + /// Adds a new LogWindow to the controller. /// - /// - /// - /// + /// The LogWindow instance to add. + /// The title to display on the tab. + /// If true, the window is tracked but not added to the DockPanel. void AddWindow (LogWindow window, string title, bool doNotAddToDockPanel = false); /// - /// + /// Removes a LogWindow from the controller without closing it. /// - /// + /// The LogWindow instance to remove. void RemoveWindow (LogWindow window); + /// + /// Closes a LogWindow, optionally prompting for confirmation if there are unsaved changes. + /// + /// The LogWindow instance to close. + /// If true, closes without prompting for confirmation. void CloseWindow (LogWindow window, bool skipConfirmation = false); + /// + /// Closes all LogWindows managed by the controller. + /// void CloseAllWindows (); + /// + /// Closes all LogWindows except the specified window. + /// + /// The LogWindow to keep open. void CloseAllExcept (LogWindow window); + /// + /// Activates and brings focus to the specified LogWindow. + /// + /// The LogWindow to activate. void ActivateWindow (LogWindow window); + /// + /// Gets the currently active LogWindow. + /// + /// The active LogWindow, or null if no window is active. LogWindow GetActiveWindow (); + /// + /// Switches focus to the next LogWindow in the tab order. + /// void SwitchToNextWindow (); + /// + /// Switches focus to the previous LogWindow in the tab order. + /// void SwitchToPreviousWindow (); + /// + /// Finds a LogWindow by its associated file name. + /// + /// The file name to search for. + /// The LogWindow associated with the file name, or null if not found. LogWindow FindWindowByFileName (string fileName); + /// + /// Gets all LogWindows explicitly tracked by the controller. + /// + /// A read-only list of all tracked LogWindows. IReadOnlyList GetAllWindows (); /// @@ -55,17 +94,45 @@ internal interface ITabController : IDisposable /// For most other operations, prefer which /// returns only explicitly tracked windows. /// - /// Read-only list of all LogWindows in the DockPanel + /// Read-only list of all LogWindows in the DockPanel. IReadOnlyList GetAllWindowsFromDockPanel (); + /// + /// Gets the total number of LogWindows managed by the controller. + /// + /// The count of tracked LogWindows. int GetWindowCount (); + /// + /// Checks if the specified LogWindow is managed by the controller. + /// + /// The LogWindow to check. + /// True if the window is tracked by the controller; otherwise, false. bool HasWindow (LogWindow window); + /// + /// Initializes the controller with the specified DockPanel for window management. + /// + /// The DockPanel to use for displaying LogWindows. void InitializeDockPanel (DockPanel dockPanel); + /// + /// Occurs when a new LogWindow is added to the controller. + /// event EventHandler WindowAdded; + + /// + /// Occurs when a LogWindow is removed from the controller. + /// event EventHandler WindowRemoved; + + /// + /// Occurs when a LogWindow is activated. + /// event EventHandler WindowActivated; + + /// + /// Occurs when a LogWindow is about to close. + /// event EventHandler WindowClosing; } From 195ae208e03311dc8c475ee6e13a4a9b86e5c801 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Jan 2026 12:20:54 +0000 Subject: [PATCH 5/9] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 7158f6b17..0540df401 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-01-16 12:12:39 UTC + /// Generated: 2026-01-16 12:20:53 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "6111BC3429644CE16B5BB86A87986B6EADD7156E7191BE81C83C36A4956382E4", + ["AutoColumnizer.dll"] = "C9AAB85F6FFC11CBE338CF3D8493DF537CD36E7FCBB7AD926D47951E0E576D5C", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "75899CB62D3F636F38ABFAF58DF99186981B87A9D3BE53E8193D555C28492DB0", - ["CsvColumnizer.dll (x86)"] = "75899CB62D3F636F38ABFAF58DF99186981B87A9D3BE53E8193D555C28492DB0", - ["DefaultPlugins.dll"] = "3B5C8FC27A5D27C3A49CE97CF7FD14D5D4008233747ADBFECCB23C955906AC39", - ["FlashIconHighlighter.dll"] = "0A49A40CE82F21A973CC2C5F86C8A9D45ABA734FA6B84AFC42C224FA675D291A", - ["GlassfishColumnizer.dll"] = "409952CBF67C7A43947ED62F6E317067201215C146B3E9EB3F3319C0E70094AD", - ["JsonColumnizer.dll"] = "C2DA798DABF72021E38CFAE4DA2C1C90186A129010717E33C110AE19A1B73C08", - ["JsonCompactColumnizer.dll"] = "109F5F4809CEC83F05811078E0ADBE2BE8800D4CBBD98851CD0A3F18496489D4", - ["Log4jXmlColumnizer.dll"] = "E4C7F352DA04CD96EF271251D9939E28979D15C18E0B1D7DD608FB4B7E2CCAE5", - ["LogExpert.Core.dll"] = "6496DF67818195305861B089CFB95B60E1FF79DFCE3A461911C8FEFC42465541", - ["LogExpert.Resources.dll"] = "B2AE047E4AF84338E6CCA03A5AF1C0E148883E93C001FA18A7667AB59EB070F1", + ["CsvColumnizer.dll"] = "FEAEBA1B77FE6CD515F18DC69FFB48AAA4601EC6CDC4A9855DE112365FFA1C98", + ["CsvColumnizer.dll (x86)"] = "FEAEBA1B77FE6CD515F18DC69FFB48AAA4601EC6CDC4A9855DE112365FFA1C98", + ["DefaultPlugins.dll"] = "0669BA2F33F4B5D56CD3037F906A41D2EB678B566F73CBA018B6200C5061E9BA", + ["FlashIconHighlighter.dll"] = "816D651A03F69CA3D30998D846EB06C600B2BD6313FD5F659164CBD0D8E0B2C3", + ["GlassfishColumnizer.dll"] = "EA925B1824BF9747DF184481B3B1B32BED7AD934FAEBE7422EB7F0A1F23E5D71", + ["JsonColumnizer.dll"] = "5E017492FF030F31D6E0C6606AAC989641146BB3191A7EF6A73EF8831EB75843", + ["JsonCompactColumnizer.dll"] = "82B21EE185DCD196BE418C2B4149FD7F26EC0FF8FBA6A0B22A74033FACC5A3C0", + ["Log4jXmlColumnizer.dll"] = "6C18EDFAFFFE42183F9D3EF3695D788DDA98F36CAC778F194C51CF2A4638CB02", + ["LogExpert.Core.dll"] = "A3689B302EE6FAE2E9B8B9542C6CB922B2D4ADDB2783350E69F043ABE18F7413", + ["LogExpert.Resources.dll"] = "5A86521CBFC03CA74BE1CE6A8E8D678464F2E05606D2431701943699012935CE", ["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"] = "B6673C107F6C909531C7515F79FD274E83112E11DAEF64F2C0590A63B072929C", - ["SftpFileSystem.dll"] = "5392F9CF20B5BD1CEC4CFDD8B9277FDC4A9CE841C16CD67133BD86FC71E4EB9E", - ["SftpFileSystem.dll (x86)"] = "C69DBE2F98F359AE78CE3E394C3A35DFE58F77ED32C1F0A490097E33B97ED2E4", - ["SftpFileSystem.Resources.dll"] = "D043CFCAD9F415FAF8727D3A52F2C72C083A3968E4B663381E615C7E4F2D6339", - ["SftpFileSystem.Resources.dll (x86)"] = "D043CFCAD9F415FAF8727D3A52F2C72C083A3968E4B663381E615C7E4F2D6339", + ["RegexColumnizer.dll"] = "10D0C45ED700B39F97869E6C03FEB74E1478FA9013461C6F98A28059C892FE33", + ["SftpFileSystem.dll"] = "DEE611A0E108B60043C1EB79AB62CBF9CEB2EA851F0F0C7D1A797D69733BF815", + ["SftpFileSystem.dll (x86)"] = "B453CE200B3E7C35C65D2348663AB057DB8E067C720EADA92D70E5212120449D", + ["SftpFileSystem.Resources.dll"] = "C0AD198F7C34C793C5A6D8037AB8FEFB6E776601B5119A588ACEA5AD44F5EBC1", + ["SftpFileSystem.Resources.dll (x86)"] = "C0AD198F7C34C793C5A6D8037AB8FEFB6E776601B5119A588ACEA5AD44F5EBC1", }; } From e7b32ae30579434cc56d327cfa1599826e498331 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Fri, 16 Jan 2026 13:25:16 +0100 Subject: [PATCH 6/9] resources update --- src/CsvColumnizer/Resources.de.resx | 14 +++++++++- src/LogExpert.Resources/Resources.Designer.cs | 27 +++++++++++++++++++ src/LogExpert.Resources/Resources.de.resx | 12 +++++++++ src/LogExpert.Resources/Resources.resx | 9 +++++++ .../Dialogs/LogTabWindow/LogTabWindow.cs | 4 +-- src/LogExpert.UI/Services/TabController.cs | 6 ++--- 6 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/CsvColumnizer/Resources.de.resx b/src/CsvColumnizer/Resources.de.resx index a0b3b097c..0422a468e 100644 --- a/src/CsvColumnizer/Resources.de.resx +++ b/src/CsvColumnizer/Resources.de.resx @@ -73,4 +73,16 @@ Abbrechen - + + Fehler + + + Fehler beim Deserialisieren der Konfigurationsdaten: {0} + + + Teilt die CSV-Dateien in Spalten auf. + +Credits: +Dieser Columnizer verwendet den CsvHelper. https://github.com/JoshClose/CsvHelper. + + \ No newline at end of file diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 06018dffb..b98b1101c 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -5916,6 +5916,33 @@ public static System.Drawing.Bitmap Star { } } + /// + /// Looks up a localized string similar to TabController is already initialized with a DockPanel. + /// + public static string TabController_Error_Message_AlreadInitialized { + get { + return ResourceManager.GetString("TabController_Error_Message_AlreadInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TabController is not initialized. Call InitializeDockPanel first.. + /// + public static string TabController_Error_Message_NotInitialized { + get { + return ResourceManager.GetString("TabController_Error_Message_NotInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Window already tracked. + /// + public static string TabController_Error_Message_WindowAlreadyTracked { + get { + return ResourceManager.GetString("TabController_Error_Message_WindowAlreadyTracked", resourceCulture); + } + } + /// /// Looks up a localized string similar to Name:. /// diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index c1ec0c3d3..46a7a27ae 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -2121,4 +2121,16 @@ LogExpert neu starten, um die Änderungen zu übernehmen? {0} ist bereits initialisiert + + {0} muss im UI-Thread erstellt werden + + + TabController ist nicht initialisiert. Rufen Sie zuerst InitializeDockPanel auf. + + + Fenster bereits verfolgt + + + TabController ist bereits mit einem DockPanel initialisiert + \ No newline at end of file diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index efeac89a0..b71dc9286 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -2133,4 +2133,13 @@ Restart LogExpert to apply changes? {0} must be created on UI thread + + TabController is not initialized. Call InitializeDockPanel first. + + + Window already tracked + + + TabController is already initialized with a DockPanel + \ No newline at end of file diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 4e61270e8..8e178b651 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -45,9 +45,7 @@ internal partial class LogTabWindow : Form, ILogTabWindow private readonly Icon _deadIcon; private readonly LedIndicatorService _ledService; - private ITabController _tabController; - - private readonly Lock _windowListLock = new(); + private readonly TabController _tabController; private bool _disposed; diff --git a/src/LogExpert.UI/Services/TabController.cs b/src/LogExpert.UI/Services/TabController.cs index a552e532a..2cf1b911e 100644 --- a/src/LogExpert.UI/Services/TabController.cs +++ b/src/LogExpert.UI/Services/TabController.cs @@ -61,7 +61,7 @@ public void InitializeDockPanel (DockPanel dockPanel) if (_initialized) { - throw new InvalidOperationException("TabController is already initialized with a DockPanel"); + throw new InvalidOperationException(Resources.TabController_Error_Message_AlreadInitialized); } _dockPanel = dockPanel; @@ -98,14 +98,14 @@ public void AddWindow (LogWindow window, string title, bool doNotAddToDockPanel if (!_initialized) { - throw new InvalidOperationException("TabController is not initialized. Call InitializeDockPanel first."); + throw new InvalidOperationException(Resources.TabController_Error_Message_NotInitialized); } lock (_windowsLock) { if (_windows.ContainsKey(window)) { - throw new InvalidOperationException("Window already tracked"); + throw new InvalidOperationException(Resources.TabController_Error_Message_WindowAlreadyTracked); } var metadata = new LogWindowMetadata From c9dbc5c4f458ba1c658bf3c1dc15824e86874ad0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Jan 2026 12:28:09 +0000 Subject: [PATCH 7/9] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 0540df401..b899daad1 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-01-16 12:20:53 UTC + /// Generated: 2026-01-16 12:28:08 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "C9AAB85F6FFC11CBE338CF3D8493DF537CD36E7FCBB7AD926D47951E0E576D5C", + ["AutoColumnizer.dll"] = "0BFB2D25838DA085A00A97A8710E48F36E4FE188F9E976800CEBB5BDA10EDCF1", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "FEAEBA1B77FE6CD515F18DC69FFB48AAA4601EC6CDC4A9855DE112365FFA1C98", - ["CsvColumnizer.dll (x86)"] = "FEAEBA1B77FE6CD515F18DC69FFB48AAA4601EC6CDC4A9855DE112365FFA1C98", - ["DefaultPlugins.dll"] = "0669BA2F33F4B5D56CD3037F906A41D2EB678B566F73CBA018B6200C5061E9BA", - ["FlashIconHighlighter.dll"] = "816D651A03F69CA3D30998D846EB06C600B2BD6313FD5F659164CBD0D8E0B2C3", - ["GlassfishColumnizer.dll"] = "EA925B1824BF9747DF184481B3B1B32BED7AD934FAEBE7422EB7F0A1F23E5D71", - ["JsonColumnizer.dll"] = "5E017492FF030F31D6E0C6606AAC989641146BB3191A7EF6A73EF8831EB75843", - ["JsonCompactColumnizer.dll"] = "82B21EE185DCD196BE418C2B4149FD7F26EC0FF8FBA6A0B22A74033FACC5A3C0", - ["Log4jXmlColumnizer.dll"] = "6C18EDFAFFFE42183F9D3EF3695D788DDA98F36CAC778F194C51CF2A4638CB02", - ["LogExpert.Core.dll"] = "A3689B302EE6FAE2E9B8B9542C6CB922B2D4ADDB2783350E69F043ABE18F7413", - ["LogExpert.Resources.dll"] = "5A86521CBFC03CA74BE1CE6A8E8D678464F2E05606D2431701943699012935CE", + ["CsvColumnizer.dll"] = "B14C7D822278C2F500DA7C6334CE270571DDDCBAC9C37AC1B83F24FD4FB830CE", + ["CsvColumnizer.dll (x86)"] = "B14C7D822278C2F500DA7C6334CE270571DDDCBAC9C37AC1B83F24FD4FB830CE", + ["DefaultPlugins.dll"] = "F4A28A04F9436DA392C96D1C9B0D170028828F43C32CAF810979ED5BDB9B2D25", + ["FlashIconHighlighter.dll"] = "0CE4817376FE88CBF8CBE57E5618B70A55CAB65E3C415930F3D7576250C17207", + ["GlassfishColumnizer.dll"] = "98F431670B729AF7395FF06618C91A3C6D1848238D25D1BDF2FB9BA99898E260", + ["JsonColumnizer.dll"] = "8B265F2AEC35C0FE66BBCC991865E51BFBD230D7DED90A83219FF33DCE7A7048", + ["JsonCompactColumnizer.dll"] = "B89B13B5631E280F45C258DC97059C54869DDB301BD0469EBDCDFC40ED6535F4", + ["Log4jXmlColumnizer.dll"] = "37F5D952E453039936889F19FFC6674C7408011166865096AE61502E19827302", + ["LogExpert.Core.dll"] = "1696D36D01BC2D13BA8EA356E002225D8379F1CBB442B379B1E60D5A9B23EC93", + ["LogExpert.Resources.dll"] = "4BE617F7376269CEF31F7DCA49FBA73619488FCC24FD940E448D0516702DE368", ["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"] = "10D0C45ED700B39F97869E6C03FEB74E1478FA9013461C6F98A28059C892FE33", - ["SftpFileSystem.dll"] = "DEE611A0E108B60043C1EB79AB62CBF9CEB2EA851F0F0C7D1A797D69733BF815", - ["SftpFileSystem.dll (x86)"] = "B453CE200B3E7C35C65D2348663AB057DB8E067C720EADA92D70E5212120449D", - ["SftpFileSystem.Resources.dll"] = "C0AD198F7C34C793C5A6D8037AB8FEFB6E776601B5119A588ACEA5AD44F5EBC1", - ["SftpFileSystem.Resources.dll (x86)"] = "C0AD198F7C34C793C5A6D8037AB8FEFB6E776601B5119A588ACEA5AD44F5EBC1", + ["RegexColumnizer.dll"] = "AA23CFE9B60C0C68D9B521FC4DA6D866B6CF10A88DAF71C428408C9EBEC30154", + ["SftpFileSystem.dll"] = "69D4978350F9B50ADCFF4B1EF01BE05121DF569F9216473F893E4D76749CD1B6", + ["SftpFileSystem.dll (x86)"] = "BAD8F0EAA25CD8E9F5C9F42D614D5B222CA6C0FFBC988B7EEFD445DC940FD6DD", + ["SftpFileSystem.Resources.dll"] = "2B447430659DCC8795EF156D4438491CA8504C2C6E2C2000A3BAF523E5C4EBDD", + ["SftpFileSystem.Resources.dll (x86)"] = "2B447430659DCC8795EF156D4438491CA8504C2C6E2C2000A3BAF523E5C4EBDD", }; } From 48ab09f69e99a8b289c19e4e20fb144c84e4f04b Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 21 Jan 2026 16:19:41 +0100 Subject: [PATCH 8/9] DisposedExceptions while testing removed --- .../Dialogs/LogTabWindow/LogTabWindow.cs | 36 ++++++++++++++++++- .../Services/LedIndicatorService.cs | 27 +++++++++++--- src/LogExpert.UI/Services/TabController.cs | 10 ++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 8e178b651..c318f182d 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -1529,9 +1529,41 @@ private void FileRespawned (LogWindow.LogWindow logWin) [SupportedOSPlatform("windows")] private void SetTabIcon (LogWindow.LogWindow logWindow, Icon icon) { + if (logWindow == null | logWindow.IsDisposed) + { + return; + } + + if (icon == null) + { + logWindow.Icon = null; + return; + } + if (logWindow != null) { - logWindow.Icon = icon; + + try + { + var handle = icon.Handle; + logWindow.Icon = (Icon)icon.Clone(); + } + catch (ObjectDisposedException) + { + //Icon Disposed + return; + } + + if (logWindow.Tag is LogWindowData data && data.OwnedIcon != null) + { + data.OwnedIcon.Dispose(); + } + + if (logWindow.Tag is LogWindowData logWindowData) + { + logWindowData.OwnedIcon = logWindow.Icon; + } + logWindow.DockHandler.Pane?.TabStripControl.Invalidate(false); } } @@ -3143,6 +3175,8 @@ private class LogWindowData public ToolTip ToolTip { get; set; } + public Icon OwnedIcon { get; set; } + #endregion } } diff --git a/src/LogExpert.UI/Services/LedIndicatorService.cs b/src/LogExpert.UI/Services/LedIndicatorService.cs index ad241ed33..392bd8ab9 100644 --- a/src/LogExpert.UI/Services/LedIndicatorService.cs +++ b/src/LogExpert.UI/Services/LedIndicatorService.cs @@ -95,14 +95,17 @@ public void Dispose () return; } - _logger.Info("Disposing LedIndicatorService"); - + _disposed = true; Stop(); + Thread.Sleep(ANIMATION_INTERVAL_MS * 2); + + _logger.Info("Disposing LedIndicatorService"); + lock (_stateLock) { DisposeBrushes(); - DisposeIcons(); + //DisposeIcons(); _windowStates.Clear(); } @@ -197,7 +200,7 @@ public Icon GetDeadIcon () return !_isInitialized ? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.LogExpert_Common_Error_Message_ServiceNotInitialized, nameof(LedIndicatorService))) - : _deadIcon; + : (Icon)_deadIcon.Clone(); } /// @@ -418,11 +421,20 @@ public void RegenerateIcons (Color tailColor) { _logger.Info("Regenerating icons with new tail color: {Color}", tailColor); + bool wasRunning = _animationTimer != null && _animationTimer.Enabled; + + if (wasRunning) + { + Stop(); + Thread.Sleep(ANIMATION_INTERVAL_MS * 2); // Wait for pending ticks + } + lock (_stateLock) { // Dispose old resources DisposeBrushes(); - DisposeIcons(); + //DisposeIcons(); + _iconCache = null; // Create new ones CurrentTailColor = tailColor; @@ -444,6 +456,11 @@ public void RegenerateIcons (Color tailColor) OnIconChanged(window, icon); } } + + if (wasRunning) + { + Start(); + } } /// diff --git a/src/LogExpert.UI/Services/TabController.cs b/src/LogExpert.UI/Services/TabController.cs index 2cf1b911e..217da0ecd 100644 --- a/src/LogExpert.UI/Services/TabController.cs +++ b/src/LogExpert.UI/Services/TabController.cs @@ -183,6 +183,11 @@ public void CloseWindow (LogWindow window, bool skipConfirmation = false) return; } + if (!window.IsDisposed && window.IsHandleCreated) + { + window.Icon = null; + } + window.Close(skipConfirmation); // Note: RemoveWindow will be called by OnWindowDisposed event handler } @@ -432,6 +437,11 @@ protected virtual void Dispose (bool disposing) { foreach (var window in _windows.Keys) { + if (!window.IsDisposed && window.IsHandleCreated) + { + window.Icon = null; + } + window.Disposed -= OnWindowDisposed; window.Activated -= OnWindowActivated; } From 0c4ba2ac91f35fb6b5a7e719bd4f09691f8cdb2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 21 Jan 2026 15:22:42 +0000 Subject: [PATCH 9/9] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index b899daad1..1a8d5d2ee 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-01-16 12:28:08 UTC + /// Generated: 2026-01-21 15:22:41 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "0BFB2D25838DA085A00A97A8710E48F36E4FE188F9E976800CEBB5BDA10EDCF1", + ["AutoColumnizer.dll"] = "DD6CCB0648ABD5BBBFF11410C9A29208B0296777AFE7888F555F7A515CC4A102", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "B14C7D822278C2F500DA7C6334CE270571DDDCBAC9C37AC1B83F24FD4FB830CE", - ["CsvColumnizer.dll (x86)"] = "B14C7D822278C2F500DA7C6334CE270571DDDCBAC9C37AC1B83F24FD4FB830CE", - ["DefaultPlugins.dll"] = "F4A28A04F9436DA392C96D1C9B0D170028828F43C32CAF810979ED5BDB9B2D25", - ["FlashIconHighlighter.dll"] = "0CE4817376FE88CBF8CBE57E5618B70A55CAB65E3C415930F3D7576250C17207", - ["GlassfishColumnizer.dll"] = "98F431670B729AF7395FF06618C91A3C6D1848238D25D1BDF2FB9BA99898E260", - ["JsonColumnizer.dll"] = "8B265F2AEC35C0FE66BBCC991865E51BFBD230D7DED90A83219FF33DCE7A7048", - ["JsonCompactColumnizer.dll"] = "B89B13B5631E280F45C258DC97059C54869DDB301BD0469EBDCDFC40ED6535F4", - ["Log4jXmlColumnizer.dll"] = "37F5D952E453039936889F19FFC6674C7408011166865096AE61502E19827302", - ["LogExpert.Core.dll"] = "1696D36D01BC2D13BA8EA356E002225D8379F1CBB442B379B1E60D5A9B23EC93", - ["LogExpert.Resources.dll"] = "4BE617F7376269CEF31F7DCA49FBA73619488FCC24FD940E448D0516702DE368", + ["CsvColumnizer.dll"] = "09DC67DE9D2DBD071A188E547FFCE2B4100359BA8028EF30DF171EF72932EBB7", + ["CsvColumnizer.dll (x86)"] = "09DC67DE9D2DBD071A188E547FFCE2B4100359BA8028EF30DF171EF72932EBB7", + ["DefaultPlugins.dll"] = "1A7969B4BEF1B6455227C01CF4D5C65F3827F28F6E0AE9BC32B10C67FB49624D", + ["FlashIconHighlighter.dll"] = "EB6DC6AD906C3046D50A585AD47B1E3DA5689C780F332ED5BB954060D14F5FEF", + ["GlassfishColumnizer.dll"] = "F7EFF40DE607081517D19B436B70525867331219F13C30044CE45E0AF2720B5D", + ["JsonColumnizer.dll"] = "C567E56354384525CA8B4EA99CA2A352EA440D6E50F089D74D917849CB4ADF1F", + ["JsonCompactColumnizer.dll"] = "391EB6C05C0BA56BA8B1FFA45C34CE034C4E3633A3FDE8B664D927867B0881B7", + ["Log4jXmlColumnizer.dll"] = "63F70818A3F23F81C2DA7ACC219670BF9B7998505A7D0269F87B51EC0122D6CB", + ["LogExpert.Core.dll"] = "80FB1059C0C402D43CE3D42A7041C5C20F86A84BD17D25C3F3868E9856EEB9ED", + ["LogExpert.Resources.dll"] = "3C9D2EB602DC8C981D97E0069A8D9060B07A9E0B998DE1A3E415C3E9B9F4492E", ["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"] = "AA23CFE9B60C0C68D9B521FC4DA6D866B6CF10A88DAF71C428408C9EBEC30154", - ["SftpFileSystem.dll"] = "69D4978350F9B50ADCFF4B1EF01BE05121DF569F9216473F893E4D76749CD1B6", - ["SftpFileSystem.dll (x86)"] = "BAD8F0EAA25CD8E9F5C9F42D614D5B222CA6C0FFBC988B7EEFD445DC940FD6DD", - ["SftpFileSystem.Resources.dll"] = "2B447430659DCC8795EF156D4438491CA8504C2C6E2C2000A3BAF523E5C4EBDD", - ["SftpFileSystem.Resources.dll (x86)"] = "2B447430659DCC8795EF156D4438491CA8504C2C6E2C2000A3BAF523E5C4EBDD", + ["RegexColumnizer.dll"] = "446733728A08D38600524C5FD2DD35A65F2793CE4E62141213683CA1E00321A6", + ["SftpFileSystem.dll"] = "FBE895218363E9D7C47FC037F2325D82D8E7768121AE76E4449FFF4213BF6093", + ["SftpFileSystem.dll (x86)"] = "2A4A93BEEBE1E70EF9C0BD12E9440A05C134F8B788831C4D04A2AF2DA762189E", + ["SftpFileSystem.Resources.dll"] = "E5BF923779D9381CB0246691E90673143DC019727A60889C5C129E7DAAE31C6D", + ["SftpFileSystem.Resources.dll (x86)"] = "E5BF923779D9381CB0246691E90673143DC019727A60889C5C129E7DAAE31C6D", }; }