diff --git a/src/ColumnizerLib/Extensions/Extensions.cs b/src/ColumnizerLib/Extensions/Extensions.cs new file mode 100644 index 00000000..f0071ee6 --- /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 698d9e5f..00000000 --- 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/CsvColumnizer/Resources.de.resx b/src/CsvColumnizer/Resources.de.resx index a0b3b097..0422a468 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.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index f8d64146..1c373065 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.Core/Interface/ILogView.cs b/src/LogExpert.Core/Interface/ILogView.cs index 5dbc0e80..8427d0f1 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.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 06018dff..b98b1101 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 c1ec0c3d..46a7a27a 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 efeac89a..b71dc928 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.Tests/Services/TabControllerTests.cs b/src/LogExpert.Tests/Services/TabControllerTests.cs new file mode 100644 index 00000000..13e65d4b --- /dev/null +++ b/src/LogExpert.Tests/Services/TabControllerTests.cs @@ -0,0 +1,436 @@ +using System.Runtime.Versioning; +using System.Windows.Forms; + +using LogExpert.UI.Controls.LogWindow; +using LogExpert.UI.Services; + +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")] +[Apartment(ApartmentState.STA)] // Required for WinForms controls +internal class TabControllerTests : IDisposable +{ + private Form _testForm; + private DockPanel _dockPanel; + private TabController _tabController; + private bool _disposed; + + [SetUp] + public void Setup() + { + // 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() + { + _tabController?.Dispose(); + _testForm?.Close(); + _testForm?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _tabController?.Dispose(); + _testForm?.Dispose(); + } + + _disposed = true; + } + + #region Constructor Tests + + [Test] + 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 + using var controller = new TabController(); + + // Act + controller.InitializeDockPanel(_dockPanel); + + // 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 InitializeDockPanel_WhenAlreadyInitialized_ThrowsInvalidOperationException() + { + // Arrange + 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(() => controller.InitializeDockPanel(dockPanel2)); + } + + #endregion + + #region GetAllWindowsFromDockPanel Tests + + [Test] + public void GetAllWindowsFromDockPanel_WhenNotInitialized_ReturnsEmptyList() + { + // Arrange + using var controller = new TabController(); + + // Act + var result = controller.GetAllWindowsFromDockPanel(); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public void GetAllWindowsFromDockPanel_WhenInitializedButEmpty_ReturnsEmptyList() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.GetAllWindowsFromDockPanel(); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public void GetAllWindowsFromDockPanel_ReturnsReadOnlyList() + { + // Arrange - already done in Setup + + // Act + 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(result, Is.Empty); + Assert.That(result, Is.InstanceOf>()); + } + + #endregion + + #region GetWindowCount Tests + + [Test] + public void GetWindowCount_WhenEmpty_ReturnsZero() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.GetWindowCount(); + + // Assert + Assert.That(result, Is.EqualTo(0)); + } + + #endregion + + #region HasWindow Tests + + [Test] + public void HasWindow_WithNullWindow_ReturnsFalse() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.HasWindow(null); + + // Assert + Assert.That(result, Is.False); + } + + #endregion + + #region GetActiveWindow Tests + + [Test] + public void GetActiveWindow_WhenNoWindowActive_ReturnsNull() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.GetActiveWindow(); + + // Assert + Assert.That(result, Is.Null); + } + + #endregion + + #region FindWindowByFileName Tests + + [Test] + public void FindWindowByFileName_WithNullFileName_ReturnsNull() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.FindWindowByFileName(null); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void FindWindowByFileName_WithEmptyFileName_ReturnsNull() + { + // Arrange - already done in Setup + + // Act + var result = _tabController.FindWindowByFileName(string.Empty); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void FindWindowByFileName_WhenNoWindowsExist_ReturnsNull() + { + // 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 4752bc3b..1094b098 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 3285cd19..8e178b65 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,8 +43,9 @@ internal partial class LogTabWindow : Form, ILogTabWindow private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly Icon _deadIcon; - private readonly ILedIndicatorService _ledService; - private readonly Lock _windowListLock = new(); + private readonly LedIndicatorService _ledService; + + private readonly TabController _tabController; private bool _disposed; @@ -53,7 +53,6 @@ internal partial class LogTabWindow : Form, ILogTabWindow private readonly int _instanceNumber; - private readonly IList _logWindowList = []; private readonly bool _showInstanceNumbers; private readonly string[] _startupFileNames; @@ -84,6 +83,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; @@ -617,49 +622,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); } } @@ -713,7 +815,7 @@ public HighlightGroup FindHighlightGroupByFileMask (string fileName) public void SelectTab (ILogWindow logWindow) { - logWindow.Activate(); + _tabController.ActivateWindow(logWindow as LogWindow.LogWindow); } [SupportedOSPlatform("windows")] @@ -762,12 +864,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; @@ -867,14 +967,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); } } } @@ -942,6 +1039,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) { @@ -950,23 +1054,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; @@ -975,10 +1069,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")] @@ -993,7 +1083,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; @@ -1007,21 +1097,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")] @@ -1041,30 +1125,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); } @@ -1536,13 +1611,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); @@ -1588,15 +1663,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")] @@ -1720,22 +1795,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 @@ -1837,7 +1897,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 @@ -1845,7 +1905,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"); } @@ -2066,7 +2126,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); } @@ -2143,23 +2203,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(); } } } @@ -2175,16 +2232,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(); } } + } } } @@ -2435,14 +2490,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(); } } @@ -2671,12 +2723,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; @@ -2695,9 +2744,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) @@ -2780,15 +2829,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/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index d56b1e01..3f324872 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/Entities/LogWindowMetadata.cs b/src/LogExpert.UI/Entities/LogWindowMetadata.cs new file mode 100644 index 00000000..40e3d865 --- /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/Extensions/ComboBoxExtensions.cs b/src/LogExpert.UI/Extensions/ComboBoxExtensions.cs deleted file mode 100644 index 9cd24a7a..00000000 --- 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 b4f4af87..00000000 --- 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 00000000..13b31df8 --- /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 f792a0e5..060645c9 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 00000000..319cfe06 --- /dev/null +++ b/src/LogExpert.UI/Services/ITabController.cs @@ -0,0 +1,138 @@ +using LogExpert.UI.Controls.LogWindow; + +using WeifenLuo.WinFormsUI.Docking; + +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 (); + + /// + /// 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 (); + + /// + /// 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; +} diff --git a/src/LogExpert.UI/Services/TabController.cs b/src/LogExpert.UI/Services/TabController.cs new file mode 100644 index 00000000..2cf1b911 --- /dev/null +++ b/src/LogExpert.UI/Services/TabController.cs @@ -0,0 +1,461 @@ +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(Resources.TabController_Error_Message_AlreadInitialized); + } + + _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(Resources.TabController_Error_Message_NotInitialized); + } + + lock (_windowsLock) + { + if (_windows.ContainsKey(window)) + { + throw new InvalidOperationException(Resources.TabController_Error_Message_WindowAlreadyTracked); + } + + 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; + } + + /// + /// 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 +} diff --git a/src/LogExpert.UI/Services/WindowActivatedEventArgs.cs b/src/LogExpert.UI/Services/WindowActivatedEventArgs.cs new file mode 100644 index 00000000..806f99cb --- /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 00000000..4277efe7 --- /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 00000000..df073838 --- /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 00000000..305d4a8d --- /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; +} diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 76362dd7..b899daad 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:28:08 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "2B9AF25F395E12C119B097B8F3ACADBE4E39D0644CBC9C76C6F9D455A048D06B", + ["AutoColumnizer.dll"] = "0BFB2D25838DA085A00A97A8710E48F36E4FE188F9E976800CEBB5BDA10EDCF1", ["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"] = "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"] = "E61F23C064D42E714383D2B948AA342A54E8A57FC94976C7BF53B7A4F4D9D78C", - ["SftpFileSystem.dll"] = "4B34EF6D27630302FB5EAC5E24E2E351D34C1A35517ACD8CC78D982324D801F9", - ["SftpFileSystem.dll (x86)"] = "B01F7467A14018CB1FF5A3E919A68E8C58CA98F93820AE17C77E47898376AA98", - ["SftpFileSystem.Resources.dll"] = "01DB02CA8CE8047FD4552A359C31DDD3100A8CE4A471A1E1FB7FE67CAA1B546D", - ["SftpFileSystem.Resources.dll (x86)"] = "01DB02CA8CE8047FD4552A359C31DDD3100A8CE4A471A1E1FB7FE67CAA1B546D", + ["RegexColumnizer.dll"] = "AA23CFE9B60C0C68D9B521FC4DA6D866B6CF10A88DAF71C428408C9EBEC30154", + ["SftpFileSystem.dll"] = "69D4978350F9B50ADCFF4B1EF01BE05121DF569F9216473F893E4D76749CD1B6", + ["SftpFileSystem.dll (x86)"] = "BAD8F0EAA25CD8E9F5C9F42D614D5B222CA6C0FFBC988B7EEFD445DC940FD6DD", + ["SftpFileSystem.Resources.dll"] = "2B447430659DCC8795EF156D4438491CA8504C2C6E2C2000A3BAF523E5C4EBDD", + ["SftpFileSystem.Resources.dll (x86)"] = "2B447430659DCC8795EF156D4438491CA8504C2C6E2C2000A3BAF523E5C4EBDD", }; }