From 6da0ae120ca7619828aecfc16f273980fa8661dd Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 30 Jul 2026 09:49:13 +0200 Subject: [PATCH 1/2] feat: add GB2312 encoding and de-duplicate the UTF-8 encoding rows Closes #688. GB2312 (code page 936) is now offered both as the Preferences default encoding and as a row in View > Encoding, so a simplified-Chinese log can be read without relying on the machine default. The issue also reported "two utf-8 encoding". The encoding menu built its row labels from Encoding..HeaderName in the constructor, after the localized resource labels had been applied. Encoding.Default is UTF-8 on .NET, so the "ANSI" row rendered as "utf-8" next to the "UTF8" row, which rendered as "utf-8" as well - two rows applying the same encoding under the same name. The ANSI row is dropped (the Preferences combo lost the same duplicate in #673, for the same reason) and the HeaderName overwrite with it, so the remaining rows keep their translated labels. Row check state is now matched by code page rather than by runtime type and equality. That ordering had a defect of its own: clicking UTF8 applies UTF-8 without a BOM, which compares equal to Encoding.Default, so the checkmark landed on the ANSI row instead. Pinned by tests at every layer the choice passes through: the offered list, the menu check state, the settings JSON and .lxp round trips, the reader stack (GB2312 is the first offered encoding that is neither single-byte nor Unicode, so the direct reader's byte-position tracking is asserted against the system reader on mixed ASCII/Chinese lines). --- .../Helpers/EncodingRegistry.cs | 11 +++ src/LogExpert.Resources/Resources.Designer.cs | 12 ++-- src/LogExpert.Resources/Resources.de.resx | 6 +- src/LogExpert.Resources/Resources.resx | 6 +- src/LogExpert.Resources/Resources.zh-CN.resx | 6 +- .../SettingsDialogEncodingListTests.cs | 1 + .../Encodings/EncodingJsonConverterTests.cs | 1 + .../Encodings/EncodingRegistryTests.cs | 2 + .../Encodings/PersisterXmlEncodingTests.cs | 1 + .../Services/FileOperationServiceTests.cs | 1 + .../Services/MenuToolbarControllerTests.cs | 52 +++++++++++++- .../LogfileReaderEncodingTests.cs | 29 ++++++++ .../PositionAwareStreamReaderDirectTests.cs | 16 +++++ .../Dialogs/LogTabWindow/LogTabWindow.cs | 23 +++--- .../LogTabWindow/LogTabWindow.designer.cs | 27 ++++--- src/LogExpert.UI/Dialogs/SettingsDialog.cs | 11 ++- .../Interface/IMenuToolbarController.cs | 4 +- .../MenuToolbarController.cs | 72 +++++++++++++------ 18 files changed, 210 insertions(+), 71 deletions(-) diff --git a/src/LogExpert.Core/Helpers/EncodingRegistry.cs b/src/LogExpert.Core/Helpers/EncodingRegistry.cs index 716e4f67c..1424059d2 100644 --- a/src/LogExpert.Core/Helpers/EncodingRegistry.cs +++ b/src/LogExpert.Core/Helpers/EncodingRegistry.cs @@ -27,6 +27,17 @@ namespace LogExpert.Core.Helpers; /// public static class EncodingRegistry { + /// + /// Code page of GB2312, the simplified-Chinese code page (issue #688). + /// + /// + /// Named — unlike the code pages that appear once, as a bare number, at the single place that + /// offers them — because three call sites have to agree on it: the Preferences default-encoding + /// list, the encoding menu row that applies it, and the check-state lookup that has to recognise a + /// file already being read with it. + /// + public const int CODE_PAGE_GB2312 = 936; + /// /// Registers on first use. /// diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 830b727ae..866168338 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -2285,20 +2285,20 @@ public static string LogTabWindow_UI_ToolStripMenuItem_dumpLogBufferInfoToolStri } /// - /// Looks up a localized string similar to ANSI. + /// Looks up a localized string similar to ASCII. /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem { + public static string LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem { get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem", resourceCulture); + return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem", resourceCulture); } } /// - /// Looks up a localized string similar to ASCII. + /// Looks up a localized string similar to GB2312. /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem { + public static string LogTabWindow_UI_ToolStripMenuItem_encodingGB2312toolStripMenuItem { get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem", resourceCulture); + return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingGB2312toolStripMenuItem", resourceCulture); } } diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index 162fb6eec..0ff9623ac 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -1327,9 +1327,6 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools ASCII - - ANSI - ISO-8859-1 @@ -1339,6 +1336,9 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools Unicode + + GB2312 + +00:00:00.000 diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index 5df529c37..6a3ae3eec 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -1350,9 +1350,6 @@ Checked tools will appear in the icon bar. All other tools are available in the ASCII - - ANSI - ISO-8859-1 @@ -1362,6 +1359,9 @@ Checked tools will appear in the icon bar. All other tools are available in the Unicode + + GB2312 + +00:00:00.000 diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx index b3ea6c935..8e58e5a8c 100644 --- a/src/LogExpert.Resources/Resources.zh-CN.resx +++ b/src/LogExpert.Resources/Resources.zh-CN.resx @@ -1969,9 +1969,6 @@ YY[YY] = 年 ASCII码 - - 美国国家标准协会 - ISO-8859-1 @@ -1981,6 +1978,9 @@ YY[YY] = 年 统一码 + + GB2312 + +00:00:00.000 diff --git a/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs b/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs index b94430323..786b79d0c 100644 --- a/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs +++ b/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs @@ -17,6 +17,7 @@ public class SettingsDialogEncodingListTests [Test] [TestCase(1250, TestName = "GetAvailableEncodings_OffersWindows1250")] [TestCase(1252, TestName = "GetAvailableEncodings_OffersWindows1252")] + [TestCase(936, TestName = "GetAvailableEncodings_OffersGb2312")] public void GetAvailableEncodings_OffersLegacyCodePage (int codePage) { var encodings = SettingsDialog.GetAvailableEncodings(); diff --git a/src/LogExpert.Tests/Encodings/EncodingJsonConverterTests.cs b/src/LogExpert.Tests/Encodings/EncodingJsonConverterTests.cs index 5db2cc6a5..a1534f2f5 100644 --- a/src/LogExpert.Tests/Encodings/EncodingJsonConverterTests.cs +++ b/src/LogExpert.Tests/Encodings/EncodingJsonConverterTests.cs @@ -20,6 +20,7 @@ public class EncodingJsonConverterTests [Test] [TestCase("windows-1250", 1250)] [TestCase("windows-1252", 1252)] + [TestCase("gb2312", 936)] public void ReadJson_LegacyCodePageName_ResolvesInsteadOfFallingBack (string encodingName, int expectedCodePage) { var encoding = Deserialize($"\"{encodingName}\""); diff --git a/src/LogExpert.Tests/Encodings/EncodingRegistryTests.cs b/src/LogExpert.Tests/Encodings/EncodingRegistryTests.cs index 0ff4fcef7..ebd709afa 100644 --- a/src/LogExpert.Tests/Encodings/EncodingRegistryTests.cs +++ b/src/LogExpert.Tests/Encodings/EncodingRegistryTests.cs @@ -18,6 +18,7 @@ public class EncodingRegistryTests [Test] [TestCase(1250)] [TestCase(1252)] + [TestCase(936)] public void GetEncoding_LegacyCodePage_Resolves (int codePage) { var encoding = EncodingRegistry.GetEncoding(codePage); @@ -30,6 +31,7 @@ public void GetEncoding_LegacyCodePage_Resolves (int codePage) [TestCase("windows-1252")] [TestCase("utf-8")] [TestCase("iso-8859-1")] + [TestCase("gb2312")] public void TryGetEncoding_SupportedName_ReturnsTrueAndEncoding (string name) { var resolved = EncodingRegistry.TryGetEncoding(name, out var encoding); diff --git a/src/LogExpert.Tests/Encodings/PersisterXmlEncodingTests.cs b/src/LogExpert.Tests/Encodings/PersisterXmlEncodingTests.cs index a917255fd..8e0f3a5f6 100644 --- a/src/LogExpert.Tests/Encodings/PersisterXmlEncodingTests.cs +++ b/src/LogExpert.Tests/Encodings/PersisterXmlEncodingTests.cs @@ -36,6 +36,7 @@ public void Cleanup () [TestCase("windows-1250", 1250)] [TestCase("windows-1252", 1252)] [TestCase("utf-8", 65001)] + [TestCase("gb2312", 936)] public void Load_PersistedEncoding_Resolves (string encodingName, int expectedCodePage) { WriteLxp($""); diff --git a/src/LogExpert.Tests/Services/FileOperationServiceTests.cs b/src/LogExpert.Tests/Services/FileOperationServiceTests.cs index f00aa7d85..51b326fea 100644 --- a/src/LogExpert.Tests/Services/FileOperationServiceTests.cs +++ b/src/LogExpert.Tests/Services/FileOperationServiceTests.cs @@ -312,6 +312,7 @@ public void AddFileTab_ValidDefaultEncoding_SetsDefaultEncoding () [Test] [TestCase("windows-1250", 1250)] [TestCase("windows-1252", 1252)] + [TestCase("gb2312", 936)] public void AddFileTab_LegacyCodePageDefaultEncoding_ResolvesWithoutOpeningPreferences (string encodingName, int expectedCodePage) { // Arrange diff --git a/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs b/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs index 1a60cc6d0..c1d0f67f5 100644 --- a/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs +++ b/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs @@ -4,6 +4,7 @@ using System.Windows.Forms; using LogExpert.Core.EventArguments; +using LogExpert.Core.Helpers; using LogExpert.Dialogs; using LogExpert.UI.Controls; using LogExpert.UI.Services; @@ -68,10 +69,10 @@ public void Setup () _encodingMenu = new ToolStripMenuItem("Encoding") { Name = "encodingToolStripMenuItem" }; _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("ASCII") { Name = "encodingASCIIToolStripMenuItem" }); - _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("ANSI") { Name = "encodingANSIToolStripMenuItem" }); _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("UTF-8") { Name = "encodingUTF8toolStripMenuItem" }); _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("UTF-16") { Name = "encodingUTF16toolStripMenuItem" }); _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("ISO-8859-1") { Name = "encodingISO88591toolStripMenuItem" }); + _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("GB2312") { Name = "encodingGB2312toolStripMenuItem" }); _ = _viewMenu.DropDownItems.Add(_encodingMenu); var timeshiftMenu = new ToolStripMenuItem("Timeshift") { Name = "timeshiftToolStripMenuItem" }; @@ -179,6 +180,55 @@ public void UpdateEncodingMenu_NullEncoding_UnchecksAll () Assert.That(utf8Item.Checked, Is.False); } + /// + /// GB2312 (issue #688) is a row of its own, and picking it must not leave another row checked. + /// + [Test] + public void UpdateEncodingMenu_Gb2312_ChecksTheGb2312Item () + { + _controller.UpdateEncodingMenu(EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312)); + + Assert.Multiple(() => + { + Assert.That(FindMenuItem("encodingGB2312toolStripMenuItem").Checked, Is.True); + Assert.That(FindMenuItem("encodingUTF8toolStripMenuItem").Checked, Is.False); + Assert.That(FindMenuItem("encodingASCIIToolStripMenuItem").Checked, Is.False); + }); + } + + /// + /// A row is identified by its code page, so the two UTF-8 instances the application passes around — + /// (BOM) and the no-BOM instance the menu applies — are the same row. + /// + [Test] + public void UpdateEncodingMenu_Utf8WithoutBom_ChecksTheSameRowAsUtf8 () + { + _controller.UpdateEncodingMenu(new UTF8Encoding(false)); + + Assert.That(FindMenuItem("encodingUTF8toolStripMenuItem").Checked, Is.True); + } + + /// + /// Encoding.Default is UTF-8 on .NET — the reason the "ANSI" row was dropped rather than kept + /// as a second, indistinguishable UTF-8 row (issue #688). It has to check the UTF-8 row, because that + /// is the encoding a file with no preamble and no configured default is actually read with. + /// + [Test] + public void UpdateEncodingMenu_EncodingDefault_ChecksTheUtf8Row () + { + _controller.UpdateEncodingMenu(Encoding.Default); + + Assert.That(FindMenuItem("encodingUTF8toolStripMenuItem").Checked, Is.True); + } + + [Test] + public void UpdateEncodingMenu_Iso88591_ChecksTheIso88591Item () + { + _controller.UpdateEncodingMenu(Encoding.Latin1); + + Assert.That(FindMenuItem("encodingISO88591toolStripMenuItem").Checked, Is.True); + } + [Test] public void UpdateHighlightGroups_PopulatesComboBox () { diff --git a/src/LogExpert.Tests/StreamReaderTests/LogfileReaderEncodingTests.cs b/src/LogExpert.Tests/StreamReaderTests/LogfileReaderEncodingTests.cs index 63b19ab29..9d791bf4d 100644 --- a/src/LogExpert.Tests/StreamReaderTests/LogfileReaderEncodingTests.cs +++ b/src/LogExpert.Tests/StreamReaderTests/LogfileReaderEncodingTests.cs @@ -100,6 +100,35 @@ public void ReadFiles_NoExplicitEncodingNoPreambleNoConfiguredDefault_UsesTheMac Assert.That(reader.CurrentEncoding.CodePage, Is.EqualTo(Encoding.Default.CodePage)); } + /// + /// GB2312 (issue #688) end to end: the encoding a user picks in Preferences has to survive down to + /// the text the grid shows. A file this size stays in one buffer, so the point of the multi-line + /// assertion is the byte position the reader keeps between lines — a variable-width encoding is + /// where that drifts. + /// + [Test] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")] + public void ReadFiles_Gb2312File_DecodesEveryLine () + { + var gb2312 = EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312); + string[] lines = ["错误: 连接失败", "INFO 启动完成", "plain ascii line", "警告"]; + File.WriteAllText(_tempFile, string.Join("\n", lines) + "\n", gb2312); + + using var reader = CreateReader(new EncodingOptions { DefaultEncoding = gb2312 }); + reader.ReadFiles(); + + Assert.Multiple(() => + { + Assert.That(reader.CurrentEncoding.CodePage, Is.EqualTo(EncodingRegistry.CODE_PAGE_GB2312)); + Assert.That(reader.LineCount, Is.EqualTo(lines.Length)); + + for (var lineNum = 0; lineNum < lines.Length; lineNum++) + { + Assert.That(LineText(reader, lineNum), Is.EqualTo(lines[lineNum])); + } + }); + } + [Test] public void ChangeEncoding_SwitchesTheReportedEncoding () { diff --git a/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs b/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs index 21d61b121..198433340 100644 --- a/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs +++ b/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs @@ -3,6 +3,7 @@ using LogExpert.Core.Classes.Log.Streamreaders; using LogExpert.Core.Entities; +using LogExpert.Core.Helpers; using NUnit.Framework; @@ -184,6 +185,21 @@ public void Position_MatchesSystemReader_UTF8_MultibyteChars () ComparePositions(text, Encoding.UTF8); } + /// + /// GB2312 (issue #688) is the first offered encoding that is neither single-byte nor Unicode: two + /// bytes per Chinese character, one per ASCII character, mixed within a line. The direct reader + /// advances its byte position by Encoding.GetByteCount over the decoded chars, so a wrong + /// assumption there would drift the position from the second line on — and every buffer boundary, + /// rollover check and .lxp scroll position is computed from it. + /// + [Test] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")] + public void Position_MatchesSystemReader_Gb2312_MixedAsciiAndChinese () + { + var text = "2026-07-30 错误: 连接失败\nINFO 启动完成\nplain ascii line\n警告\n"; + ComparePositions(text, EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312)); + } + private static void ComparePositions (string text, Encoding encoding) { var bytes = encoding.GetBytes(text); diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index fd7ed7d7f..687c33bcf 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -13,6 +13,7 @@ using LogExpert.Core.Entities; using LogExpert.Core.Enums; using LogExpert.Core.EventArguments; +using LogExpert.Core.Helpers; using LogExpert.Core.Interfaces; using LogExpert.Dialogs; using LogExpert.UI.Dialogs; @@ -143,12 +144,6 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu var index = buttonToolStrip.Items.IndexOfKey("toolStripButtonTail"); - encodingASCIIToolStripMenuItem.Text = Encoding.ASCII.HeaderName; - encodingANSIToolStripMenuItem.Text = Encoding.Default.HeaderName; - encodingISO88591toolStripMenuItem.Text = Encoding.GetEncoding("iso-8859-1").HeaderName; - encodingUTF8toolStripMenuItem.Text = Encoding.UTF8.HeaderName; - encodingUTF16toolStripMenuItem.Text = Encoding.Unicode.HeaderName; - if (index != -1) { buttonToolStrip.Items.RemoveAt(index); @@ -453,10 +448,10 @@ private void ApplyContextMenuResources () columnFinderToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_columnFinderToolStripMenuItem; encodingToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingToolStripMenuItem; encodingASCIIToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem; - encodingANSIToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem; encodingISO88591toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem; encodingUTF8toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem; encodingUTF16toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem; + encodingGB2312toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingGB2312toolStripMenuItem; timeshiftToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_timeshiftToolStripMenuItem; timeshiftToolStripTextBox.Text = Resources.LogTabWindow_UI_ToolStripTextBox_timeshiftToolStripTextBox; copyMarkedLinesIntoNewTabToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_copyMarkedLinesIntoNewTabToolStripMenuItem; @@ -2178,12 +2173,6 @@ private void OnASCIIToolStripMenuItemClick (object sender, EventArgs e) CurrentLogWindow?.ChangeEncoding(Encoding.ASCII); } - [SupportedOSPlatform("windows")] - private void OnANSIToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(Encoding.Default); - } - [SupportedOSPlatform("windows")] private void OnUTF8ToolStripMenuItemClick (object sender, EventArgs e) { @@ -2199,7 +2188,13 @@ private void OnUTF16ToolStripMenuItemClick (object sender, EventArgs e) [SupportedOSPlatform("windows")] private void OnISO88591ToolStripMenuItemClick (object sender, EventArgs e) { - CurrentLogWindow?.ChangeEncoding(Encoding.GetEncoding("iso-8859-1")); + CurrentLogWindow?.ChangeEncoding(Encoding.Latin1); + } + + [SupportedOSPlatform("windows")] + private void OnGB2312ToolStripMenuItemClick (object sender, EventArgs e) + { + CurrentLogWindow?.ChangeEncoding(EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312)); } [SupportedOSPlatform("windows")] diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs index 8f7bab2ae..e16d7d3e3 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs @@ -1,4 +1,4 @@ -using System.Windows.Forms; +using System.Windows.Forms; using LogExpert.Core.Enums; using LogExpert.Dialogs; using WeifenLuo.WinFormsUI.Docking; @@ -59,10 +59,10 @@ private void InitializeComponent () ToolStripSeparator5 = new ToolStripSeparator(); encodingToolStripMenuItem = new ToolStripMenuItem(); encodingASCIIToolStripMenuItem = new ToolStripMenuItem(); - encodingANSIToolStripMenuItem = new ToolStripMenuItem(); encodingISO88591toolStripMenuItem = new ToolStripMenuItem(); encodingUTF8toolStripMenuItem = new ToolStripMenuItem(); encodingUTF16toolStripMenuItem = new ToolStripMenuItem(); + encodingGB2312toolStripMenuItem = new ToolStripMenuItem(); ToolStripSeparator6 = new ToolStripSeparator(); timeshiftToolStripMenuItem = new ToolStripMenuItem(); timeshiftToolStripTextBox = new ToolStripTextBox(); @@ -438,7 +438,7 @@ private void InitializeComponent () // // encodingToolStripMenuItem // - encodingToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { encodingASCIIToolStripMenuItem, encodingANSIToolStripMenuItem, encodingISO88591toolStripMenuItem, encodingUTF8toolStripMenuItem, encodingUTF16toolStripMenuItem }); + encodingToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { encodingASCIIToolStripMenuItem, encodingISO88591toolStripMenuItem, encodingUTF8toolStripMenuItem, encodingUTF16toolStripMenuItem, encodingGB2312toolStripMenuItem }); encodingToolStripMenuItem.Name = "encodingToolStripMenuItem"; encodingToolStripMenuItem.Size = new Size(177, 22); encodingToolStripMenuItem.Text = "Encoding"; @@ -452,16 +452,6 @@ private void InitializeComponent () encodingASCIIToolStripMenuItem.Text = "ASCII"; encodingASCIIToolStripMenuItem.Click += OnASCIIToolStripMenuItemClick; // - // encodingANSIToolStripMenuItem - // - encodingANSIToolStripMenuItem.BackColor = SystemColors.Control; - encodingANSIToolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; - encodingANSIToolStripMenuItem.Name = "encodingANSIToolStripMenuItem"; - encodingANSIToolStripMenuItem.Size = new Size(132, 22); - encodingANSIToolStripMenuItem.Tag = ""; - encodingANSIToolStripMenuItem.Text = "ANSI"; - encodingANSIToolStripMenuItem.Click += OnANSIToolStripMenuItemClick; - // // encodingISO88591toolStripMenuItem // encodingISO88591toolStripMenuItem.BackColor = SystemColors.Control; @@ -489,6 +479,15 @@ private void InitializeComponent () encodingUTF16toolStripMenuItem.Text = "Unicode"; encodingUTF16toolStripMenuItem.Click += OnUTF16ToolStripMenuItemClick; // + // encodingGB2312toolStripMenuItem + // + encodingGB2312toolStripMenuItem.BackColor = SystemColors.Control; + encodingGB2312toolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; + encodingGB2312toolStripMenuItem.Name = "encodingGB2312toolStripMenuItem"; + encodingGB2312toolStripMenuItem.Size = new Size(132, 22); + encodingGB2312toolStripMenuItem.Text = "GB2312"; + encodingGB2312toolStripMenuItem.Click += OnGB2312ToolStripMenuItemClick; + // // ToolStripSeparator6 // ToolStripSeparator6.Name = "ToolStripSeparator6"; @@ -1143,9 +1142,9 @@ private void InitializeComponent () private System.Windows.Forms.ToolStripMenuItem jumpToPrevToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem encodingToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem encodingASCIIToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingANSIToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem encodingUTF8toolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem encodingUTF16toolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem encodingGB2312toolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem reloadToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem columnizerToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem; diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index 62732f7e5..152420c62 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -712,8 +712,8 @@ private void FillEncodingList () } /// - /// The encodings offered as the default encoding: ASCII, ISO-8859-1, UTF-8, Unicode, Windows-1250 - /// and Windows-1252. + /// The encodings offered as the default encoding: ASCII, ISO-8859-1, UTF-8, Unicode, Windows-1250, + /// Windows-1252 and GB2312. /// /// /// Separate from so the offered set can be asserted without building @@ -722,6 +722,10 @@ private void FillEncodingList () /// ) and no two entries may share a code page — Encoding.Default /// is deliberately absent because it is code page 65001 just like , /// differing only in the BOM it emits, which a read-side default encoding never uses. + /// + /// New entries are appended rather than sorted in, so an existing user's row does not move under + /// the cursor on upgrade. + /// /// internal static IReadOnlyList GetAvailableEncodings () { @@ -732,7 +736,8 @@ internal static IReadOnlyList GetAvailableEncodings () Encoding.UTF8, Encoding.Unicode, EncodingRegistry.GetEncoding(1250), - EncodingRegistry.GetEncoding(1252) + EncodingRegistry.GetEncoding(1252), + EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312) ]; } diff --git a/src/LogExpert.UI/Interface/IMenuToolbarController.cs b/src/LogExpert.UI/Interface/IMenuToolbarController.cs index 9bd7c3a85..da3a921a3 100644 --- a/src/LogExpert.UI/Interface/IMenuToolbarController.cs +++ b/src/LogExpert.UI/Interface/IMenuToolbarController.cs @@ -34,8 +34,8 @@ void InitializeMenus (MenuStrip mainMenu, ToolStrip buttonToolbar, ToolStrip ext void UpdateGuiState (GuiStateEventArgs state, bool timestampControlEnabled); /// - /// Updates encoding menu to show current encoding. - /// Also updates the ANSI menu item header text. + /// Checks the encoding menu row the file is currently read with, and unchecks every other row. + /// Leaves all rows unchecked when the encoding is null or is one the menu does not offer. /// void UpdateEncodingMenu (Encoding currentEncoding); diff --git a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs index 04cb3083d..9b4231430 100644 --- a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs +++ b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs @@ -4,6 +4,7 @@ using System.Text; using LogExpert.Core.EventArguments; +using LogExpert.Core.Helpers; using LogExpert.Dialogs; using LogExpert.UI.Interface; @@ -37,11 +38,12 @@ internal sealed class MenuToolbarController : IMenuToolbarController private ToolStripMenuItem _columnFinderMenuItem; // Encoding menu items + private ToolStripMenuItem _encodingMenuItem; private ToolStripMenuItem _encodingAsciiMenuItem; - private ToolStripMenuItem _encodingAnsiMenuItem; private ToolStripMenuItem _encodingUtf8MenuItem; private ToolStripMenuItem _encodingUtf16MenuItem; private ToolStripMenuItem _encodingIso88591MenuItem; + private ToolStripMenuItem _encodingGb2312MenuItem; // Toolbar items private ToolStripButton _bubblesButton; @@ -94,11 +96,12 @@ public void InitializeMenus (MenuStrip mainMenu, ToolStrip buttonToolbar, _columnFinderMenuItem = FindMenuItem("columnFinderToolStripMenuItem"); // Encoding menu items + _encodingMenuItem = FindMenuItem("encodingToolStripMenuItem"); _encodingAsciiMenuItem = FindMenuItem("encodingASCIIToolStripMenuItem"); - _encodingAnsiMenuItem = FindMenuItem("encodingANSIToolStripMenuItem"); _encodingUtf8MenuItem = FindMenuItem("encodingUTF8toolStripMenuItem"); _encodingUtf16MenuItem = FindMenuItem("encodingUTF16toolStripMenuItem"); _encodingIso88591MenuItem = FindMenuItem("encodingISO88591toolStripMenuItem"); + _encodingGb2312MenuItem = FindMenuItem("encodingGB2312toolStripMenuItem"); // Toolbar items _bubblesButton = FindToolStripItem(_buttonToolbar, "toolStripButtonBubbles"); @@ -198,41 +201,66 @@ public void UpdateEncodingMenu (Encoding currentEncoding) return; } - // Clear all checks - SetCheckedSafe(_encodingAsciiMenuItem, false); - SetCheckedSafe(_encodingAnsiMenuItem, false); - SetCheckedSafe(_encodingUtf8MenuItem, false); - SetCheckedSafe(_encodingUtf16MenuItem, false); - SetCheckedSafe(_encodingIso88591MenuItem, false); + // Clear every row through the dropdown rather than row by row, so a row added to the menu + // cannot be left checked by a clear list nobody remembered to extend. + if (_encodingMenuItem != null) + { + foreach (var row in _encodingMenuItem.DropDownItems) + { + SetCheckedSafe(row as ToolStripMenuItem, false); + } + } - if (currentEncoding == null) + SetCheckedSafe(MenuItemFor(currentEncoding), true); + } + + /// + /// The encoding menu row representing , or null when the file is read + /// with an encoding the menu does not offer (a Preferences default such as windows-1250, say). + /// + /// + /// Matched by code page rather than by runtime type, because several instances stand for the same + /// row: the menu applies UTF-8 without a BOM while the Preferences default is + /// with one, and Encoding.Default — what a file with neither a + /// preamble nor a configured default is read with — is a third UTF-8 instance. Type and equality + /// checks put those on different rows (or, before the "ANSI" row was dropped, on the row for a + /// different encoding entirely). + /// + private ToolStripMenuItem MenuItemFor (Encoding encoding) + { + if (encoding == null) { - return; + return null; } - if (currentEncoding is ASCIIEncoding) + var codePage = encoding.CodePage; + + if (codePage == Encoding.ASCII.CodePage) { - SetCheckedSafe(_encodingAsciiMenuItem, true); + return _encodingAsciiMenuItem; } - else if (currentEncoding.Equals(Encoding.Default)) + + if (codePage == Encoding.UTF8.CodePage) { - SetCheckedSafe(_encodingAnsiMenuItem, true); + return _encodingUtf8MenuItem; } - else if (currentEncoding is UTF8Encoding) + + if (codePage == Encoding.Unicode.CodePage || codePage == Encoding.BigEndianUnicode.CodePage) { - SetCheckedSafe(_encodingUtf8MenuItem, true); + return _encodingUtf16MenuItem; } - else if (currentEncoding is UnicodeEncoding) + + if (codePage == Encoding.Latin1.CodePage) { - SetCheckedSafe(_encodingUtf16MenuItem, true); + return _encodingIso88591MenuItem; } - else if (currentEncoding.Equals(Encoding.GetEncoding("iso-8859-1"))) + + if (codePage == EncodingRegistry.CODE_PAGE_GB2312) { - SetCheckedSafe(_encodingIso88591MenuItem, true); + return _encodingGb2312MenuItem; } - // Preserve existing behavior: update ANSI display name - _ = (_encodingAnsiMenuItem?.Text = Encoding.Default.HeaderName); + return null; } public void UpdateHighlightGroups (IEnumerable groups, string selectedGroup) From 2a7568533b9d8153e00836782dc0ca3292a52b78 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 30 Jul 2026 10:43:18 +0200 Subject: [PATCH 2/2] refactor: one offered-encoding list, in EncodingRegistry The Preferences default-encoding combo box and the View > Encoding menu each carried their own list of encodings, and they had drifted: the menu lacked windows-1250 and windows-1252, and carried an "ANSI" row that the combo did not. Adding GB2312 for #688 meant editing both, plus a third place - the check-state lookup that maps the encoding a file is read with back onto a menu row. EncodingRegistry.OfferedEncodings is now the one list, and it documents the invariants both UIs depend on (resolvable by its own header name, no two entries sharing a code page, append rather than sort). Adding an encoding is a one-line change there. The menu rows are built from it at runtime by EncodingMenuBuilder rather than declared one by one in the designer. Each row carries the Encoding it stands for in its Tag, so the click handler applies whatever the clicked row carries (one handler, was five) and the check-state lookup compares against it (was a cascade naming every encoding). Rows are labelled with their header name, the same text the combo box shows, which retires five resource keys that were dead anyway - the constructor overwrote them before anyone saw them. Also fixes what GB2312 exposed in the legacy and XML readers, which advance their position per character by a per-encoding step: GetPosIncPrecomputed credited every encoding that is not UTF-8 or UTF-16 with one byte per character. GB2312 is the first offered encoding that is neither single-byte nor Unicode, so the position drifted on the first Chinese character and every later line began at the wrong offset. The step is now keyed on Encoding.IsSingleByte, and 0 ("measure the character") covers the variable-width case. Test coverage follows the seam: the list invariants are asserted once against the registry instead of per dialog, and the menu fixture is filled by the real builder, so it can no longer describe a menu the application does not build. --- .../PositionAwareStreamReaderBase.cs | 15 +- .../Helpers/EncodingRegistry.cs | 51 ++++- src/LogExpert.Resources/Resources.Designer.cs | 45 ----- src/LogExpert.Resources/Resources.de.resx | 15 -- src/LogExpert.Resources/Resources.resx | 15 -- src/LogExpert.Resources/Resources.zh-CN.resx | 15 -- .../SettingsDialogEncodingListTests.cs | 121 +++--------- .../Encodings/OfferedEncodingsTests.cs | 176 ++++++++++++++++++ .../Services/MenuToolbarControllerTests.cs | 85 ++++----- .../StreamReaderTests/LogStreamReaderTest.cs | 62 ++++++ .../Dialogs/LogTabWindow/LogTabWindow.cs | 47 ++--- .../LogTabWindow/LogTabWindow.designer.cs | 57 +----- src/LogExpert.UI/Dialogs/SettingsDialog.cs | 42 +---- .../MenuToolbarService/EncodingMenuBuilder.cs | 70 +++++++ .../MenuToolbarController.cs | 84 ++------- 15 files changed, 482 insertions(+), 418 deletions(-) create mode 100644 src/LogExpert.Tests/Encodings/OfferedEncodingsTests.cs create mode 100644 src/LogExpert.UI/Services/MenuToolbarService/EncodingMenuBuilder.cs diff --git a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs index e186bc002..ac234e5c9 100644 --- a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs +++ b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using LogExpert.Core.Entities; @@ -221,8 +221,19 @@ public static (int length, Encoding? detectedEncoding) DetectPreambleLength (Str return (0, null); } + /// + /// Bytes to advance the position by per character read, or 0 for "measure the character". + /// + /// + /// The fallback is keyed on , not on "everything else is one + /// byte": a variable-width code page such as GB2312 (one byte per ASCII character, two per Chinese + /// one) is neither a nor a , and counting it + /// as one byte per character drifts the position on the first non-ASCII character. + /// public static int GetPosIncPrecomputed (Encoding usedEncoding) { + ArgumentNullException.ThrowIfNull(usedEncoding); + switch (usedEncoding) { case UTF8Encoding: @@ -235,7 +246,7 @@ public static int GetPosIncPrecomputed (Encoding usedEncoding) } default: { - return 1; + return usedEncoding.IsSingleByte ? 1 : 0; } } } diff --git a/src/LogExpert.Core/Helpers/EncodingRegistry.cs b/src/LogExpert.Core/Helpers/EncodingRegistry.cs index 1424059d2..98f18d894 100644 --- a/src/LogExpert.Core/Helpers/EncodingRegistry.cs +++ b/src/LogExpert.Core/Helpers/EncodingRegistry.cs @@ -30,13 +30,54 @@ public static class EncodingRegistry /// /// Code page of GB2312, the simplified-Chinese code page (issue #688). /// + public const int CODE_PAGE_GB2312 = 936; + + /// + /// The encodings LogExpert offers the user to choose from, in the order they are presented. + /// /// - /// Named — unlike the code pages that appear once, as a bare number, at the single place that - /// offers them — because three call sites have to agree on it: the Preferences default-encoding - /// list, the encoding menu row that applies it, and the check-state lookup that has to recognise a - /// file already being read with it. + /// The single list: the Preferences default-encoding combo box and the per-file View → Encoding + /// menu both draw their rows from it, so they cannot drift apart and adding an encoding is a + /// one-line change here. Three invariants, each pinned by a test: + /// + /// + /// Every entry resolves from its own , because that name is + /// what gets persisted (in the settings JSON and in a .lxp) and resolved again on the next + /// start. An entry that did not would silently degrade to a fallback. + /// + /// + /// No two entries share a code page. Both UIs label a row with its header name, so two entries + /// for one code page are two rows the user cannot tell apart — which is exactly what + /// Encoding.Default alongside produced (both are code page + /// 65001 on .NET, differing only in the BOM they emit, which a read-side encoding never uses). + /// Hence no Encoding.Default entry. + /// + /// + /// New entries are appended rather than sorted in, so an existing user's row does not move + /// under the cursor on upgrade. + /// + /// /// - public const int CODE_PAGE_GB2312 = 936; + public static IReadOnlyList OfferedEncodings => _offeredEncodings.Value; + + /// + /// Built once: instances are immutable and the ones from + /// are cached by .NET anyway, so the list is safe to share. Lazy + /// rather than a plain initialiser so the code pages resolve through + /// rather than depending on when this class is first touched. + /// + private static readonly Lazy> _offeredEncodings = new( + () => + [ + Encoding.ASCII, + Encoding.Latin1, + Encoding.UTF8, + Encoding.Unicode, + GetEncoding(1250), + GetEncoding(1252), + GetEncoding(CODE_PAGE_GB2312) + ], + LazyThreadSafetyMode.ExecutionAndPublication); /// /// Registers on first use. diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 866168338..1c400f52a 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -2284,33 +2284,6 @@ public static string LogTabWindow_UI_ToolStripMenuItem_dumpLogBufferInfoToolStri } } - /// - /// Looks up a localized string similar to ASCII. - /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem { - get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GB2312. - /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingGB2312toolStripMenuItem { - get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingGB2312toolStripMenuItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ISO-8859-1. - /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem { - get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem", resourceCulture); - } - } - /// /// Looks up a localized string similar to Encoding. /// @@ -2320,24 +2293,6 @@ public static string LogTabWindow_UI_ToolStripMenuItem_encodingToolStripMenuItem } } - /// - /// Looks up a localized string similar to Unicode. - /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem { - get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UTF8. - /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem { - get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem", resourceCulture); - } - } - /// /// Looks up a localized string similar to Exit. /// diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index 0ff9623ac..e0e19567e 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -1324,21 +1324,6 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools Encoding - - ASCII - - - ISO-8859-1 - - - UTF8 - - - Unicode - - - GB2312 - +00:00:00.000 diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index 6a3ae3eec..a08b755c2 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -1347,21 +1347,6 @@ Checked tools will appear in the icon bar. All other tools are available in the Encoding - - ASCII - - - ISO-8859-1 - - - UTF8 - - - Unicode - - - GB2312 - +00:00:00.000 diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx index 8e58e5a8c..f4af56f51 100644 --- a/src/LogExpert.Resources/Resources.zh-CN.resx +++ b/src/LogExpert.Resources/Resources.zh-CN.resx @@ -1966,21 +1966,6 @@ YY[YY] = 年 菜单条1 - - ASCII码 - - - ISO-8859-1 - - - UTF8 - - - 统一码 - - - GB2312 - +00:00:00.000 diff --git a/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs b/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs index 786b79d0c..c01871722 100644 --- a/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs +++ b/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs @@ -8,123 +8,64 @@ namespace LogExpert.Tests.Dialogs; /// -/// The Preferences encoding dropdown is the only way to set Preferences.DefaultEncoding, so the -/// list is asserted directly — building the dialog is not needed to know what it offers. +/// The Preferences encoding combo box, which is the only way to set Preferences.DefaultEncoding. +/// What it offers is and is asserted in +/// OfferedEncodingsTests; what is left here is the part only a real +/// can answer. /// [TestFixture] public class SettingsDialogEncodingListTests { - [Test] - [TestCase(1250, TestName = "GetAvailableEncodings_OffersWindows1250")] - [TestCase(1252, TestName = "GetAvailableEncodings_OffersWindows1252")] - [TestCase(936, TestName = "GetAvailableEncodings_OffersGb2312")] - public void GetAvailableEncodings_OffersLegacyCodePage (int codePage) - { - var encodings = SettingsDialog.GetAvailableEncodings(); - - Assert.That(encodings.Select(encoding => encoding.CodePage), Does.Contain(codePage)); - } - /// - /// Every offered encoding is saved as its name and resolved from that name on the next start, so a - /// name that cannot be resolved again would silently degrade to - /// . + /// The persist/restore round trip against a real , configured the way the + /// dialog configures it: the reselect goes through Items.IndexOf, and WinForms — not NUnit — + /// has the final say on whether a row is reachable. Deliberately kept alongside the plain assertion + /// in OfferedEncodingsTests rather than replacing it: this one needs an STA apartment and a + /// WinForms control, and the invariant should still be pinned where that is unavailable. /// [Test] - public void GetAvailableEncodings_EveryEntryResolvesByItsPersistedName () - { - var encodings = SettingsDialog.GetAvailableEncodings(); - - Assert.Multiple(() => - { - foreach (var encoding in encodings) - { - Assert.That( - EncodingRegistry.TryGetEncoding(encoding.HeaderName, out _), - Is.True, - $"'{encoding.HeaderName}' cannot be resolved back from a saved preference"); - } - }); - } - - /// - /// The dropdown renders each entry as its , so two entries sharing a - /// code page are two rows the user cannot tell apart. Encoding.Default and - /// are both code page 65001 on .NET and both read utf-8. - /// - [Test] - public void GetAvailableEncodings_NoTwoEntriesShareACodePage () + [Apartment(ApartmentState.STA)] + public void EncodingComboBox_EveryOfferedRowIsReselectableAfterARestart () { - var codePages = SettingsDialog.GetAvailableEncodings().Select(encoding => encoding.CodePage); - - Assert.That(codePages, Is.Unique); - } + var encodings = EncodingRegistry.OfferedEncodings; - /// - /// The selection is persisted by name and restored with - /// comboBoxEncoding.SelectedItem = EncodingRegistry.GetEncoding(name, …). WinForms locates that - /// item with , so an entry whose name resolves back to an instance - /// that is not equal to it can never be reselected — the row goes dead after the first restart. - /// - [Test] - public void GetAvailableEncodings_EveryEntryIsReselectableAfterARoundTrip () - { - var encodings = SettingsDialog.GetAvailableEncodings(); + using var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, FormattingEnabled = true }; + comboBox.Items.AddRange([.. encodings]); Assert.Multiple(() => { - foreach (var encoding in encodings) + for (var row = 0; row < encodings.Count; row++) { - var restored = EncodingRegistry.GetEncoding(encoding.HeaderName, SettingsDialog.FallbackEncoding); + comboBox.SelectedIndex = row; + var saved = ((Encoding)comboBox.SelectedItem).HeaderName; + + comboBox.SelectedIndex = -1; + comboBox.SelectedItem = EncodingRegistry.GetEncoding(saved, SettingsDialog.FallbackEncoding); - Assert.That( - restored, - Is.EqualTo(encoding), - $"picking '{encoding.HeaderName}' saves a name that reselects a different entry"); + Assert.That(comboBox.SelectedIndex, Is.EqualTo(row), $"row {row} ('{saved}') is unreachable after a restart"); } }); } /// - /// FillDialog and SavePreferences fall back to this encoding when the persisted name is - /// unusable, so it has to be an offered entry — otherwise the dropdown shows no selection and OK - /// rewrites the preference to something that was never on the list. - /// - [Test] - public void GetAvailableEncodings_OffersTheFallbackEncoding () - { - var encodings = SettingsDialog.GetAvailableEncodings(); - - Assert.That(encodings, Does.Contain(SettingsDialog.FallbackEncoding)); - } - - /// - /// The same round trip against a real , because the reselect actually goes - /// through Items.IndexOf — WinForms, not NUnit, has the final say on whether a row is - /// reachable. Deliberately kept alongside the assertion above rather than replacing it: this one - /// needs an STA apartment and a WinForms control, and the invariant should still be pinned where - /// that is unavailable. + /// The combo box has no DisplayMember — the dialog only sets ValueMember, and WinForms + /// falls back to it for the display text. Pinned because without that fallback every row would render + /// as Encoding.ToString(), i.e. a .NET type name, and the rows the user picks between would be + /// indistinguishable for a different reason than the one issue #688 reported. /// [Test] [Apartment(ApartmentState.STA)] - public void GetAvailableEncodings_EveryOfferedRowIsReselectableInARealComboBox () + public void EncodingComboBox_RendersEachRowAsItsHeaderName () { - var encodings = SettingsDialog.GetAvailableEncodings(); - - using var comboBox = new ComboBox { FormattingEnabled = true }; - comboBox.Items.AddRange([.. encodings]); + using var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, FormattingEnabled = true }; + comboBox.ValueMember = "HeaderName"; + comboBox.Items.AddRange([.. EncodingRegistry.OfferedEncodings]); Assert.Multiple(() => { - for (var row = 0; row < encodings.Count; row++) + foreach (Encoding encoding in comboBox.Items) { - comboBox.SelectedIndex = row; - var saved = ((Encoding)comboBox.SelectedItem).HeaderName; - - comboBox.SelectedIndex = -1; - comboBox.SelectedItem = EncodingRegistry.GetEncoding(saved, SettingsDialog.FallbackEncoding); - - Assert.That(comboBox.SelectedIndex, Is.EqualTo(row), $"row {row} ('{saved}') is unreachable after a restart"); + Assert.That(comboBox.GetItemText(encoding), Is.EqualTo(encoding.HeaderName)); } }); } diff --git a/src/LogExpert.Tests/Encodings/OfferedEncodingsTests.cs b/src/LogExpert.Tests/Encodings/OfferedEncodingsTests.cs new file mode 100644 index 000000000..a78fa74bf --- /dev/null +++ b/src/LogExpert.Tests/Encodings/OfferedEncodingsTests.cs @@ -0,0 +1,176 @@ +using System.Text; + +using LogExpert.Core.Helpers; +using LogExpert.Dialogs; +using LogExpert.UI.Services.MenuToolbarService; + +using NUnit.Framework; + +namespace LogExpert.Tests.Encodings; + +/// +/// is the one list of encodings a user can pick: the +/// Preferences default-encoding combo box and the per-file View → Encoding menu are both built from it. +/// These tests pin the invariants that list has to hold for either UI to work, so they are asserted once +/// here rather than per UI. +/// +[TestFixture] +public class OfferedEncodingsTests +{ + [Test] + [TestCase(1250, TestName = "OfferedEncodings_OffersWindows1250")] + [TestCase(1252, TestName = "OfferedEncodings_OffersWindows1252")] + [TestCase(936, TestName = "OfferedEncodings_OffersGb2312")] + public void OfferedEncodings_OffersLegacyCodePage (int codePage) + { + Assert.That(EncodingRegistry.OfferedEncodings.Select(encoding => encoding.CodePage), Does.Contain(codePage)); + } + + /// + /// Every offered encoding is saved as its name and resolved from that name on the next start, so a + /// name that cannot be resolved again would silently degrade to a fallback. + /// + [Test] + public void OfferedEncodings_EveryEntryResolvesByItsPersistedName () + { + Assert.Multiple(() => + { + foreach (var encoding in EncodingRegistry.OfferedEncodings) + { + Assert.That( + EncodingRegistry.TryGetEncoding(encoding.HeaderName, out _), + Is.True, + $"'{encoding.HeaderName}' cannot be resolved back from a saved preference"); + } + }); + } + + /// + /// Both UIs render an entry as its , so two entries sharing a code + /// page are two rows the user cannot tell apart. Encoding.Default and + /// are both code page 65001 on .NET and both read utf-8 — the + /// duplicate reported in issue #688. + /// + [Test] + public void OfferedEncodings_NoTwoEntriesShareACodePage () + { + Assert.That(EncodingRegistry.OfferedEncodings.Select(encoding => encoding.CodePage), Is.Unique); + } + + /// + /// The same, stated as the property the reporter actually saw: no two rows carry the same label. + /// + [Test] + public void OfferedEncodings_NoTwoEntriesShareAHeaderName () + { + Assert.That(EncodingRegistry.OfferedEncodings.Select(encoding => encoding.HeaderName), Is.Unique); + } + + /// + /// The list is shared and handed out repeatedly (every Preferences open, every window's encoding + /// menu), so it must be a stable snapshot rather than something a caller could append to. + /// + [Test] + public void OfferedEncodings_IsTheSameListEveryTime () + { + Assert.That(EncodingRegistry.OfferedEncodings, Is.SameAs(EncodingRegistry.OfferedEncodings)); + } + + /// + /// The Preferences selection is persisted by name and restored with + /// comboBoxEncoding.SelectedItem = EncodingRegistry.GetEncoding(name, …). WinForms locates that + /// item with , so an entry whose name resolves back to an instance + /// that is not equal to it can never be reselected — the row goes dead after the first restart. + /// + [Test] + public void OfferedEncodings_EveryEntryIsReselectableAfterARoundTrip () + { + Assert.Multiple(() => + { + foreach (var encoding in EncodingRegistry.OfferedEncodings) + { + var restored = EncodingRegistry.GetEncoding(encoding.HeaderName, SettingsDialog.FallbackEncoding); + + Assert.That( + restored, + Is.EqualTo(encoding), + $"picking '{encoding.HeaderName}' saves a name that reselects a different entry"); + } + }); + } + + /// + /// FillDialog and SavePreferences fall back to this encoding when the persisted name is + /// unusable, so it has to be an offered entry — otherwise the dropdown shows no selection and OK + /// rewrites the preference to something that was never on the list. + /// + [Test] + public void OfferedEncodings_OffersTheSettingsDialogFallback () + { + Assert.That(EncodingRegistry.OfferedEncodings, Does.Contain(SettingsDialog.FallbackEncoding)); + } + + /// + /// The point of the shared list: the encoding menu offers exactly what Preferences offers, in the + /// same order. Before this, the menu was hand-declared in the designer and had drifted — it lacked + /// windows-1250 and windows-1252, and carried an "ANSI" row Preferences did not. + /// + [Test] + [Apartment(ApartmentState.STA)] + public void EncodingMenu_OffersExactlyTheOfferedEncodings () + { + using var encodingMenu = new ToolStripMenuItem("Encoding"); + + EncodingMenuBuilder.Fill(encodingMenu, (_, _) => { }); + + var rows = encodingMenu.DropDownItems.Cast().ToList(); + + Assert.Multiple(() => + { + Assert.That( + rows.Select(EncodingMenuBuilder.EncodingOf), + Is.EqualTo(EncodingRegistry.OfferedEncodings).AsCollection); + + Assert.That( + rows.Select(row => row.Text), + Is.EqualTo(EncodingRegistry.OfferedEncodings.Select(encoding => encoding.HeaderName)).AsCollection, + "a row is labelled with its encoding's header name, the same text the Preferences combo shows"); + }); + } + + /// + /// Filling twice — a second Log Tab Window, or a rebuild — must not stack a second set of rows. + /// + [Test] + [Apartment(ApartmentState.STA)] + public void EncodingMenu_FilledTwice_DoesNotDuplicateRows () + { + using var encodingMenu = new ToolStripMenuItem("Encoding"); + + EncodingMenuBuilder.Fill(encodingMenu, (_, _) => { }); + EncodingMenuBuilder.Fill(encodingMenu, (_, _) => { }); + + Assert.That(encodingMenu.DropDownItems, Has.Count.EqualTo(EncodingRegistry.OfferedEncodings.Count)); + } + + /// + /// Clicking a row has to hand the click handler that row's encoding — that is the whole mechanism by + /// which one handler serves every row. + /// + [Test] + [Apartment(ApartmentState.STA)] + public void EncodingMenu_ClickingARow_YieldsThatRowsEncoding () + { + using var encodingMenu = new ToolStripMenuItem("Encoding"); + var clicked = new List(); + + EncodingMenuBuilder.Fill(encodingMenu, (sender, _) => clicked.Add(EncodingMenuBuilder.EncodingOf(sender as ToolStripItem))); + + foreach (var row in encodingMenu.DropDownItems.Cast().ToList()) + { + row.PerformClick(); + } + + Assert.That(clicked, Is.EqualTo(EncodingRegistry.OfferedEncodings).AsCollection); + } +} diff --git a/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs b/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs index c1d0f67f5..e499cce92 100644 --- a/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs +++ b/src/LogExpert.Tests/Services/MenuToolbarControllerTests.cs @@ -67,12 +67,10 @@ public void Setup () _ = _viewMenu.DropDownItems.Add(new ToolStripMenuItem("Filter") { Name = "filterToolStripMenuItem" }); _ = _viewMenu.DropDownItems.Add(new ToolStripMenuItem("Column Finder") { Name = "columnFinderToolStripMenuItem" }); + // Filled by the same builder the Log Tab Window uses, so this fixture cannot describe a menu + // the application does not actually build. _encodingMenu = new ToolStripMenuItem("Encoding") { Name = "encodingToolStripMenuItem" }; - _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("ASCII") { Name = "encodingASCIIToolStripMenuItem" }); - _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("UTF-8") { Name = "encodingUTF8toolStripMenuItem" }); - _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("UTF-16") { Name = "encodingUTF16toolStripMenuItem" }); - _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("ISO-8859-1") { Name = "encodingISO88591toolStripMenuItem" }); - _ = _encodingMenu.DropDownItems.Add(new ToolStripMenuItem("GB2312") { Name = "encodingGB2312toolStripMenuItem" }); + EncodingMenuBuilder.Fill(_encodingMenu, (_, _) => { }); _ = _viewMenu.DropDownItems.Add(_encodingMenu); var timeshiftMenu = new ToolStripMenuItem("Timeshift") { Name = "timeshiftToolStripMenuItem" }; @@ -157,15 +155,25 @@ public void UpdateGuiState_HidesTimestampControl_WhenTimeshiftNotPossible () Assert.That(_dragControl.Visible, Is.False); } + /// + /// Exactly one row is checked, and it is the row for the encoding passed in — asserted for every + /// offered encoding, so no row can be unreachable. + /// [Test] - public void UpdateEncodingMenu_Utf8_ChecksCorrectItem () + public void UpdateEncodingMenu_OfferedEncoding_ChecksOnlyThatRow () { - _controller.UpdateEncodingMenu(Encoding.UTF8); + Assert.Multiple(() => + { + foreach (var encoding in EncodingRegistry.OfferedEncodings) + { + _controller.UpdateEncodingMenu(encoding); - var utf8Item = FindMenuItem("encodingUTF8toolStripMenuItem"); - var asciiItem = FindMenuItem("encodingASCIIToolStripMenuItem"); - Assert.That(utf8Item.Checked, Is.True); - Assert.That(asciiItem.Checked, Is.False); + Assert.That( + CheckedEncodingRowNames(), + Is.EqualTo(new[] { EncodingMenuBuilder.RowName(encoding) }).AsCollection, + $"'{encoding.HeaderName}' did not check exactly its own row"); + } + }); } [Test] @@ -176,57 +184,50 @@ public void UpdateEncodingMenu_NullEncoding_UnchecksAll () // Then clear _controller.UpdateEncodingMenu(null); - var utf8Item = FindMenuItem("encodingUTF8toolStripMenuItem"); - Assert.That(utf8Item.Checked, Is.False); + Assert.That(CheckedEncodingRowNames(), Is.Empty); } /// - /// GB2312 (issue #688) is a row of its own, and picking it must not leave another row checked. + /// A row is identified by its code page, so the UTF-8 instances the application passes around are the + /// same row: (emits a BOM), a no-BOM instance, and Encoding.Default + /// — which is UTF-8 on .NET, and is the reason the "ANSI" row was dropped rather than kept as a + /// second, indistinguishable UTF-8 row (issue #688). /// [Test] - public void UpdateEncodingMenu_Gb2312_ChecksTheGb2312Item () + public void UpdateEncodingMenu_AnyUtf8Instance_ChecksTheUtf8Row () { - _controller.UpdateEncodingMenu(EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312)); + var utf8Row = new[] { EncodingMenuBuilder.RowName(Encoding.UTF8) }; Assert.Multiple(() => { - Assert.That(FindMenuItem("encodingGB2312toolStripMenuItem").Checked, Is.True); - Assert.That(FindMenuItem("encodingUTF8toolStripMenuItem").Checked, Is.False); - Assert.That(FindMenuItem("encodingASCIIToolStripMenuItem").Checked, Is.False); - }); - } - - /// - /// A row is identified by its code page, so the two UTF-8 instances the application passes around — - /// (BOM) and the no-BOM instance the menu applies — are the same row. - /// - [Test] - public void UpdateEncodingMenu_Utf8WithoutBom_ChecksTheSameRowAsUtf8 () - { - _controller.UpdateEncodingMenu(new UTF8Encoding(false)); + foreach (var utf8 in new[] { Encoding.UTF8, new UTF8Encoding(false), Encoding.Default }) + { + _controller.UpdateEncodingMenu(utf8); - Assert.That(FindMenuItem("encodingUTF8toolStripMenuItem").Checked, Is.True); + Assert.That(CheckedEncodingRowNames(), Is.EqualTo(utf8Row).AsCollection); + } + }); } /// - /// Encoding.Default is UTF-8 on .NET — the reason the "ANSI" row was dropped rather than kept - /// as a second, indistinguishable UTF-8 row (issue #688). It has to check the UTF-8 row, because that - /// is the encoding a file with no preamble and no configured default is actually read with. + /// A file read with an encoding the menu does not offer leaves every row unchecked — checking one + /// would claim a row reproduces the file's encoding when clicking it would change it. /// [Test] - public void UpdateEncodingMenu_EncodingDefault_ChecksTheUtf8Row () + public void UpdateEncodingMenu_EncodingThatIsNotOffered_ChecksNothing () { - _controller.UpdateEncodingMenu(Encoding.Default); + _controller.UpdateEncodingMenu(Encoding.BigEndianUnicode); - Assert.That(FindMenuItem("encodingUTF8toolStripMenuItem").Checked, Is.True); + Assert.That(CheckedEncodingRowNames(), Is.Empty); } - [Test] - public void UpdateEncodingMenu_Iso88591_ChecksTheIso88591Item () + private List CheckedEncodingRowNames () { - _controller.UpdateEncodingMenu(Encoding.Latin1); - - Assert.That(FindMenuItem("encodingISO88591toolStripMenuItem").Checked, Is.True); + return [.. _encodingMenu.DropDownItems + .Cast() + .OfType() + .Where(row => row.Checked) + .Select(row => row.Name)]; } [Test] diff --git a/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs b/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs index 89b015920..37ee01e3d 100644 --- a/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs +++ b/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs @@ -2,6 +2,7 @@ using LogExpert.Core.Classes.Log.Streamreaders; using LogExpert.Core.Entities; +using LogExpert.Core.Helpers; using LogExpert.Core.Interfaces; using NUnit.Framework; @@ -293,4 +294,65 @@ public void TryReadLine_LegacyReader_ReadsAllLines (string text, int expectedLin Assert.That(lineCount, Is.EqualTo(expectedLines)); } + + /// + /// The legacy reader advances its position per character read, by a per-encoding step + /// (GetPosIncPrecomputed). GB2312 (issue #688) is the first offered encoding that is neither + /// single-byte nor Unicode — one byte per ASCII character, two per Chinese one — so a step of "1 + /// byte unless UTF-8 or UTF-16" drifted the position on the first Chinese character and every + /// subsequent line started at the wrong offset. + /// + [Test] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")] + public void TryReadLine_LegacyReader_Gb2312_TracksTheBytePositionPerLine () + { + var gb2312 = EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312); + string[] lines = ["错误: 连接失败", "INFO 启动完成", "plain ascii", "警告"]; + var text = string.Join("\n", lines) + "\n"; + + using var stream = new MemoryStream(gb2312.GetBytes(text)); + using var reader = new PositionAwareStreamReaderLegacy(stream, new EncodingOptions { Encoding = gb2312 }, 500); + + var expectedPosition = 0; + + Assert.Multiple(() => + { + foreach (var line in lines) + { + expectedPosition += gb2312.GetByteCount(line + "\n"); + + Assert.That(reader.TryReadLine(out var lineMemory), Is.True); + Assert.That(lineMemory.Span.ToString(), Is.EqualTo(line)); + Assert.That(reader.Position, Is.EqualTo(expectedPosition), $"position drifted after '{line}'"); + } + }); + } + + /// + /// A step of 0 means "measure the character", which is the only correct answer for a variable-width + /// encoding. Pinned per offered encoding so a newly offered one cannot silently get a fixed step. + /// + [Test] + public void GetPosIncPrecomputed_IsAFixedStepOnlyForFixedWidthEncodings () + { + Assert.Multiple(() => + { + foreach (var encoding in EncodingRegistry.OfferedEncodings) + { + var step = PositionAwareStreamReaderBase.GetPosIncPrecomputed(encoding); + + if (step != 0) + { + Assert.That( + encoding.GetByteCount("a"), + Is.EqualTo(step), + $"'{encoding.HeaderName}' is credited a fixed {step} byte(s) per character"); + Assert.That( + encoding.IsSingleByte || encoding is UnicodeEncoding, + Is.True, + $"'{encoding.HeaderName}' is variable-width, so its step has to be measured"); + } + } + }); + } } diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 687c33bcf..2e44ec9e8 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -13,7 +13,6 @@ using LogExpert.Core.Entities; using LogExpert.Core.Enums; using LogExpert.Core.EventArguments; -using LogExpert.Core.Helpers; using LogExpert.Core.Interfaces; using LogExpert.Dialogs; using LogExpert.UI.Dialogs; @@ -142,6 +141,10 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu BackColor = Color.FromKnownColor(KnownColor.Transparent) }; + // Built here rather than declared in the designer so the menu and the Preferences combo box + // cannot offer different encodings. + EncodingMenuBuilder.Fill(encodingToolStripMenuItem, OnEncodingToolStripMenuItemClick); + var index = buttonToolStrip.Items.IndexOfKey("toolStripButtonTail"); if (index != -1) @@ -446,12 +449,9 @@ private void ApplyContextMenuResources () jumpToPrevToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_jumpToPrevToolStripMenuItem; showBookmarkListToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_showBookmarkListToolStripMenuItem; columnFinderToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_columnFinderToolStripMenuItem; + // The rows below it are labelled with their encoding's header name, not from resources — an + // encoding name is the same in every language. encodingToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingToolStripMenuItem; - encodingASCIIToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem; - encodingISO88591toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem; - encodingUTF8toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem; - encodingUTF16toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem; - encodingGB2312toolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_encodingGB2312toolStripMenuItem; timeshiftToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_timeshiftToolStripMenuItem; timeshiftToolStripTextBox.Text = Resources.LogTabWindow_UI_ToolStripTextBox_timeshiftToolStripTextBox; copyMarkedLinesIntoNewTabToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_copyMarkedLinesIntoNewTabToolStripMenuItem; @@ -2167,34 +2167,19 @@ private void OnJumpToPrevToolStripMenuItemClick (object sender, EventArgs e) CurrentLogWindow?.JumpPrevBookmark(); } + /// + /// One handler for every row of the Encoding menu: the clicked row carries the encoding it stands + /// for, so this does not know which encodings are offered. + /// [SupportedOSPlatform("windows")] - private void OnASCIIToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(Encoding.ASCII); - } - - [SupportedOSPlatform("windows")] - private void OnUTF8ToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(new UTF8Encoding(false)); - } - - [SupportedOSPlatform("windows")] - private void OnUTF16ToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(Encoding.Unicode); - } - - [SupportedOSPlatform("windows")] - private void OnISO88591ToolStripMenuItemClick (object sender, EventArgs e) + private void OnEncodingToolStripMenuItemClick (object sender, EventArgs e) { - CurrentLogWindow?.ChangeEncoding(Encoding.Latin1); - } + var encoding = EncodingMenuBuilder.EncodingOf(sender as ToolStripItem); - [SupportedOSPlatform("windows")] - private void OnGB2312ToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312)); + if (encoding != null) + { + CurrentLogWindow?.ChangeEncoding(encoding); + } } [SupportedOSPlatform("windows")] diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs index e16d7d3e3..c7d641095 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs @@ -58,11 +58,6 @@ private void InitializeComponent () columnFinderToolStripMenuItem = new ToolStripMenuItem(); ToolStripSeparator5 = new ToolStripSeparator(); encodingToolStripMenuItem = new ToolStripMenuItem(); - encodingASCIIToolStripMenuItem = new ToolStripMenuItem(); - encodingISO88591toolStripMenuItem = new ToolStripMenuItem(); - encodingUTF8toolStripMenuItem = new ToolStripMenuItem(); - encodingUTF16toolStripMenuItem = new ToolStripMenuItem(); - encodingGB2312toolStripMenuItem = new ToolStripMenuItem(); ToolStripSeparator6 = new ToolStripSeparator(); timeshiftToolStripMenuItem = new ToolStripMenuItem(); timeshiftToolStripTextBox = new ToolStripTextBox(); @@ -438,56 +433,11 @@ private void InitializeComponent () // // encodingToolStripMenuItem // - encodingToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { encodingASCIIToolStripMenuItem, encodingISO88591toolStripMenuItem, encodingUTF8toolStripMenuItem, encodingUTF16toolStripMenuItem, encodingGB2312toolStripMenuItem }); + // Rows are added at runtime by EncodingMenuBuilder, from the single offered-encoding list. encodingToolStripMenuItem.Name = "encodingToolStripMenuItem"; encodingToolStripMenuItem.Size = new Size(177, 22); encodingToolStripMenuItem.Text = "Encoding"; // - // encodingASCIIToolStripMenuItem - // - encodingASCIIToolStripMenuItem.BackColor = SystemColors.Control; - encodingASCIIToolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; - encodingASCIIToolStripMenuItem.Name = "encodingASCIIToolStripMenuItem"; - encodingASCIIToolStripMenuItem.Size = new Size(132, 22); - encodingASCIIToolStripMenuItem.Text = "ASCII"; - encodingASCIIToolStripMenuItem.Click += OnASCIIToolStripMenuItemClick; - // - // encodingISO88591toolStripMenuItem - // - encodingISO88591toolStripMenuItem.BackColor = SystemColors.Control; - encodingISO88591toolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; - encodingISO88591toolStripMenuItem.Name = "encodingISO88591toolStripMenuItem"; - encodingISO88591toolStripMenuItem.Size = new Size(132, 22); - encodingISO88591toolStripMenuItem.Text = "ISO-8859-1"; - encodingISO88591toolStripMenuItem.Click += OnISO88591ToolStripMenuItemClick; - // - // encodingUTF8toolStripMenuItem - // - encodingUTF8toolStripMenuItem.BackColor = SystemColors.Control; - encodingUTF8toolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; - encodingUTF8toolStripMenuItem.Name = "encodingUTF8toolStripMenuItem"; - encodingUTF8toolStripMenuItem.Size = new Size(132, 22); - encodingUTF8toolStripMenuItem.Text = "UTF8"; - encodingUTF8toolStripMenuItem.Click += OnUTF8ToolStripMenuItemClick; - // - // encodingUTF16toolStripMenuItem - // - encodingUTF16toolStripMenuItem.BackColor = SystemColors.Control; - encodingUTF16toolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; - encodingUTF16toolStripMenuItem.Name = "encodingUTF16toolStripMenuItem"; - encodingUTF16toolStripMenuItem.Size = new Size(132, 22); - encodingUTF16toolStripMenuItem.Text = "Unicode"; - encodingUTF16toolStripMenuItem.Click += OnUTF16ToolStripMenuItemClick; - // - // encodingGB2312toolStripMenuItem - // - encodingGB2312toolStripMenuItem.BackColor = SystemColors.Control; - encodingGB2312toolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; - encodingGB2312toolStripMenuItem.Name = "encodingGB2312toolStripMenuItem"; - encodingGB2312toolStripMenuItem.Size = new Size(132, 22); - encodingGB2312toolStripMenuItem.Text = "GB2312"; - encodingGB2312toolStripMenuItem.Click += OnGB2312ToolStripMenuItemClick; - // // ToolStripSeparator6 // ToolStripSeparator6.Name = "ToolStripSeparator6"; @@ -1141,10 +1091,6 @@ private void InitializeComponent () private System.Windows.Forms.ToolStripMenuItem jumpToNextToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem jumpToPrevToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem encodingToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingASCIIToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingUTF8toolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingUTF16toolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingGB2312toolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem reloadToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem columnizerToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem; @@ -1194,7 +1140,6 @@ private void InitializeComponent () private ToolStripMenuItem disableWordHighlightModeToolStripMenuItem; private ToolStripMenuItem multifileMaskToolStripMenuItem; private ToolStripMenuItem multiFileEnabledStripMenuItem; - private ToolStripMenuItem encodingISO88591toolStripMenuItem; private ToolStripMenuItem lockInstanceToolStripMenuItem; private ToolStripMenuItem newFromClipboardToolStripMenuItem; private ToolStripMenuItem openURIToolStripMenuItem; diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index 152420c62..4620e18d0 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -124,8 +124,8 @@ public SettingsDialog (Preferences prefs, LogTabWindow logTabWin, int tabToOpen, /// /// The encoding the dialog falls back to when the persisted name is unusable — on both the way in /// (nothing to select) and the way out (nothing selected). Has to be one of - /// , otherwise the combo box shows a blank selection and OK writes - /// back a value the list never offered. + /// , otherwise the combo box shows a blank selection + /// and OK writes back a value the list never offered. /// internal static Encoding FallbackEncoding { get; } = Encoding.UTF8; @@ -696,14 +696,16 @@ private void DisplayCurrentIcon () } /// - /// Populates the encoding list in the combo box from . The value - /// member of the combo box is set to a specific header name defined in the resources. + /// Populates the encoding combo box from — the same + /// list the per-file View → Encoding menu is built from. The value member is set to a specific + /// header name defined in the resources; WinForms falls back to it for the display text, so each row + /// reads as its . /// private void FillEncodingList () { comboBoxEncoding.Items.Clear(); - foreach (var encoding in GetAvailableEncodings()) + foreach (var encoding in EncodingRegistry.OfferedEncodings) { _ = comboBoxEncoding.Items.Add(encoding); } @@ -711,36 +713,6 @@ private void FillEncodingList () comboBoxEncoding.ValueMember = Resources.SettingsDialog_UI_ComboBox_Encoding_ValueMember_HeaderName; } - /// - /// The encodings offered as the default encoding: ASCII, ISO-8859-1, UTF-8, Unicode, Windows-1250, - /// Windows-1252 and GB2312. - /// - /// - /// Separate from so the offered set can be asserted without building - /// the dialog. The selected entry is saved by name and reselected by equality on the next start, so - /// every entry has to be resolvable by name (which is why the code pages go through - /// ) and no two entries may share a code page — Encoding.Default - /// is deliberately absent because it is code page 65001 just like , - /// differing only in the BOM it emits, which a read-side default encoding never uses. - /// - /// New entries are appended rather than sorted in, so an existing user's row does not move under - /// the cursor on upgrade. - /// - /// - internal static IReadOnlyList GetAvailableEncodings () - { - return - [ - Encoding.ASCII, - Encoding.Latin1, - Encoding.UTF8, - Encoding.Unicode, - EncodingRegistry.GetEncoding(1250), - EncodingRegistry.GetEncoding(1252), - EncodingRegistry.GetEncoding(EncodingRegistry.CODE_PAGE_GB2312) - ]; - } - /// /// Populates the language selection list with available language options. /// diff --git a/src/LogExpert.UI/Services/MenuToolbarService/EncodingMenuBuilder.cs b/src/LogExpert.UI/Services/MenuToolbarService/EncodingMenuBuilder.cs new file mode 100644 index 000000000..18ce4ff18 --- /dev/null +++ b/src/LogExpert.UI/Services/MenuToolbarService/EncodingMenuBuilder.cs @@ -0,0 +1,70 @@ +using System; +using System.Runtime.Versioning; +using System.Text; + +using LogExpert.Core.Helpers; + +namespace LogExpert.UI.Services.MenuToolbarService; + +/// +/// Builds the View → Encoding dropdown from , and reads +/// the encoding back off a row. +/// +/// +/// The rows used to be hand-declared in the designer, one per encoding, each with its own click handler +/// and its own branch in the check-state lookup — so the menu could (and did) drift from the Preferences +/// list, and adding an encoding meant editing all three places. Here every row carries the +/// it stands for in its Tag: the handler applies whatever the clicked row +/// carries and the check-state lookup compares against it, so neither knows the list. +/// +[SupportedOSPlatform("windows")] +internal static class EncodingMenuBuilder +{ + /// + /// Replaces 's rows with one per offered encoding, labelled with its + /// — the same name the Preferences combo box shows. + /// + /// The Encoding dropdown to fill. + /// Handler for a row click; read the encoding with . + internal static void Fill (ToolStripMenuItem encodingMenu, EventHandler onRowClick) + { + ArgumentNullException.ThrowIfNull(encodingMenu); + + encodingMenu.DropDownItems.Clear(); + + foreach (var encoding in EncodingRegistry.OfferedEncodings) + { + var row = new ToolStripMenuItem(encoding.HeaderName) + { + Name = RowName(encoding), + Tag = encoding, + // Carried over from the designer-declared rows, which set both on every encoding row. + BackColor = SystemColors.Control, + ForeColor = SystemColors.ControlDarkDark + }; + + row.Click += onRowClick; + + _ = encodingMenu.DropDownItems.Add(row); + } + } + + /// + /// The designer-style name of the row for . Keyed on the code page + /// because that is what identifies a row; the header name is display text. + /// + internal static string RowName (Encoding encoding) + { + ArgumentNullException.ThrowIfNull(encoding); + + return $"encoding{encoding.CodePage}ToolStripMenuItem"; + } + + /// + /// The encoding stands for, or null when it is not an encoding row. + /// + internal static Encoding EncodingOf (ToolStripItem row) + { + return row?.Tag as Encoding; + } +} diff --git a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs index 9b4231430..fbe8a851c 100644 --- a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs +++ b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs @@ -4,7 +4,6 @@ using System.Text; using LogExpert.Core.EventArguments; -using LogExpert.Core.Helpers; using LogExpert.Dialogs; using LogExpert.UI.Interface; @@ -37,13 +36,9 @@ internal sealed class MenuToolbarController : IMenuToolbarController private ToolStripMenuItem _cellSelectMenuItem; private ToolStripMenuItem _columnFinderMenuItem; - // Encoding menu items + // The Encoding dropdown. Its rows are built by EncodingMenuBuilder and carry the encoding they + // stand for, so this controller never names an individual encoding. private ToolStripMenuItem _encodingMenuItem; - private ToolStripMenuItem _encodingAsciiMenuItem; - private ToolStripMenuItem _encodingUtf8MenuItem; - private ToolStripMenuItem _encodingUtf16MenuItem; - private ToolStripMenuItem _encodingIso88591MenuItem; - private ToolStripMenuItem _encodingGb2312MenuItem; // Toolbar items private ToolStripButton _bubblesButton; @@ -97,11 +92,6 @@ public void InitializeMenus (MenuStrip mainMenu, ToolStrip buttonToolbar, // Encoding menu items _encodingMenuItem = FindMenuItem("encodingToolStripMenuItem"); - _encodingAsciiMenuItem = FindMenuItem("encodingASCIIToolStripMenuItem"); - _encodingUtf8MenuItem = FindMenuItem("encodingUTF8toolStripMenuItem"); - _encodingUtf16MenuItem = FindMenuItem("encodingUTF16toolStripMenuItem"); - _encodingIso88591MenuItem = FindMenuItem("encodingISO88591toolStripMenuItem"); - _encodingGb2312MenuItem = FindMenuItem("encodingGB2312toolStripMenuItem"); // Toolbar items _bubblesButton = FindToolStripItem(_buttonToolbar, "toolStripButtonBubbles"); @@ -201,66 +191,26 @@ public void UpdateEncodingMenu (Encoding currentEncoding) return; } - // Clear every row through the dropdown rather than row by row, so a row added to the menu - // cannot be left checked by a clear list nobody remembered to extend. - if (_encodingMenuItem != null) + if (_encodingMenuItem == null) { - foreach (var row in _encodingMenuItem.DropDownItems) - { - SetCheckedSafe(row as ToolStripMenuItem, false); - } - } - - SetCheckedSafe(MenuItemFor(currentEncoding), true); - } - - /// - /// The encoding menu row representing , or null when the file is read - /// with an encoding the menu does not offer (a Preferences default such as windows-1250, say). - /// - /// - /// Matched by code page rather than by runtime type, because several instances stand for the same - /// row: the menu applies UTF-8 without a BOM while the Preferences default is - /// with one, and Encoding.Default — what a file with neither a - /// preamble nor a configured default is read with — is a third UTF-8 instance. Type and equality - /// checks put those on different rows (or, before the "ANSI" row was dropped, on the row for a - /// different encoding entirely). - /// - private ToolStripMenuItem MenuItemFor (Encoding encoding) - { - if (encoding == null) - { - return null; - } - - var codePage = encoding.CodePage; - - if (codePage == Encoding.ASCII.CodePage) - { - return _encodingAsciiMenuItem; + return; } - if (codePage == Encoding.UTF8.CodePage) - { - return _encodingUtf8MenuItem; - } + // Matched by code page, not by instance: several Encoding instances stand for the same row — + // the menu applies UTF-8 without a BOM, the Preferences default is Encoding.UTF8 with one, and + // Encoding.Default (what a file with neither a preamble nor a configured default is read with) + // is a third UTF-8 instance. A file read with an encoding the menu does not offer leaves every + // row unchecked, which is honest: no offered row would reproduce it. + var currentCodePage = currentEncoding?.CodePage; - if (codePage == Encoding.Unicode.CodePage || codePage == Encoding.BigEndianUnicode.CodePage) + foreach (var row in _encodingMenuItem.DropDownItems) { - return _encodingUtf16MenuItem; - } - - if (codePage == Encoding.Latin1.CodePage) - { - return _encodingIso88591MenuItem; - } - - if (codePage == EncodingRegistry.CODE_PAGE_GB2312) - { - return _encodingGb2312MenuItem; + if (row is ToolStripMenuItem encodingRow) + { + encodingRow.Checked = currentCodePage != null + && EncodingMenuBuilder.EncodingOf(encodingRow)?.CodePage == currentCodePage; + } } - - return null; } public void UpdateHighlightGroups (IEnumerable groups, string selectedGroup) @@ -437,7 +387,7 @@ private void LogMissingItems () LogIfNull(_searchMenuItem, "searchToolStripMenuItem"); LogIfNull(_filterMenuItem, "filterToolStripMenuItem"); LogIfNull(_timeshiftMenuItem, "timeshiftToolStripMenuItem"); - LogIfNull(_encodingAsciiMenuItem, "encodingASCIIToolStripMenuItem"); + LogIfNull(_encodingMenuItem, "encodingToolStripMenuItem"); LogIfNull(_highlightGroupCombo, "highlightGroupsToolStripComboBox"); LogIfNull(_lastUsedMenuItem, "lastUsedToolStripMenuItem"); }