diff --git a/src/LogExpert.Configuration/ConfigManager.cs b/src/LogExpert.Configuration/ConfigManager.cs index 4b80b9e3..f73c2c57 100644 --- a/src/LogExpert.Configuration/ConfigManager.cs +++ b/src/LogExpert.Configuration/ConfigManager.cs @@ -315,15 +315,11 @@ public void AddToFileHistory (string fileName) Save(SettingsFlags.FileHistory); } - public void RemoveFromFileHistory (string fileName) + public void ClearFileHistory () { - bool findName (string s) => s.ToUpperInvariant().Equals(fileName.ToUpperInvariant(), StringComparison.Ordinal); - - var index = Instance.Settings.FileHistoryList.FindIndex(findName); - - if (index != -1) + lock (_loadSaveLock) { - Instance.Settings.FileHistoryList.RemoveAt(index); + Instance.Settings.FileHistoryList.Clear(); } Save(SettingsFlags.FileHistory); diff --git a/src/LogExpert.Core/Interfaces/IConfigManager.cs b/src/LogExpert.Core/Interfaces/IConfigManager.cs index ac523daf..8d430811 100644 --- a/src/LogExpert.Core/Interfaces/IConfigManager.cs +++ b/src/LogExpert.Core/Interfaces/IConfigManager.cs @@ -177,10 +177,9 @@ public interface IConfigManager void AddToFileHistory (string fileName); /// - /// Removes the specified file name from the file history list. + /// Removes all entries from the file history list. /// - /// The name of the file to remove from the file history list. Comparison is case-insensitive. - void RemoveFromFileHistory (string fileName); + void ClearFileHistory (); /// /// Clears the list of recently opened files. diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 6bf4905d..5e9f174d 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -2383,6 +2383,15 @@ public static string LogTabWindow_UI_ToolStripMenuItem_cellSelectModeToolStripMe } } + /// + /// Looks up a localized string similar to Clear recently used. + /// + public static string LogTabWindow_UI_ToolStripMenuItem_clearFileHistoryToolStripMenuItem { + get { + return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_clearFileHistoryToolStripMenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close all tabs. /// diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index 67ef82bf..736bc8a4 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -1240,6 +1240,9 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools Zuletzt benutzt + + Zuletzt benutzte löschen + Exit diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index bd9b3a53..441e4b87 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -1176,6 +1176,9 @@ Checked tools will appear in the icon bar. All other tools are available in the Last used + + Clear recently used + Exit diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx index 9b875814..58f62e03 100644 --- a/src/LogExpert.Resources/Resources.zh-CN.resx +++ b/src/LogExpert.Resources/Resources.zh-CN.resx @@ -1046,6 +1046,9 @@ 最近使用 + + 清除最近使用 + 退出 diff --git a/src/LogExpert.Tests/ConfigManagerTests/ClearFileHistoryTests.cs b/src/LogExpert.Tests/ConfigManagerTests/ClearFileHistoryTests.cs new file mode 100644 index 00000000..7b24ebb0 --- /dev/null +++ b/src/LogExpert.Tests/ConfigManagerTests/ClearFileHistoryTests.cs @@ -0,0 +1,113 @@ +using LogExpert.Core.Config; + +using Newtonsoft.Json; + +using NUnit.Framework; + +namespace LogExpert.Tests.ConfigManagerTests; + +[TestFixture] +public class ClearFileHistoryTests +{ + private string _testDir; + private Configuration.ConfigManager _configManager; + private object? _originalSettings; + private bool _originalIsInitialized; + private string _originalConfigDir; + + [SetUp] + public void SetUp () + { + _configManager = Configuration.ConfigManager.Instance; + _originalIsInitialized = ConfigManagerTestReflection.GetPrivateField(_configManager, "_isInitialized") is true; + _originalSettings = ConfigManagerTestReflection.GetPrivateField(_configManager, "_settings"); + _originalConfigDir = _configManager.ConfigDir; + + _testDir = Path.Join(Path.GetTempPath(), "LogExpert_ClearHistoryTest_" + Guid.NewGuid().ToString("N")); + _ = Directory.CreateDirectory(_testDir); + + ConfigManagerTestReflection.ResetConfigManagerInitialization(_configManager); + _configManager.Initialize(_testDir, new Rectangle(0, 0, 1920, 1080)); + _configManager.ConfigDir = _testDir; + } + + [TearDown] + public void TearDown () + { + ConfigManagerTestReflection.SetPrivateField(_configManager, "_isInitialized", _originalIsInitialized); + ConfigManagerTestReflection.SetPrivateField(_configManager, "_settings", _originalSettings); + _configManager.ConfigDir = _originalConfigDir; + + try + { + if (Directory.Exists(_testDir)) + { + Directory.Delete(_testDir, recursive: true); + } + } + catch (IOException) + { + // Ignore IO errors during cleanup + } + catch (UnauthorizedAccessException) + { + // Ignore access errors during cleanup + } + } + + [Test] + public void ClearFileHistory_RemovesAllEntries () + { + _configManager.AddToFileHistory(@"C:\logs\one.log"); + _configManager.AddToFileHistory(@"C:\logs\two.log"); + Assert.That(_configManager.Settings.FileHistoryList, Has.Count.EqualTo(2)); + + _configManager.ClearFileHistory(); + + Assert.That(_configManager.Settings.FileHistoryList, Is.Empty); + } + + [Test] + public void ClearFileHistory_WritesEmptyListToSettingsFile () + { + _configManager.AddToFileHistory(@"C:\logs\one.log"); + + _configManager.ClearFileHistory(); + + var settingsFile = Path.Join(_configManager.ActiveConfigDir, "settings.json"); + Assert.That(File.Exists(settingsFile), Is.True); + + var saved = JsonConvert.DeserializeObject(File.ReadAllText(settingsFile)); + Assert.That(saved, Is.Not.Null); + Assert.That(saved.FileHistoryList, Is.Empty); + } + + [Test] + public void ClearFileHistory_LeavesOtherHistoryListsUntouched () + { + _configManager.AddToFileHistory(@"C:\logs\one.log"); + _configManager.Settings.LastOpenFilesList.Add(@"C:\logs\one.log"); + _configManager.Settings.SearchHistoryList.Add("error"); + _configManager.Settings.FilterHistoryList.Add("WARN"); + _configManager.Settings.FilterRangeHistoryList.Add("10"); + _configManager.Settings.UriHistoryList.Add("https://example.org/one.log"); + _configManager.Settings.RegexHistory.ExpressionHistoryList.Add(@"^\d+"); + _configManager.Settings.RegexHistory.TesttextHistoryList.Add("42"); + _configManager.Settings.ColumnizerHistoryList.Add(new ColumnizerHistoryEntry(@"C:\logs\one.log", "JsonColumnizer")); + + _configManager.ClearFileHistory(); + + Assert.Multiple(() => + { + Assert.That(_configManager.Settings.FileHistoryList, Is.Empty); + Assert.That(_configManager.Settings.LastOpenFilesList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.SearchHistoryList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.FilterHistoryList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.FilterRangeHistoryList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.UriHistoryList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.RegexHistory.ExpressionHistoryList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.RegexHistory.TesttextHistoryList, Has.Count.EqualTo(1)); + Assert.That(_configManager.Settings.ColumnizerHistoryList, Has.Count.EqualTo(1)); + }); + } +} diff --git a/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerPortableModeTests.cs b/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerPortableModeTests.cs index bb98741f..11729834 100644 --- a/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerPortableModeTests.cs +++ b/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerPortableModeTests.cs @@ -26,7 +26,7 @@ public void SetUp () _configManager = Configuration.ConfigManager.Instance; // Reset the singleton's initialization state using reflection - ResetConfigManagerInitialization(); + ConfigManagerTestReflection.ResetConfigManagerInitialization(_configManager); _configManager.Initialize(_testDir, new Rectangle(0, 0, 1920, 1080)); } @@ -64,19 +64,6 @@ public void TearDown () #region Helper Methods - /// - /// Resets ConfigManager singleton initialization state via reflection. - /// - private void ResetConfigManagerInitialization () - { - var isInitializedField = typeof(Configuration.ConfigManager).GetField("_isInitialized", BindingFlags.NonPublic | BindingFlags.Instance); - isInitializedField?.SetValue(_configManager, false); - - // Reset settings so they reload from the new path - var settingsField = typeof(Configuration.ConfigManager).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance); - settingsField?.SetValue(_configManager, null); - } - /// /// Invokes a private instance method using reflection. /// @@ -758,7 +745,7 @@ public void Load_NewPortableLayout_DetectedCorrectly () CreateSettingsFile(_configManager.PortableConfigDir); // Reset settings to force reload - ResetConfigManagerInitialization(); + ConfigManagerTestReflection.ResetConfigManagerInitialization(_configManager); _configManager.Initialize(_testDir, new Rectangle(0, 0, 1920, 1080)); // Act - Access Settings to trigger Load() @@ -783,7 +770,7 @@ public void Load_OldPortableLayout_TriggersMigration () """{ "Preferences": { "FontName": "OldLayout" }, "FilterList": [] }"""); // Reset settings to force reload - ResetConfigManagerInitialization(); + ConfigManagerTestReflection.ResetConfigManagerInitialization(_configManager); _configManager.Initialize(_testDir, new Rectangle(0, 0, 1920, 1080)); // Act - Access Settings to trigger Load() which should migrate diff --git a/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerTestReflection.cs b/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerTestReflection.cs new file mode 100644 index 00000000..5b2f8084 --- /dev/null +++ b/src/LogExpert.Tests/ConfigManagerTests/ConfigManagerTestReflection.cs @@ -0,0 +1,40 @@ +using System.Reflection; + +using LogExpert.Core.Config; + +namespace LogExpert.Tests.ConfigManagerTests; + +/// +/// Shared reflection helpers for fixtures that reset the ConfigManager singleton +/// between tests. +/// +internal static class ConfigManagerTestReflection +{ + /// + /// Resets the ConfigManager singleton's initialization state so the next + /// Initialize call reloads settings from its own directory. + /// + internal static void ResetConfigManagerInitialization (Configuration.ConfigManager configManager) + { + SetPrivateField(configManager, "_isInitialized", false); + + // Reset settings so they reload from the new path + SetPrivateField(configManager, "_settings", null); + } + + internal static object? GetPrivateField (Configuration.ConfigManager configManager, string fieldName) + { + return Field(fieldName).GetValue(configManager); + } + + internal static void SetPrivateField (Configuration.ConfigManager configManager, string fieldName, object? value) + { + Field(fieldName).SetValue(configManager, value); + } + + private static FieldInfo Field (string fieldName) + { + return typeof(Configuration.ConfigManager).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException($"ConfigManager.{fieldName} is gone; update the shared reset helper."); + } +} diff --git a/src/LogExpert.UI.Tests/Services/MenuToolbarControllerTests.cs b/src/LogExpert.UI.Tests/Services/MenuToolbarControllerTests.cs index f41d720d..c97bd246 100644 --- a/src/LogExpert.UI.Tests/Services/MenuToolbarControllerTests.cs +++ b/src/LogExpert.UI.Tests/Services/MenuToolbarControllerTests.cs @@ -268,8 +268,45 @@ public void PopulateFileHistory_CreatesMenuItems () _controller.PopulateFileHistory(history); var lastUsed = FindMenuItem("lastUsedToolStripMenuItem"); - Assert.That(lastUsed.DropDownItems, Has.Count.EqualTo(2)); + Assert.That(lastUsed.DropDownItems, Has.Count.EqualTo(4)); Assert.That(lastUsed.DropDownItems[0].Text, Is.EqualTo(@"C:\log1.txt")); + Assert.That(lastUsed.DropDownItems[2], Is.TypeOf()); + Assert.That(lastUsed.DropDownItems[3].Name, Is.EqualTo("clearFileHistoryToolStripMenuItem")); + Assert.That(lastUsed.DropDownItems[3].Enabled, Is.True); + } + + [Test] + public void PopulateFileHistory_DisablesClearItem_WhenHistoryIsEmpty () + { + _controller.PopulateFileHistory([]); + + var lastUsed = FindMenuItem("lastUsedToolStripMenuItem"); + Assert.That(lastUsed.DropDownItems, Has.Count.EqualTo(1)); + Assert.That(lastUsed.DropDownItems[0].Name, Is.EqualTo("clearFileHistoryToolStripMenuItem")); + Assert.That(lastUsed.DropDownItems[0].Enabled, Is.False); + } + + [Test] + public void ClearHistoryItem_RaisesFileHistoryClearRequested_AndNotFileClicked () + { + var clearRequested = 0; + string clickedFile = null; + _controller.FileHistoryClearRequested += (_, _) => clearRequested++; + _controller.HistoryItemClicked += (_, e) => clickedFile = e.FileName; + + _controller.PopulateFileHistory(["test.log"]); + var lastUsed = FindMenuItem("lastUsedToolStripMenuItem"); + var clearItem = lastUsed.DropDownItems[^1]; + + var args = new ToolStripItemClickedEventArgs(clearItem); + var onItemClicked = typeof(ToolStrip).GetMethod("OnItemClicked", BindingFlags.Instance | BindingFlags.NonPublic); + _ = onItemClicked.Invoke(lastUsed.DropDown, [args]); + + Assert.Multiple(() => + { + Assert.That(clearRequested, Is.EqualTo(1)); + Assert.That(clickedFile, Is.Null); + }); } [Test] diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 5ec992ae..3b1218b3 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -235,7 +235,7 @@ private void OnFileOperationServiceFileOpened (object? sender, FileOpenedEventAr private void InitializeMenuToolbarControllerEvents () { _menuToolbarController.HistoryItemClicked += OnMenuControllerHistoryItemClicked; - _menuToolbarController.HistoryItemRemoveRequested += OnMenuControllerHistoryItemRemoveRequested; + _menuToolbarController.FileHistoryClearRequested += OnMenuControllerFileHistoryClearRequested; _menuToolbarController.HighlightGroupSelected += OnMenuControllerHighlightGroupSelected; } @@ -244,10 +244,11 @@ private void OnMenuControllerHighlightGroupSelected (object? sender, HighlightGr CurrentLogWindow?.SetCurrentHighlightGroup(e.GroupName); } - private void OnMenuControllerHistoryItemRemoveRequested (object? sender, HistoryItemClickedEventArgs e) + private void OnMenuControllerFileHistoryClearRequested (object? sender, EventArgs e) { - ConfigManager.RemoveFromFileHistory(e.FileName); - FillHistoryMenu(); + // ClearFileHistory persists via Save, whose ConfigChanged event reaches + // ApplySettings and rebuilds the history menu from there. + ConfigManager.ClearFileHistory(); } private void OnMenuControllerHistoryItemClicked (object? sender, HistoryItemClickedEventArgs e) diff --git a/src/LogExpert.UI/Interface/IMenuToolbarController.cs b/src/LogExpert.UI/Interface/IMenuToolbarController.cs index da3a921a..8bd491d8 100644 --- a/src/LogExpert.UI/Interface/IMenuToolbarController.cs +++ b/src/LogExpert.UI/Interface/IMenuToolbarController.cs @@ -45,13 +45,15 @@ void InitializeMenus (MenuStrip mainMenu, ToolStrip buttonToolbar, ToolStrip ext void UpdateHighlightGroups (IEnumerable groups, string selectedGroup); /// - /// Populates file history menu with recent files. - /// Includes right-click removal support. + /// Populates the file history menu with recent files, followed by a + /// separator and a "clear recently used" row that raises + /// when clicked. + /// The clear row is disabled when the history is empty. /// void PopulateFileHistory (IEnumerable fileHistory); // Events event EventHandler HistoryItemClicked; - event EventHandler HistoryItemRemoveRequested; + event EventHandler FileHistoryClearRequested; event EventHandler HighlightGroupSelected; } diff --git a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs index 5e1644f0..55e6a30f 100644 --- a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs +++ b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs @@ -49,13 +49,15 @@ internal sealed class MenuToolbarController : IMenuToolbarController // History menu private ToolStripMenuItem _lastUsedMenuItem; + private ToolStripMenuItem _clearFileHistoryItem; + private readonly SynchronizationContext _uiContext; private readonly int _uiThreadId = Environment.CurrentManagedThreadId; private bool _disposed; private bool _suppressEvents; public event EventHandler HistoryItemClicked; - public event EventHandler HistoryItemRemoveRequested; + public event EventHandler FileHistoryClearRequested; public event EventHandler HighlightGroupSelected; public MenuToolbarController () @@ -265,7 +267,6 @@ public void PopulateFileHistory (IEnumerable fileHistory) if (_lastUsedMenuItem.DropDown != null) { _lastUsedMenuItem.DropDown.ItemClicked -= OnHistoryMenuItemClicked; - _lastUsedMenuItem.DropDown.MouseUp -= OnHistoryStripMouseUp; } var strip = new ToolStripDropDownMenu(); @@ -275,8 +276,20 @@ public void PopulateFileHistory (IEnumerable fileHistory) _ = strip.Items.Add(new ToolStripMenuItem(file)); } + if (strip.Items.Count > 0) + { + _ = strip.Items.Add(new ToolStripSeparator()); + } + + _clearFileHistoryItem = new ToolStripMenuItem(Resources.LogTabWindow_UI_ToolStripMenuItem_clearFileHistoryToolStripMenuItem) + { + Name = "clearFileHistoryToolStripMenuItem", + Enabled = strip.Items.Count > 0 + }; + + _ = strip.Items.Add(_clearFileHistoryItem); + strip.ItemClicked += OnHistoryMenuItemClicked; - strip.MouseUp += OnHistoryStripMouseUp; _lastUsedMenuItem.DropDown = strip; } @@ -302,28 +315,16 @@ private void OnHighlightGroupComboSelectedIndexChanged (object sender, EventArgs private void OnHistoryMenuItemClicked (object sender, ToolStripItemClickedEventArgs e) { - var fileName = e.ClickedItem?.Text; - if (!string.IsNullOrEmpty(fileName)) - { - HistoryItemClicked?.Invoke(this, new HistoryItemClickedEventArgs(fileName)); - } - } - - private void OnHistoryStripMouseUp (object sender, MouseEventArgs e) - { - // Right-click to remove from history (preserves existing LogTabWindow behavior) - if (e.Button != MouseButtons.Right) + if (ReferenceEquals(e.ClickedItem, _clearFileHistoryItem)) { + FileHistoryClearRequested?.Invoke(this, EventArgs.Empty); return; } - if (sender is ToolStripDropDownMenu strip) + var fileName = e.ClickedItem?.Text; + if (!string.IsNullOrEmpty(fileName)) { - var item = strip.GetItemAt(e.Location); - if (item != null && !string.IsNullOrEmpty(item.Text)) - { - HistoryItemRemoveRequested?.Invoke(this, new HistoryItemClickedEventArgs(item.Text)); - } + HistoryItemClicked?.Invoke(this, new HistoryItemClickedEventArgs(fileName)); } } @@ -415,7 +416,6 @@ public void Dispose () if (_lastUsedMenuItem?.DropDown != null) { _lastUsedMenuItem.DropDown.ItemClicked -= OnHistoryMenuItemClicked; - _lastUsedMenuItem.DropDown.MouseUp -= OnHistoryStripMouseUp; } _disposed = true;